Initial commit
This commit is contained in:
94
application/Espo/Tools/ActionHistory/Service.php
Normal file
94
application/Espo/Tools/ActionHistory/Service.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\ActionHistory;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Record\ActionHistory\Action;
|
||||
use Espo\Core\Record\Collection as RecordCollection;
|
||||
use Espo\Entities\ActionHistoryRecord;
|
||||
use Espo\Core\FieldProcessing\ListLoadProcessor;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class Service
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private EntityManager $entityManager,
|
||||
private User $user,
|
||||
private ListLoadProcessor $listLoadProcessor
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return RecordCollection<ActionHistoryRecord>
|
||||
*/
|
||||
public function getLastViewed(?int $maxSize, ?int $offset): RecordCollection
|
||||
{
|
||||
$scopes = $this->metadata->get('scopes');
|
||||
|
||||
$targetTypeList = array_filter(
|
||||
array_keys($scopes),
|
||||
function ($item) use ($scopes) {
|
||||
return !empty($scopes[$item]['object']) || !empty($scopes[$item]['lastViewed']);
|
||||
}
|
||||
);
|
||||
|
||||
$maxSize = $maxSize ?? 0;
|
||||
$offset = $offset ?? 0;
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepositoryByClass(ActionHistoryRecord::class)
|
||||
->where([
|
||||
'userId' => $this->user->getId(),
|
||||
'action' => Action::READ,
|
||||
'targetType' => $targetTypeList,
|
||||
])
|
||||
->order('MAX:' . Field::CREATED_AT, 'DESC')
|
||||
->select([
|
||||
'targetId',
|
||||
'targetType',
|
||||
'MAX:number',
|
||||
['MAX:createdAt', Field::CREATED_AT],
|
||||
])
|
||||
->group(['targetId', 'targetType'])
|
||||
->limit($offset, $maxSize + 1)
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
$this->listLoadProcessor->process($entity);
|
||||
|
||||
$entity->set('id', Util::generateId());
|
||||
}
|
||||
|
||||
return RecordCollection::createNoCount($collection, $maxSize);
|
||||
}
|
||||
}
|
||||
105
application/Espo/Tools/Address/CountryDefaultsPopulator.php
Normal file
105
application/Espo/Tools/Address/CountryDefaultsPopulator.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Address;
|
||||
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\File\Manager;
|
||||
use Espo\Core\Utils\Id\RecordIdGenerator;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Entities\AddressCountry;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\DeleteBuilder;
|
||||
use RuntimeException;
|
||||
|
||||
class CountryDefaultsPopulator
|
||||
{
|
||||
private string $file = 'application/Espo/Resources/data/locale/en_US/countryList.json';
|
||||
|
||||
private const CACHE_KEY = 'addressCountryData';
|
||||
|
||||
public function __construct(
|
||||
private Manager $fileManager,
|
||||
private EntityManager $entityManager,
|
||||
private DataCache $dataCache,
|
||||
private RecordIdGenerator $recordIdGenerator
|
||||
) {}
|
||||
|
||||
public function populate(): void
|
||||
{
|
||||
if (!$this->fileManager->exists($this->file)) {
|
||||
throw new RuntimeException("No file '$this->file'.");
|
||||
}
|
||||
|
||||
$contents = $this->fileManager->getContents($this->file);
|
||||
|
||||
$dataList = Json::decode($contents, true);
|
||||
|
||||
if (!is_array($dataList)) {
|
||||
throw new RuntimeException("Bad data.");
|
||||
}
|
||||
|
||||
$collection = $this->entityManager->getCollectionFactory()->create(AddressCountry::ENTITY_TYPE);
|
||||
|
||||
foreach ($dataList as $data) {
|
||||
if (!is_array($data)) {
|
||||
throw new RuntimeException("Bad data.");
|
||||
}
|
||||
|
||||
$name = $data['name'] ?? null;
|
||||
$code = $data['code'] ?? null;
|
||||
$isPreferred = $data['isPreferred'] ?? false;
|
||||
|
||||
if (!is_string($name) || !is_string($code)) {
|
||||
throw new RuntimeException("Bad data.");
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getNewEntity(AddressCountry::ENTITY_TYPE);
|
||||
|
||||
$entity->setMultiple([
|
||||
'id' => $this->recordIdGenerator->generate(),
|
||||
'name' => $name,
|
||||
'code' => $code,
|
||||
'isPreferred' => $isPreferred,
|
||||
]);
|
||||
|
||||
$collection->append($entity);
|
||||
}
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute(
|
||||
DeleteBuilder::create()
|
||||
->from(AddressCountry::ENTITY_TYPE)
|
||||
->build()
|
||||
);
|
||||
|
||||
$this->entityManager->getMapper()->massInsert($collection);
|
||||
|
||||
$this->dataCache->clear(self::CACHE_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\AdminNotifications\Jobs;
|
||||
|
||||
use Espo\Core\Job\JobDataLess;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
use Espo\Entities\Extension;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\Tools\AdminNotifications\LatestReleaseDataRequester;
|
||||
|
||||
/**
|
||||
* Checking for new extension versions.
|
||||
*/
|
||||
class CheckNewExtensionVersion implements JobDataLess
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private ConfigWriter $configWriter,
|
||||
private EntityManager $entityManager,
|
||||
private LatestReleaseDataRequester $requester
|
||||
) {}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
if (
|
||||
!$this->config->get('adminNotifications') ||
|
||||
!$this->config->get('adminNotificationsNewExtensionVersion')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->select()
|
||||
->from(Extension::ENTITY_TYPE)
|
||||
->select([Attribute::ID, Field::NAME, 'version', 'checkVersionUrl'])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'isInstalled' => true,
|
||||
])
|
||||
->order([Field::CREATED_AT])
|
||||
->build();
|
||||
|
||||
$sth = $this->entityManager->getQueryExecutor()->execute($query);
|
||||
|
||||
$latestReleases = [];
|
||||
|
||||
while ($row = $sth->fetch()) {
|
||||
$url = !empty($row['checkVersionUrl']) ? $row['checkVersionUrl'] : null;
|
||||
|
||||
$extensionName = $row['name'];
|
||||
|
||||
$latestRelease = $this->requester->request($url, [
|
||||
'name' => $extensionName,
|
||||
]);
|
||||
|
||||
if (!empty($latestRelease) && !isset($latestRelease['error'])) {
|
||||
$latestReleases[$extensionName] = $latestRelease;
|
||||
}
|
||||
}
|
||||
|
||||
$latestExtensionVersions = $this->config->get('latestExtensionVersions', []);
|
||||
|
||||
$save = false;
|
||||
|
||||
foreach ($latestReleases as $extensionName => $extensionData) {
|
||||
if (empty($latestExtensionVersions[$extensionName])) {
|
||||
$latestExtensionVersions[$extensionName] = $extensionData['version'];
|
||||
$save = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($latestExtensionVersions[$extensionName] != $extensionData['version']) {
|
||||
$latestExtensionVersions[$extensionName] = $extensionData['version'];
|
||||
|
||||
/*if (!empty($extensionData['notes'])) {
|
||||
//todo: create notification
|
||||
}*/
|
||||
|
||||
$save = true;
|
||||
|
||||
//continue;
|
||||
}
|
||||
|
||||
/*if (!empty($extensionData['notes'])) {
|
||||
//todo: find and modify notification
|
||||
}*/
|
||||
}
|
||||
|
||||
if ($save) {
|
||||
$this->configWriter->set('latestExtensionVersions', $latestExtensionVersions);
|
||||
|
||||
$this->configWriter->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\AdminNotifications\Jobs;
|
||||
|
||||
use Espo\Core\Job\JobDataLess;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
use Espo\Tools\AdminNotifications\LatestReleaseDataRequester;
|
||||
|
||||
/**
|
||||
* Checking for a new EspoCRM version.
|
||||
*/
|
||||
class CheckNewVersion implements JobDataLess
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private ConfigWriter $configWriter,
|
||||
private LatestReleaseDataRequester $requester
|
||||
) {}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
if (
|
||||
!$this->config->get('adminNotifications') ||
|
||||
!$this->config->get('adminNotificationsNewVersion')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$latestRelease = $this->requester->request();
|
||||
|
||||
if ($latestRelease === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($latestRelease['version'])) {
|
||||
// @todo Check the logic. WTF?
|
||||
$this->configWriter->set('latestVersion', $latestRelease['version']);
|
||||
|
||||
$this->configWriter->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->config->get('latestVersion') != $latestRelease['version']) {
|
||||
$this->configWriter->set('latestVersion', $latestRelease['version']);
|
||||
|
||||
/*if (!empty($latestRelease['notes'])) {
|
||||
// @todo Create a notification.
|
||||
}*/
|
||||
|
||||
$this->configWriter->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*if (!empty($latestRelease['notes'])) {
|
||||
// @todo Find and modify notification.
|
||||
}*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\AdminNotifications;
|
||||
|
||||
class LatestReleaseDataRequester
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $requestData
|
||||
* @return ?array<int|string, mixed>
|
||||
*/
|
||||
public function request(
|
||||
?string $url = null,
|
||||
array $requestData = [],
|
||||
string $urlPath = 'release/latest'
|
||||
): ?array {
|
||||
|
||||
if (!function_exists('curl_version')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
$requestUrl = $url ? trim($url) : base64_decode('aHR0cHM6Ly9zLmVzcG9jcm0uY29tLw==');
|
||||
$requestUrl = str_ends_with($requestUrl, '/') ? $requestUrl : $requestUrl . '/';
|
||||
|
||||
$requestUrl .= empty($requestData) ?
|
||||
$urlPath . '/' :
|
||||
$urlPath . '/?' . http_build_query($requestData);
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $requestUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
|
||||
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS | CURLPROTO_HTTP);
|
||||
|
||||
/** @var string|false $result */
|
||||
$result = curl_exec($ch);
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($result, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
278
application/Espo/Tools/AdminNotifications/Manager.php
Normal file
278
application/Espo/Tools/AdminNotifications/Manager.php
Normal file
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\AdminNotifications;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Core\Utils\ScheduledJob;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Entities\Extension;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* Notifications on the admin panel.
|
||||
*/
|
||||
class Manager
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Config $config,
|
||||
private Language $language,
|
||||
private ScheduledJob $scheduledJob,
|
||||
private Config\SystemConfig $systemConfig,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: string, type: string, message: string}>
|
||||
*/
|
||||
public function getNotificationList(): array
|
||||
{
|
||||
$notificationList = [];
|
||||
|
||||
if (!$this->config->get('adminNotifications')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$this->systemConfig->useCache()) {
|
||||
$notificationList[] = [
|
||||
'id' => 'cacheIsDisabled',
|
||||
'type' => 'cacheIsDisabled',
|
||||
'message' => $this->language->translateLabel('cacheIsDisabled', 'messages', 'Admin'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->config->get('adminNotificationsCronIsNotConfigured')) {
|
||||
if ($this->config->get('cronDisabled')) {
|
||||
$notificationList[] = [
|
||||
'id' => 'cronIsDisabled',
|
||||
'type' => 'cronIsDisabled',
|
||||
'message' => $this->language->translateLabel('cronIsDisabled', 'messages', 'Admin'),
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->isCronConfigured()) {
|
||||
$notificationList[] = [
|
||||
'id' => 'cronIsNotConfigured',
|
||||
'type' => 'cronIsNotConfigured',
|
||||
'message' => $this->language->translateLabel('cronIsNotConfigured', 'messages', 'Admin'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->config->get('adminNotificationsNewVersion')) {
|
||||
$instanceNeedingUpgrade = $this->getInstanceNeedingUpgrade();
|
||||
|
||||
if (!empty($instanceNeedingUpgrade)) {
|
||||
$message = $this->language->translateLabel('newVersionIsAvailable', 'messages', 'Admin');
|
||||
|
||||
$notificationList[] = [
|
||||
'id' => 'newVersionIsAvailable',
|
||||
'type' => 'newVersionIsAvailable',
|
||||
'message' => $this->prepareMessage($message, $instanceNeedingUpgrade),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->config->get('adminNotificationsNewExtensionVersion')) {
|
||||
$extensionsNeedingUpgrade = $this->getExtensionsNeedingUpgrade();
|
||||
|
||||
foreach ($extensionsNeedingUpgrade as $extensionName => $extensionDetails) {
|
||||
$label = 'new' . Util::toCamelCase($extensionName, ' ', true) . 'VersionIsAvailable';
|
||||
|
||||
$message = $this->language->get(['Admin', 'messages', $label]);
|
||||
|
||||
if (!$message) {
|
||||
$message = $this->language
|
||||
->translate('newExtensionVersionIsAvailable', 'messages', 'Admin');
|
||||
}
|
||||
|
||||
$notificationList[] = [
|
||||
'id' => 'newExtensionVersionIsAvailable' . Util::toCamelCase($extensionName, ' ', true),
|
||||
'type' => 'newExtensionVersionIsAvailable',
|
||||
'message' => $this->prepareMessage($message, $extensionDetails)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->config->get('adminNotificationsExtensionLicenseDisabled')) {
|
||||
$notificationList = array_merge(
|
||||
$notificationList,
|
||||
$this->getExtensionLicenseNotificationList()
|
||||
);
|
||||
}
|
||||
|
||||
return $notificationList;
|
||||
}
|
||||
|
||||
private function isCronConfigured(): bool
|
||||
{
|
||||
return $this->scheduledJob->isCronConfigured();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array{currentVersion:string,latestVersion:string}
|
||||
*/
|
||||
private function getInstanceNeedingUpgrade(): ?array
|
||||
{
|
||||
$latestVersion = $this->config->get('latestVersion');
|
||||
|
||||
if (!isset($latestVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentVersion = $this->systemConfig->getVersion();
|
||||
|
||||
if ($currentVersion === 'dev') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (version_compare($latestVersion, $currentVersion, '>')) {
|
||||
return [
|
||||
'currentVersion' => $currentVersion,
|
||||
'latestVersion' => $latestVersion,
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return array<string, array{currentVersion: string, latestVersion: string, extensionName: string}>
|
||||
*/
|
||||
private function getExtensionsNeedingUpgrade(): array
|
||||
{
|
||||
$extensions = [];
|
||||
|
||||
$latestExtensionVersions = $this->config->get('latestExtensionVersions');
|
||||
|
||||
if (empty($latestExtensionVersions) || !is_array($latestExtensionVersions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($latestExtensionVersions as $extensionName => $extensionLatestVersion) {
|
||||
$currentVersion = $this->getExtensionLatestInstalledVersion($extensionName);
|
||||
|
||||
if (isset($currentVersion) && version_compare($extensionLatestVersion, $currentVersion, '>')) {
|
||||
$extensions[$extensionName] = [
|
||||
'currentVersion' => $currentVersion,
|
||||
'latestVersion' => $extensionLatestVersion,
|
||||
'extensionName' => $extensionName,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
private function getExtensionLatestInstalledVersion(string $extensionName): ?string
|
||||
{
|
||||
$extension = $this->entityManager
|
||||
->getRDBRepository(Extension::ENTITY_TYPE)
|
||||
->select(['version'])
|
||||
->where([
|
||||
'name' => $extensionName,
|
||||
'isInstalled' => true,
|
||||
])
|
||||
->order(Field::CREATED_AT, true)
|
||||
->findOne();
|
||||
|
||||
if (!$extension) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $extension->get('version');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $data
|
||||
*/
|
||||
private function prepareMessage(string $message, array $data = []): string
|
||||
{
|
||||
foreach ($data as $name => $value) {
|
||||
$message = str_replace('{'.$name.'}', $value, $message);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: string, type: string, message: string}>
|
||||
*/
|
||||
private function getExtensionLicenseNotificationList(): array
|
||||
{
|
||||
$extensionList = $this->entityManager
|
||||
->getRDBRepositoryByClass(Extension::class)
|
||||
->where([
|
||||
'licenseStatus' => [
|
||||
Extension::LICENSE_STATUS_INVALID,
|
||||
Extension::LICENSE_STATUS_EXPIRED,
|
||||
Extension::LICENSE_STATUS_SOFT_EXPIRED,
|
||||
],
|
||||
])
|
||||
->find();
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($extensionList as $extension) {
|
||||
$message =
|
||||
$extension->getLicenseStatusMessage() ??
|
||||
$this->getExtensionLicenseMessageLabel($extension);
|
||||
|
||||
if (!$message) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$message = $this->language->translateLabel($message, 'messages');
|
||||
|
||||
$name = $extension->getName();
|
||||
|
||||
$list[] = [
|
||||
'id' => 'newExtensionVersionIsAvailable' . Util::toCamelCase($name, ' ', true),
|
||||
'type' => 'newExtensionVersionIsAvailable',
|
||||
'message' => $this->prepareMessage($message, ['name' => $name]),
|
||||
];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function getExtensionLicenseMessageLabel(Extension $extension): ?string
|
||||
{
|
||||
$status = $extension->getLicenseStatus();
|
||||
|
||||
if (!$status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'extensionLicense' . ucfirst(Util::hyphenToCamelCase($status));
|
||||
}
|
||||
}
|
||||
88
application/Espo/Tools/Api/Cors/DefaultHelper.php
Normal file
88
application/Espo/Tools/Api/Cors/DefaultHelper.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Api\Cors;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Psr\Http\Message\RequestInterface as Request;
|
||||
|
||||
class DefaultHelper implements Helper
|
||||
{
|
||||
public function __construct(private Config $config) {}
|
||||
|
||||
public function isCredentialsAllowed(Request $request): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAllowedOrigin(Request $request): ?string
|
||||
{
|
||||
$origin = $request->getHeaderLine('Origin');
|
||||
|
||||
if (!$origin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return in_array($origin, $this->getAllowedOrigins()) ?
|
||||
$origin :
|
||||
null;
|
||||
}
|
||||
|
||||
public function getAllowedMethods(Request $request): array
|
||||
{
|
||||
return $this->config->get('apiCorsAllowedMethodList') ?? [];
|
||||
}
|
||||
|
||||
public function getAllowedHeaders(Request $request): array
|
||||
{
|
||||
if (!$request->hasHeader('Access-Control-Request-Headers')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->config->get('apiCorsAllowedHeaderList') ?? [];
|
||||
}
|
||||
|
||||
public function getSuccessStatus(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getMaxAge(): ?int
|
||||
{
|
||||
return $this->config->get('apiCorsMaxAge');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getAllowedOrigins(): array
|
||||
{
|
||||
return $this->config->get('apiCorsAllowedOriginList') ?? [];
|
||||
}
|
||||
}
|
||||
53
application/Espo/Tools/Api/Cors/Helper.php
Normal file
53
application/Espo/Tools/Api/Cors/Helper.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Api\Cors;
|
||||
|
||||
use Psr\Http\Message\RequestInterface as Request;
|
||||
|
||||
interface Helper
|
||||
{
|
||||
public function isCredentialsAllowed(Request $request): bool;
|
||||
|
||||
public function getAllowedOrigin(Request $request): ?string;
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAllowedMethods(Request $request): array;
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAllowedHeaders(Request $request): array;
|
||||
|
||||
public function getSuccessStatus(): ?int;
|
||||
|
||||
public function getMaxAge(): ?int;
|
||||
}
|
||||
89
application/Espo/Tools/Api/Cors/Middleware.php
Normal file
89
application/Espo/Tools/Api/Cors/Middleware.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Api\Cors;
|
||||
|
||||
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as ServerRequest;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
use Slim\Psr7\Factory\ResponseFactory;
|
||||
|
||||
class Middleware implements MiddlewareInterface
|
||||
{
|
||||
private const DEFAULT_SUCCESS_STATUS = 204;
|
||||
private const DEFAULT_MAX_AGE = 86400;
|
||||
|
||||
public function __construct(private Helper $helper)
|
||||
{}
|
||||
|
||||
public function process(ServerRequest $request, RequestHandler $handler): Response
|
||||
{
|
||||
$isPreFlight = $request->getMethod() === RequestMethod::METHOD_OPTIONS;
|
||||
|
||||
$response = $isPreFlight ?
|
||||
(new ResponseFactory)->createResponse() :
|
||||
$handler->handle($request);
|
||||
|
||||
$allowedOrigin = $this->helper->getAllowedOrigin($request);
|
||||
|
||||
if (!$allowedOrigin) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$status = $this->helper->getSuccessStatus() ?? self::DEFAULT_SUCCESS_STATUS;
|
||||
$allowedMethods = $this->helper->getAllowedMethods($request);
|
||||
$allowedHeaders = $this->helper->getAllowedHeaders($request);
|
||||
$maxAge = $this->helper->getMaxAge() ?? self::DEFAULT_MAX_AGE;
|
||||
$credentialsAllowed = $this->helper->isCredentialsAllowed($request);
|
||||
|
||||
$response = $response
|
||||
->withHeader('Access-Control-Allow-Origin', $allowedOrigin)
|
||||
->withHeader('Access-Control-Max-Age', (string) $maxAge);
|
||||
|
||||
if ($credentialsAllowed) {
|
||||
$response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
|
||||
if (!$isPreFlight) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($allowedMethods !== []) {
|
||||
$response = $response->withHeader('Access-Control-Allow-Methods', implode(', ', $allowedMethods));
|
||||
}
|
||||
|
||||
if ($allowedHeaders !== []) {
|
||||
$response = $response->withHeader('Access-Control-Allow-Headers', implode(', ', $allowedHeaders));
|
||||
}
|
||||
|
||||
return $response->withStatus($status);
|
||||
}
|
||||
}
|
||||
58
application/Espo/Tools/App/Api/GetAbout.php
Normal file
58
application/Espo/Tools/App/Api/GetAbout.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Resource\FileReader;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GetAbout implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private FileReader $fileReader,
|
||||
private Config\SystemConfig $systemConfig,
|
||||
) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$text = $this->fileReader->read('texts/about.md', FileReader\Params::create());
|
||||
|
||||
return ResponseComposer::json([
|
||||
'text' => $text,
|
||||
'version' => $this->systemConfig->getVersion(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
54
application/Espo/Tools/App/Api/GetUser.php
Normal file
54
application/Espo/Tools/App/Api/GetUser.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Tools\App\AppService as Service;
|
||||
|
||||
/**
|
||||
* Gets user data.
|
||||
*/
|
||||
class GetUser implements Action
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $this->injectableFactory
|
||||
->create(Service::class)
|
||||
->getUserData();
|
||||
|
||||
return ResponseComposer::json($data);
|
||||
}
|
||||
}
|
||||
67
application/Espo/Tools/App/Api/PostDestroyAuthToken.php
Normal file
67
application/Espo/Tools/App/Api/PostDestroyAuthToken.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Authentication\AuthenticationFactory;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
class PostDestroyAuthToken implements Action
|
||||
{
|
||||
public function __construct(private AuthenticationFactory $authenticationFactory) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$token = $data->token ?? null;
|
||||
|
||||
if (!$token || !is_string($token)) {
|
||||
throw new BadRequest("No `token`.");
|
||||
}
|
||||
|
||||
$authentication = $this->authenticationFactory->create();
|
||||
|
||||
$response = ResponseComposer::empty();
|
||||
|
||||
try {
|
||||
$authentication->destroyAuthToken($token, $request, $response);
|
||||
} catch (NotFound) {
|
||||
return $response->writeBody(Json::encode(false));
|
||||
}
|
||||
|
||||
return $response->writeBody(Json::encode(true));
|
||||
}
|
||||
}
|
||||
40
application/Espo/Tools/App/AppParam.php
Normal file
40
application/Espo/Tools/App/AppParam.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App;
|
||||
|
||||
/**
|
||||
* App parameter to be passed to the frontend.
|
||||
*
|
||||
* @see https://docs.espocrm.com/development/app-params/
|
||||
*/
|
||||
interface AppParam
|
||||
{
|
||||
public function get(): mixed;
|
||||
}
|
||||
515
application/Espo/Tools/App/AppService.php
Normal file
515
application/Espo/Tools/App/AppService.php
Normal file
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App;
|
||||
|
||||
use Espo\Core\Authentication\Util\MethodProvider as AuthenticationMethodProvider;
|
||||
use Espo\Core\Mail\ConfigDataProvider as EmailConfigDataProvider;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Name\Link;
|
||||
use Espo\Core\Utils\SystemUser;
|
||||
use Espo\Entities\DashboardTemplate;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailAccount;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\Entities\Settings;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Authentication\Logins\Espo;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\Preferences;
|
||||
use Espo\ORM\EntityManager;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
class AppService
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $forbiddenUserAttributeList = [
|
||||
'apiKey',
|
||||
'authTokenId',
|
||||
'password',
|
||||
'rolesIds',
|
||||
'rolesNames',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private array $allowedUserAttributeList = [
|
||||
'type',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private array $allowedInternalUserAttributeList = [
|
||||
'teamsIds',
|
||||
'defaultTeamId',
|
||||
'defaultTeamName',
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private array $allowedPortalUserAttributeList = [
|
||||
'contactId',
|
||||
'contactName',
|
||||
'accountId',
|
||||
'accountsIds',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private EntityManager $entityManager,
|
||||
private Metadata $metadata,
|
||||
private Acl $acl,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private SettingsService $settingsService,
|
||||
private User $user,
|
||||
private Preferences $preferences,
|
||||
private FieldUtil $fieldUtil,
|
||||
private Log $log,
|
||||
private AuthenticationMethodProvider $authenticationMethodProvider,
|
||||
private SystemUser $systemUser,
|
||||
private EmailConfigDataProvider $emailConfigDataProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getUserData(): array
|
||||
{
|
||||
$preferencesData = $this->preferences->getValueMap();
|
||||
|
||||
$this->filterPreferencesData($preferencesData);
|
||||
|
||||
$user = $this->user;
|
||||
|
||||
if (!$user->has('teamsIds')) {
|
||||
$user->loadLinkMultipleField(Field::TEAMS);
|
||||
}
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$user->loadAccountField();
|
||||
$user->loadLinkMultipleField('accounts');
|
||||
}
|
||||
|
||||
$settings = $this->settingsService->getConfigData();
|
||||
|
||||
$dashboardTemplateId = $user->get('dashboardTemplateId');
|
||||
|
||||
if ($dashboardTemplateId) {
|
||||
$dashboardTemplate = $this->entityManager
|
||||
->getEntityById(DashboardTemplate::ENTITY_TYPE, $dashboardTemplateId);
|
||||
|
||||
if ($dashboardTemplate) {
|
||||
$settings->forcedDashletsOptions = $dashboardTemplate->get('dashletsOptions') ?? (object) [];
|
||||
$settings->forcedDashboardLayout = $dashboardTemplate->get('layout') ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
$language = Language::detectLanguage($this->config, $this->preferences);
|
||||
|
||||
return [
|
||||
'user' => $this->getUserDataForFrontend(),
|
||||
'acl' => $this->getAclDataForFrontend(),
|
||||
'preferences' => $preferencesData,
|
||||
'token' => $this->user->get('token'),
|
||||
'settings' => $settings,
|
||||
'language' => $language,
|
||||
'appParams' => $this->getAppParams(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getAppParams(): array
|
||||
{
|
||||
$user = $this->user;
|
||||
|
||||
$auth2FARequired =
|
||||
$user->isRegular() &&
|
||||
$this->config->get('auth2FA') &&
|
||||
$this->config->get('auth2FAForced') &&
|
||||
!$user->get('auth2FA');
|
||||
|
||||
$authenticationMethod = $this->authenticationMethodProvider->get();
|
||||
|
||||
$passwordChangeForNonAdminDisabled = $authenticationMethod !== Espo::NAME;
|
||||
$logoutWait = (bool) $this->metadata->get(['authenticationMethods', $authenticationMethod, 'logoutClassName']);
|
||||
|
||||
$timeZoneList = $this->metadata
|
||||
->get(['entityDefs', Settings::ENTITY_TYPE, 'fields', 'timeZone', 'options']) ?? [];
|
||||
|
||||
$appParams = [
|
||||
'maxUploadSize' => $this->getMaxUploadSize() / 1024.0 / 1024.0,
|
||||
'isRestrictedMode' => $this->config->get('restrictedMode'),
|
||||
'passwordChangeForNonAdminDisabled' => $passwordChangeForNonAdminDisabled,
|
||||
'timeZoneList' => $timeZoneList,
|
||||
'auth2FARequired' => $auth2FARequired,
|
||||
'logoutWait' => $logoutWait,
|
||||
'systemUserId' => $this->systemUser->getId(),
|
||||
];
|
||||
|
||||
/** @var array<string, array<string, mixed>> $map */
|
||||
$map = $this->metadata->get(['app', 'appParams']) ?? [];
|
||||
|
||||
foreach ($map as $paramKey => $item) {
|
||||
/** @var ?class-string<AppParam> $className */
|
||||
$className = $item['className'] ?? null;
|
||||
|
||||
if (!$className) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var AppParam $obj */
|
||||
$obj = $this->injectableFactory->create($className);
|
||||
|
||||
$itemParams = $obj->get();
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error("AppParam $paramKey: " . $e->getMessage(), ['exception' => $e]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$appParams[$paramKey] = $itemParams;
|
||||
}
|
||||
|
||||
return $appParams;
|
||||
}
|
||||
|
||||
private function getUserDataForFrontend(): stdClass
|
||||
{
|
||||
$user = $this->user;
|
||||
|
||||
$data = $user->getValueMap();
|
||||
|
||||
$emailAddressData = $this->getEmailAddressData();
|
||||
|
||||
$data->emailAddressList = $emailAddressData['emailAddressList'];
|
||||
$data->userEmailAddressList = $emailAddressData['userEmailAddressList'];
|
||||
$data->excludeFromReplyEmailAddressList = $emailAddressData['excludeFromReplyEmailAddressList'];
|
||||
|
||||
foreach ($this->forbiddenUserAttributeList as $attribute) {
|
||||
unset($data->$attribute);
|
||||
}
|
||||
|
||||
$forbiddenAttributeList = $this->acl->getScopeForbiddenAttributeList(User::ENTITY_TYPE);
|
||||
|
||||
$isPortal = $user->isPortal();
|
||||
|
||||
foreach ($forbiddenAttributeList as $attribute) {
|
||||
if (in_array($attribute, $this->allowedUserAttributeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($isPortal && in_array($attribute, $this->allowedPortalUserAttributeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$isPortal && in_array($attribute, $this->allowedInternalUserAttributeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unset($data->$attribute);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getAclDataForFrontend(): stdClass
|
||||
{
|
||||
$data = $this->acl->getMapData();
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
$data = unserialize(serialize($data));
|
||||
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['scopes'], []));
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
if (!$this->acl->check($scope)) {
|
||||
unset($data->table->$scope);
|
||||
unset($data->fieldTable->$scope);
|
||||
unset($data->fieldTableQuickAccess->$scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* emailAddressList: string[],
|
||||
* userEmailAddressList: string[],
|
||||
* excludeFromReplyEmailAddressList: string[],
|
||||
* }
|
||||
*/
|
||||
private function getEmailAddressData(): array
|
||||
{
|
||||
$user = $this->user;
|
||||
|
||||
$systemIsShared = $this->emailConfigDataProvider->isSystemOutboundAddressShared();
|
||||
$systemAddress = $this->emailConfigDataProvider->getSystemOutboundAddress();
|
||||
|
||||
$addressList = [];
|
||||
$userAddressList = [];
|
||||
|
||||
/** @var iterable<EmailAddress> $emailAddresses */
|
||||
$emailAddresses = $this->entityManager
|
||||
->getRelation($user, Link::EMAIL_ADDRESSES)
|
||||
->find();
|
||||
|
||||
foreach ($emailAddresses as $emailAddress) {
|
||||
if ($emailAddress->isInvalid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$userAddressList[] = $emailAddress->getAddress();
|
||||
|
||||
if ($user->getEmailAddress() === $emailAddress->getAddress()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$addressList[] = $emailAddress->getAddress();
|
||||
}
|
||||
|
||||
if ($user->getEmailAddress()) {
|
||||
array_unshift($addressList, $user->getEmailAddress());
|
||||
}
|
||||
|
||||
if (!$systemIsShared) {
|
||||
$addressList = $this->filterUserEmailAddressList($user, $addressList);
|
||||
}
|
||||
|
||||
$addressList = array_merge($addressList, $this->getUserGroupEmailAddressList($user));
|
||||
|
||||
if ($systemIsShared && $systemAddress) {
|
||||
$addressList[] = $systemAddress;
|
||||
}
|
||||
|
||||
$addressList = array_values(array_unique($addressList));
|
||||
|
||||
return [
|
||||
'emailAddressList' => $addressList,
|
||||
'userEmailAddressList' => $userAddressList,
|
||||
'excludeFromReplyEmailAddressList' => $this->getExcludeFromReplyAddressList(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $emailAddressList
|
||||
* @return string[]
|
||||
*/
|
||||
private function filterUserEmailAddressList(User $user, array $emailAddressList): array
|
||||
{
|
||||
$emailAccountCollection = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailAccount::class)
|
||||
->select([
|
||||
Attribute::ID,
|
||||
Field::EMAIL_ADDRESS,
|
||||
])
|
||||
->where([
|
||||
'assignedUserId' => $user->getId(),
|
||||
'useSmtp' => true,
|
||||
'status' => EmailAccount::STATUS_ACTIVE,
|
||||
])
|
||||
->find();
|
||||
|
||||
$inAccountList = array_map(
|
||||
fn (EmailAccount $e) => $e->getEmailAddress(),
|
||||
[...$emailAccountCollection]
|
||||
);
|
||||
|
||||
return array_values(array_filter(
|
||||
$emailAddressList,
|
||||
fn (string $item) => in_array($item, $inAccountList)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getUserGroupEmailAddressList(User $user): array
|
||||
{
|
||||
$groupEmailAccountPermission = $this->acl->getPermissionLevel(Acl\Permission::GROUP_EMAIL_ACCOUNT);
|
||||
|
||||
if (!$groupEmailAccountPermission || $groupEmailAccountPermission === Acl\Table::LEVEL_NO) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($groupEmailAccountPermission === Acl\Table::LEVEL_TEAM) {
|
||||
$teamIdList = $user->getLinkMultipleIdList(Field::TEAMS);
|
||||
|
||||
if (!count($teamIdList)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$inboundEmailList = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmail::class)
|
||||
->where([
|
||||
'status' => InboundEmail::STATUS_ACTIVE,
|
||||
'useSmtp' => true,
|
||||
'smtpIsShared' => true,
|
||||
'teamsMiddle.teamId' => $teamIdList,
|
||||
])
|
||||
->join(Field::TEAMS)
|
||||
->distinct()
|
||||
->find();
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($inboundEmailList as $inboundEmail) {
|
||||
if (!$inboundEmail->getEmailAddress()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $inboundEmail->getEmailAddress();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
if ($groupEmailAccountPermission === Acl\Table::LEVEL_ALL) {
|
||||
$inboundEmailList = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmail::class)
|
||||
->where([
|
||||
'status' => InboundEmail::STATUS_ACTIVE,
|
||||
'useSmtp' => true,
|
||||
'smtpIsShared' => true,
|
||||
])
|
||||
->find();
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($inboundEmailList as $inboundEmail) {
|
||||
if (!$inboundEmail->getEmailAddress()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $inboundEmail->getEmailAddress();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private function getMaxUploadSize()
|
||||
{
|
||||
$maxSize = 0;
|
||||
|
||||
$postMaxSize = $this->convertPHPSizeToBytes(ini_get('post_max_size'));
|
||||
|
||||
if ($postMaxSize > 0) {
|
||||
$maxSize = $postMaxSize;
|
||||
}
|
||||
|
||||
return $maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|false $size
|
||||
* @return int
|
||||
*/
|
||||
private function convertPHPSizeToBytes($size)
|
||||
{
|
||||
if (is_numeric($size)) {
|
||||
return (int) $size;
|
||||
}
|
||||
|
||||
if ($size === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$suffix = strtoupper(substr($size, -1));
|
||||
$value = (int) substr($size, 0, -1);
|
||||
|
||||
if ($suffix == 'P') {
|
||||
$value *= pow(1024, 5);
|
||||
} else if ($suffix == 'T') {
|
||||
$value *= pow(1024, 4);
|
||||
} else if ($suffix == 'G') {
|
||||
$value *= pow(1024, 3);
|
||||
} else if ($suffix == 'M') {
|
||||
$value *= pow(1024, 2);
|
||||
} elseif ($suffix == 'K') {
|
||||
$value *= 1024;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function filterPreferencesData(stdClass $data): void
|
||||
{
|
||||
$passwordFieldList = $this->fieldUtil->getFieldByTypeList(Preferences::ENTITY_TYPE, 'password');
|
||||
|
||||
foreach ($passwordFieldList as $field) {
|
||||
unset($data->$field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getExcludeFromReplyAddressList(): array
|
||||
{
|
||||
if (!$this->acl->checkScope(Email::ENTITY_TYPE, Acl\Table::ACTION_CREATE)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var iterable<InboundEmail> $accounts */
|
||||
$accounts = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmail::class)
|
||||
->select('emailAddress')
|
||||
->where(['excludeFromReply' => true])
|
||||
->find();
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
if (!$account->getEmailAddress()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $account->getEmailAddress();
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
52
application/Espo/Tools/App/Jobs/ClearCache.php
Normal file
52
application/Espo/Tools/App/Jobs/ClearCache.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Jobs;
|
||||
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Job\JobDataLess;
|
||||
|
||||
class ClearCache implements JobDataLess
|
||||
{
|
||||
private DataManager $dataManager;
|
||||
|
||||
public function __construct(DataManager $dataManager)
|
||||
{
|
||||
$this->dataManager = $dataManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->dataManager->clearCache();
|
||||
}
|
||||
}
|
||||
52
application/Espo/Tools/App/Jobs/Rebuild.php
Normal file
52
application/Espo/Tools/App/Jobs/Rebuild.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Jobs;
|
||||
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Job\JobDataLess;
|
||||
|
||||
class Rebuild implements JobDataLess
|
||||
{
|
||||
private DataManager $dataManager;
|
||||
|
||||
public function __construct(DataManager $dataManager)
|
||||
{
|
||||
$this->dataManager = $dataManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->dataManager->rebuild();
|
||||
}
|
||||
}
|
||||
69
application/Espo/Tools/App/Language/AclDependencyItem.php
Normal file
69
application/Espo/Tools/App/Language/AclDependencyItem.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Language;
|
||||
|
||||
class AclDependencyItem
|
||||
{
|
||||
/**
|
||||
* @param ?string[] $anyScopeList
|
||||
*/
|
||||
public function __construct(
|
||||
private string $target,
|
||||
private ?array $anyScopeList,
|
||||
private ?string $scope,
|
||||
private ?string $field
|
||||
) {}
|
||||
|
||||
/**
|
||||
* A language path to be allowed if a user has access to a specific scope/field.
|
||||
*/
|
||||
public function getTarget(): string
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
*/
|
||||
public function getAnyScopeList(): ?array
|
||||
{
|
||||
return $this->anyScopeList;
|
||||
}
|
||||
|
||||
public function getScope(): ?string
|
||||
{
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function getField(): ?string
|
||||
{
|
||||
return $this->field;
|
||||
}
|
||||
}
|
||||
216
application/Espo/Tools/App/Language/AclDependencyProvider.php
Normal file
216
application/Espo/Tools/App/Language/AclDependencyProvider.php
Normal file
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Language;
|
||||
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\SystemConfig;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
|
||||
class AclDependencyProvider
|
||||
{
|
||||
private const CACHE_KEY = 'languageAclDependency';
|
||||
|
||||
/** @var string[] */
|
||||
private array $enumFieldTypeList = [
|
||||
FieldType::ENUM,
|
||||
FieldType::MULTI_ENUM,
|
||||
FieldType::ARRAY,
|
||||
FieldType::CHECKLIST,
|
||||
];
|
||||
|
||||
/** @var ?AclDependencyItem[] */
|
||||
private ?array $data = null;
|
||||
private bool $useCache;
|
||||
|
||||
public function __construct(
|
||||
private DataCache $dataCache,
|
||||
private Metadata $metadata,
|
||||
private Defs $ormDefs,
|
||||
SystemConfig $systemConfig,
|
||||
) {
|
||||
$this->useCache = $systemConfig->useCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AclDependencyItem[]
|
||||
*/
|
||||
public function get(): array
|
||||
{
|
||||
if ($this->data === null) {
|
||||
$this->data = $this->loadData();
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AclDependencyItem[]
|
||||
*/
|
||||
private function loadData(): array
|
||||
{
|
||||
if ($this->useCache && $this->dataCache->has(self::CACHE_KEY)) {
|
||||
/** @var array<string, mixed>[] $raw */
|
||||
$raw = $this->dataCache->get(self::CACHE_KEY);
|
||||
|
||||
return $this->buildFromRaw($raw);
|
||||
}
|
||||
|
||||
return $this->buildData();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AclDependencyItem[]
|
||||
*/
|
||||
private function buildData(): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach (($this->metadata->get(['app', 'language', 'aclDependencies']) ?? []) as $target => $item) {
|
||||
$anyScopeList = $item['anyScopeList'] ?? null;
|
||||
$scope = $item['scope'] ?? null;
|
||||
$field = $item['field'] ?? null;
|
||||
|
||||
$data[] = [
|
||||
'target' => $target,
|
||||
'anyScopeList' => $anyScopeList,
|
||||
'scope' => $scope,
|
||||
'field' => $field,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($this->ormDefs->getEntityList() as $entityDefs) {
|
||||
if (!$this->metadata->get(['scopes', $entityDefs->getName(), 'object'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($entityDefs->getFieldList() as $fieldDefs) {
|
||||
$item = $this->getDataFromField($entityDefs->getName(), $fieldDefs);
|
||||
|
||||
if ($item) {
|
||||
$data[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->useCache) {
|
||||
$this->dataCache->store(self::CACHE_KEY, $data);
|
||||
}
|
||||
|
||||
return $this->buildFromRaw($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
private function getDataFromField(string $entityType, Defs\FieldDefs $fieldDefs): ?array
|
||||
{
|
||||
if ($fieldDefs->getType() === FieldType::FOREIGN) {
|
||||
$refEntityType = $fieldDefs->getParam('link') ?
|
||||
$this->ormDefs
|
||||
->getEntity($entityType)
|
||||
->tryGetRelation($fieldDefs->getParam('link'))
|
||||
?->tryGetForeignEntityType() :
|
||||
null;
|
||||
|
||||
$refField = $fieldDefs->getParam('field');
|
||||
|
||||
if (!$refEntityType || !$refField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$foreignFieldType = $this->ormDefs
|
||||
->tryGetEntity($refEntityType)
|
||||
?->tryGetField($refField)
|
||||
?->getType();
|
||||
|
||||
if (
|
||||
!in_array($foreignFieldType, [
|
||||
FieldType::ENUM,
|
||||
FieldType::MULTI_ENUM,
|
||||
FieldType::ARRAY,
|
||||
FieldType::CHECKLIST,
|
||||
])
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'target' => "$refEntityType.options.$refField",
|
||||
'anyScopeList' => null,
|
||||
'scope' => $entityType,
|
||||
'field' => $fieldDefs->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
if (!in_array($fieldDefs->getType(), $this->enumFieldTypeList)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$optionsReference = $fieldDefs->getParam('optionsReference');
|
||||
|
||||
if (!$optionsReference || !str_contains($optionsReference, '.')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$refEntityType, $refField] = explode('.', $optionsReference);
|
||||
|
||||
$target = "$refEntityType.options.$refField";
|
||||
|
||||
return [
|
||||
'target' => $target,
|
||||
'anyScopeList' => null,
|
||||
'scope' => $entityType,
|
||||
'field' => $fieldDefs->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>[] $raw
|
||||
* @return AclDependencyItem[]
|
||||
*/
|
||||
private function buildFromRaw(array $raw): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($raw as $rawItem) {
|
||||
$target = $rawItem['target'] ?? null;
|
||||
$anyScopeList = $rawItem['anyScopeList'] ?? null;
|
||||
$scope = $rawItem['scope'] ?? null;
|
||||
$field = $rawItem['field'] ?? null;
|
||||
|
||||
$list[] = new AclDependencyItem($target, $anyScopeList, $scope, $field);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
272
application/Espo/Tools/App/LanguageService.php
Normal file
272
application/Espo/Tools/App/LanguageService.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App;
|
||||
|
||||
use Espo\Core\Utils\Language as LanguageUtil;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Container;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Tools\App\Language\AclDependencyProvider;
|
||||
|
||||
class LanguageService
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private Acl $acl,
|
||||
private User $user,
|
||||
private AclDependencyProvider $aclDependencyProvider,
|
||||
private Container $container
|
||||
) {}
|
||||
|
||||
// @todo Use proxy.
|
||||
protected function getDefaultLanguage(): LanguageUtil
|
||||
{
|
||||
/** @var LanguageUtil */
|
||||
return $this->container->get('defaultLanguage');
|
||||
}
|
||||
|
||||
protected function getLanguage(): LanguageUtil
|
||||
{
|
||||
/** @var LanguageUtil */
|
||||
return $this->container->get('language');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getDataForFrontendFromLanguage(LanguageUtil $language): array
|
||||
{
|
||||
$data = $language->getAll();
|
||||
|
||||
if ($this->user->isSystem()) {
|
||||
unset($data['Global']['scopeNames']);
|
||||
unset($data['Global']['scopeNamesPlural']);
|
||||
unset($data['Global']['dashlets']);
|
||||
unset($data['Global']['links']);
|
||||
|
||||
foreach ($data as $k => $item) {
|
||||
if (
|
||||
in_array($k, ['Global', 'User', 'Campaign']) ||
|
||||
$this->metadata->get(['scopes', $k, 'languageIsGlobal'])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unset($data[$k]);
|
||||
}
|
||||
|
||||
unset($data['User']['fields']);
|
||||
unset($data['User']['links']);
|
||||
unset($data['User']['options']);
|
||||
unset($data['User']['filters']);
|
||||
unset($data['User']['presetFilters']);
|
||||
unset($data['User']['boolFilters']);
|
||||
unset($data['User']['tooltips']);
|
||||
|
||||
unset($data['Campaign']['fields']);
|
||||
unset($data['Campaign']['links']);
|
||||
unset($data['Campaign']['options']);
|
||||
unset($data['Campaign']['tooltips']);
|
||||
unset($data['Campaign']['presetFilters']);
|
||||
} else if (!$this->user->isAdmin()) {
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['scopes'], []));
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
if (!$this->metadata->get(['scopes', $scope, 'entity'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->metadata->get(['scopes', $scope, 'languageAclDisabled'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->acl->tryCheck($scope)) {
|
||||
unset($data[$scope]);
|
||||
unset($data['Global']['scopeNames'][$scope]);
|
||||
unset($data['Global']['scopeNamesPlural'][$scope]);
|
||||
} else {
|
||||
if (in_array($scope, ['EmailAccount', 'InboundEmail'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->acl->getScopeForbiddenFieldList($scope) as $field) {
|
||||
if (isset($data[$scope]['fields'])) {
|
||||
unset($data[$scope]['fields'][$field]);
|
||||
}
|
||||
|
||||
if (isset($data[$scope]['options'])) {
|
||||
unset($data[$scope]['options'][$field]);
|
||||
}
|
||||
|
||||
if (isset($data[$scope]['links'])) {
|
||||
unset($data[$scope]['links'][$field]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->unsetEmpty($data, $scope);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
$this->prepareDataNonAdmin($data, $language);
|
||||
}
|
||||
}
|
||||
|
||||
$data['User']['fields'] = $data['User']['fields'] ?? [];
|
||||
|
||||
$data['User']['fields']['password'] = $language->translate('password', 'fields', 'User');
|
||||
$data['User']['fields']['passwordConfirm'] = $language->translate('passwordConfirm', 'fields', 'User');
|
||||
$data['User']['fields']['newPassword'] = $language->translate('newPassword', 'fields', 'User');
|
||||
$data['User']['fields']['newPasswordConfirm'] = $language->translate('newPasswordConfirm', 'fields', 'User');
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getDataForFrontend(bool $default = false): array
|
||||
{
|
||||
if ($default) {
|
||||
$languageObj = $this->getDefaultLanguage();
|
||||
} else {
|
||||
$languageObj = $this->getLanguage();
|
||||
}
|
||||
|
||||
return $this->getDataForFrontendFromLanguage($languageObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function unsetEmpty(array &$data, string $scope): void
|
||||
{
|
||||
if (($data[$scope]['options'] ?? null) === []) {
|
||||
unset($data[$scope]['options']);
|
||||
}
|
||||
|
||||
if (($data[$scope]['fields'] ?? null) === []) {
|
||||
unset($data[$scope]['fields']);
|
||||
}
|
||||
|
||||
if (($data[$scope]['links'] ?? null) === []) {
|
||||
unset($data[$scope]['links']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function prepareDataNonAdmin(array &$data, LanguageUtil $languageObj): void
|
||||
{
|
||||
unset($data['Admin']);
|
||||
unset($data['LayoutManager']);
|
||||
unset($data['EntityManager']);
|
||||
unset($data['FieldManager']);
|
||||
unset($data['Settings']);
|
||||
unset($data['ApiUser']);
|
||||
unset($data['DynamicLogic']);
|
||||
|
||||
$data['Settings'] = [
|
||||
'options' => [
|
||||
'auth2FAMethodList' => $languageObj->get(['Settings', 'options', 'auth2FAMethodList']),
|
||||
],
|
||||
];
|
||||
|
||||
$data['Admin'] = [
|
||||
'messages' => [
|
||||
'userHasNoEmailAddress' => $languageObj->translate('userHasNoEmailAddress', 'messages', 'Admin'),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($this->aclDependencyProvider->get() as $dependencyItem) {
|
||||
$target = $dependencyItem->getTarget();
|
||||
$aclScope = $dependencyItem->getScope();
|
||||
$aclField = $dependencyItem->getField();
|
||||
$anyScopeList = $dependencyItem->getAnyScopeList();
|
||||
|
||||
$targetArr = explode('.', $target);
|
||||
|
||||
$isFullScope = !str_contains($target, '.');
|
||||
|
||||
if ($isFullScope && isset($data[$target])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($anyScopeList) {
|
||||
$skip = true;
|
||||
|
||||
foreach ($anyScopeList as $itemScope) {
|
||||
if ($this->acl->tryCheck($itemScope)) {
|
||||
$skip = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($skip) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($aclScope) {
|
||||
if (!$this->acl->tryCheck($aclScope)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($aclField && in_array($aclField, $this->acl->getScopeForbiddenFieldList($aclScope))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$pointer =& $data;
|
||||
|
||||
foreach ($targetArr as $i => $k) {
|
||||
if ($i === count($targetArr) - 1) {
|
||||
$pointer[$k] = $languageObj->get($targetArr);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isset($pointer[$k])) {
|
||||
$pointer[$k] = [];
|
||||
}
|
||||
|
||||
$pointer =& $pointer[$k];
|
||||
}
|
||||
|
||||
if ($isFullScope) {
|
||||
$this->unsetEmpty($data, $target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
application/Espo/Tools/App/Metadata/AclDependencyItem.php
Normal file
70
application/Espo/Tools/App/Metadata/AclDependencyItem.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Metadata;
|
||||
|
||||
class AclDependencyItem
|
||||
{
|
||||
/**
|
||||
* @param ?string[] $anyScopeList
|
||||
*/
|
||||
public function __construct(
|
||||
private string $target,
|
||||
private ?string $scope,
|
||||
private ?string $field,
|
||||
private ?array $anyScopeList = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* A metadata path to be allowed if a user has access to a specific scope/field.
|
||||
*/
|
||||
public function getTarget(): string
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function getScope(): ?string
|
||||
{
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function getField(): ?string
|
||||
{
|
||||
return $this->field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string[]
|
||||
* @since 9.2.5
|
||||
*/
|
||||
public function getAnyScopeList(): ?array
|
||||
{
|
||||
return $this->anyScopeList;
|
||||
}
|
||||
}
|
||||
209
application/Espo/Tools/App/Metadata/AclDependencyProvider.php
Normal file
209
application/Espo/Tools/App/Metadata/AclDependencyProvider.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App\Metadata;
|
||||
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DataCache;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
|
||||
class AclDependencyProvider
|
||||
{
|
||||
private const CACHE_KEY = 'metadataAclDependency';
|
||||
|
||||
/** @var string[] */
|
||||
private array $enumFieldTypeList = [
|
||||
FieldType::ENUM,
|
||||
FieldType::MULTI_ENUM,
|
||||
FieldType::ARRAY,
|
||||
FieldType::CHECKLIST,
|
||||
];
|
||||
|
||||
/** @var ?AclDependencyItem[] */
|
||||
private ?array $data = null;
|
||||
private bool $useCache;
|
||||
|
||||
public function __construct(
|
||||
private DataCache $dataCache,
|
||||
private Metadata $metadata,
|
||||
private Defs $ormDefs,
|
||||
Config\SystemConfig $systemConfig,
|
||||
) {
|
||||
$this->useCache = $systemConfig->useCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AclDependencyItem[]
|
||||
*/
|
||||
public function get(): array
|
||||
{
|
||||
if ($this->data === null) {
|
||||
$this->data = $this->loadData();
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AclDependencyItem[]
|
||||
*/
|
||||
private function loadData(): array
|
||||
{
|
||||
if ($this->useCache && $this->dataCache->has(self::CACHE_KEY)) {
|
||||
/** @var array<string, mixed>[] $raw */
|
||||
$raw = $this->dataCache->get(self::CACHE_KEY);
|
||||
|
||||
return $this->buildFromRaw($raw);
|
||||
}
|
||||
|
||||
return $this->buildData();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AclDependencyItem[]
|
||||
*/
|
||||
private function buildData(): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach (($this->metadata->get(['app', 'metadata', 'aclDependencies']) ?? []) as $target => $item) {
|
||||
$anyScopeList = $item['anyScopeList'] ?? null;
|
||||
$scope = $item['scope'] ?? null;
|
||||
$field = $item['field'] ?? null;
|
||||
|
||||
$data[] = [
|
||||
'target' => $target,
|
||||
'anyScopeList' => $anyScopeList,
|
||||
'scope' => $scope,
|
||||
'field' => $field,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($this->ormDefs->getEntityList() as $entityDefs) {
|
||||
if (!$this->metadata->get(['scopes', $entityDefs->getName(), 'object'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($entityDefs->getFieldList() as $fieldDefs) {
|
||||
$item = $this->getDataFromField($entityDefs->getName(), $fieldDefs);
|
||||
|
||||
if ($item) {
|
||||
$data[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->useCache) {
|
||||
$this->dataCache->store(self::CACHE_KEY, $data);
|
||||
}
|
||||
|
||||
return $this->buildFromRaw($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
private function getDataFromField(string $entityType, Defs\FieldDefs $fieldDefs): ?array
|
||||
{
|
||||
if ($fieldDefs->getType() === FieldType::FOREIGN) {
|
||||
$refEntityType = $fieldDefs->getParam('link') ?
|
||||
$this->ormDefs
|
||||
->getEntity($entityType)
|
||||
->tryGetRelation($fieldDefs->getParam('link'))
|
||||
?->tryGetForeignEntityType() :
|
||||
null;
|
||||
|
||||
$refField = $fieldDefs->getParam('field');
|
||||
|
||||
if (!$refEntityType || !$refField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'target' => "entityDefs.$refEntityType.fields.$refField",
|
||||
'scope' => $entityType,
|
||||
'field' => $fieldDefs->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
if (!in_array($fieldDefs->getType(), $this->enumFieldTypeList)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$optionsPath = $fieldDefs->getParam('optionsPath');
|
||||
$optionsReference = $fieldDefs->getParam('optionsReference');
|
||||
|
||||
if (
|
||||
!$optionsPath &&
|
||||
$optionsReference &&
|
||||
str_contains($optionsReference, '.')
|
||||
) {
|
||||
[$refEntityType, $refField] = explode('.', $optionsReference);
|
||||
|
||||
$optionsPath = "entityDefs.$refEntityType.fields.$refField.options";
|
||||
}
|
||||
|
||||
if (!$optionsPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'target' => $optionsPath,
|
||||
'scope' => $entityType,
|
||||
'field' => $fieldDefs->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>[] $raw
|
||||
* @return AclDependencyItem[]
|
||||
*/
|
||||
private function buildFromRaw(array $raw): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($raw as $rawItem) {
|
||||
$target = $rawItem['target'] ?? null;
|
||||
$scope = $rawItem['scope'] ?? null;
|
||||
$field = $rawItem['field'] ?? null;
|
||||
$anyScopeList = $rawItem['anyScopeList'] ?? null;
|
||||
|
||||
$list[] = new AclDependencyItem(
|
||||
target: $target,
|
||||
scope: $scope,
|
||||
field: $field,
|
||||
anyScopeList: $anyScopeList,
|
||||
);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
296
application/Espo/Tools/App/MetadataService.php
Normal file
296
application/Espo/Tools/App/MetadataService.php
Normal file
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Utils\Metadata as MetadataUtil;
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Reminder;
|
||||
use Espo\Tools\App\Metadata\AclDependencyProvider;
|
||||
use stdClass;
|
||||
|
||||
class MetadataService
|
||||
{
|
||||
private const ANY_KEY = '__ANY__';
|
||||
|
||||
public function __construct(
|
||||
private Acl $acl,
|
||||
private MetadataUtil $metadata,
|
||||
private User $user,
|
||||
private AclDependencyProvider $aclDependencyProvider
|
||||
) {}
|
||||
|
||||
public function getDataForFrontend(): stdClass
|
||||
{
|
||||
$data = $this->metadata->getAll();
|
||||
|
||||
$hiddenPathList = $this->metadata->get(['app', 'metadata', 'frontendHiddenPathList'], []);
|
||||
|
||||
foreach ($hiddenPathList as $row) {
|
||||
$this->removeDataByPath($row, $data);
|
||||
}
|
||||
|
||||
if ($this->user->isAdmin()) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$data = ObjectUtil::clone($data);
|
||||
|
||||
$hiddenPathList = $this->metadata->get(['app', 'metadata', 'frontendNonAdminHiddenPathList'], []);
|
||||
|
||||
foreach ($hiddenPathList as $row) {
|
||||
$this->removeDataByPath($row, $data);
|
||||
}
|
||||
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['entityDefs'], []));
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
$isEntity = $this->metadata->get(['scopes', $scope, 'entity']);
|
||||
|
||||
if ($isEntity === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($scope === Reminder::ENTITY_TYPE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isAllowed = $isEntity !== null && $this->acl->tryCheck($scope);
|
||||
|
||||
if (!$isAllowed) {
|
||||
unset($data->entityDefs->$scope);
|
||||
unset($data->clientDefs->$scope);
|
||||
unset($data->entityAcl->$scope);
|
||||
unset($data->scopes->$scope);
|
||||
unset($data->logicDefs->$scope);
|
||||
}
|
||||
}
|
||||
|
||||
$entityTypeList = array_keys(get_object_vars($data->entityDefs));
|
||||
|
||||
foreach ($entityTypeList as $entityType) {
|
||||
$linksDefs = $this->metadata->get(['entityDefs', $entityType, 'links'], []);
|
||||
|
||||
$forbiddenFieldList = $this->acl->getScopeForbiddenFieldList($entityType);
|
||||
|
||||
foreach ($linksDefs as $link => $defs) {
|
||||
$type = $defs['type'] ?? null;
|
||||
|
||||
$hasField = (bool) $this->metadata->get(['entityDefs', $entityType, 'fields', $link]);
|
||||
|
||||
if ($type === 'belongsToParent') {
|
||||
if ($hasField) {
|
||||
$parentEntityList = $this->metadata
|
||||
->get(['entityDefs', $entityType, 'fields', $link, 'entityList']);
|
||||
|
||||
if (is_array($parentEntityList)) {
|
||||
foreach ($parentEntityList as $i => $e) {
|
||||
if (!$this->acl->tryCheck($e)) {
|
||||
unset($parentEntityList[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
$parentEntityList = array_values($parentEntityList);
|
||||
|
||||
$data->entityDefs->$entityType->fields->$link->entityList = $parentEntityList;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$foreignEntityType = $defs['entity'] ?? null;
|
||||
|
||||
if ($foreignEntityType) {
|
||||
if ($this->acl->tryCheck($foreignEntityType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->user->isPortal()) {
|
||||
if ($foreignEntityType === 'Account' || $foreignEntityType === 'Contact') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasField) {
|
||||
if (!in_array($link, $forbiddenFieldList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unset($data->entityDefs->$entityType->fields->$link);
|
||||
}
|
||||
|
||||
unset($data->entityDefs->$entityType->links->$link);
|
||||
|
||||
if (isset($data->clientDefs->$entityType->relationshipPanels)) {
|
||||
unset($data->clientDefs->$entityType->relationshipPanels->$link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unset($data->entityDefs->Settings);
|
||||
|
||||
/** @var string[] $dashletList */
|
||||
$dashletList = array_keys($this->metadata->get(['dashlets'], []));
|
||||
|
||||
foreach ($dashletList as $item) {
|
||||
$aclScope = $this->metadata->get(['dashlets', $item, 'aclScope']);
|
||||
|
||||
if ($aclScope && !$this->acl->tryCheck($aclScope)) {
|
||||
unset($data->dashlets->$item);
|
||||
}
|
||||
}
|
||||
|
||||
unset($data->authenticationMethods);
|
||||
unset($data->formula);
|
||||
|
||||
foreach ($this->aclDependencyProvider->get() as $dependencyItem) {
|
||||
$aclScope = $dependencyItem->getScope();
|
||||
$aclField = $dependencyItem->getField();
|
||||
$anyScopeList = $dependencyItem->getAnyScopeList();
|
||||
|
||||
if ($anyScopeList) {
|
||||
$skip = true;
|
||||
|
||||
foreach ($anyScopeList as $itemScope) {
|
||||
if ($this->acl->tryCheck($itemScope)) {
|
||||
$skip = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($skip) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($aclScope) {
|
||||
if (!$this->acl->tryCheck($aclScope)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($aclField && in_array($aclField, $this->acl->getScopeForbiddenFieldList($aclScope))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$targetArr = explode('.', $dependencyItem->getTarget());
|
||||
|
||||
$pointer = $data;
|
||||
|
||||
$value = $this->metadata->getObjects($targetArr);
|
||||
|
||||
if ($value === null) {
|
||||
// Important.
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($targetArr as $i => $k) {
|
||||
if ($i === count($targetArr) - 1) {
|
||||
$pointer->$k = $value;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isset($pointer->$k)) {
|
||||
$pointer->$k = (object) [];
|
||||
}
|
||||
|
||||
$pointer = $pointer->$k;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string[] $row
|
||||
* @param stdClass $data
|
||||
*/
|
||||
private function removeDataByPath($row, &$data): void
|
||||
{
|
||||
$p = &$data;
|
||||
$path = [&$p];
|
||||
|
||||
foreach ($row as $i => $item) {
|
||||
if (is_array($item)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($item === self::ANY_KEY) {
|
||||
foreach (get_object_vars($p) as &$v) {
|
||||
$this->removeDataByPath(
|
||||
array_slice($row, $i + 1),
|
||||
$v
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!property_exists($p, $item)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($i == count($row) - 1) {
|
||||
unset($p->$item);
|
||||
|
||||
$o = &$p;
|
||||
|
||||
for ($j = $i - 1; $j > 0; $j--) {
|
||||
if (is_object($o) && !count(get_object_vars($o))) {
|
||||
$o = &$path[$j];
|
||||
$k = $row[$j];
|
||||
|
||||
unset($o->$k);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$p = &$p->$item;
|
||||
$path[] = &$p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getDataForFrontendByKey(?string $key): mixed
|
||||
{
|
||||
$data = $this->getDataForFrontend();
|
||||
|
||||
return Util::getValueByKey($data, $key);
|
||||
}
|
||||
}
|
||||
232
application/Espo/Tools/App/PreferencesService.php
Normal file
232
application/Espo/Tools/App/PreferencesService.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Repositories\Preferences as Repository;
|
||||
use Espo\Entities\Preferences;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\FieldValidation\FieldValidationManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class PreferencesService
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private User $user;
|
||||
private Acl $acl;
|
||||
private Config $config;
|
||||
private FieldValidationManager $fieldValidationManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
User $user,
|
||||
Acl $acl,
|
||||
Config $config,
|
||||
FieldValidationManager $fieldValidationManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->user = $user;
|
||||
$this->acl = $acl;
|
||||
$this->config = $config;
|
||||
$this->fieldValidationManager = $fieldValidationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
protected function processAccessCheck(string $userId): void
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
if ($this->user->getId() !== $userId) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function read(string $userId): Preferences
|
||||
{
|
||||
$this->processAccessCheck($userId);
|
||||
|
||||
/** @var ?Preferences $entity */
|
||||
$entity = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$entity || !$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$entity->set(Field::NAME, $user->getName());
|
||||
$entity->set('isPortalUser', $user->isPortal());
|
||||
|
||||
// @todo Remove.
|
||||
$entity->clear('smtpPassword');
|
||||
|
||||
$forbiddenAttributeList = $this->acl
|
||||
->getScopeForbiddenAttributeList(Preferences::ENTITY_TYPE, Table::ACTION_READ);
|
||||
|
||||
foreach ($forbiddenAttributeList as $attribute) {
|
||||
$entity->clear($attribute);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function update(string $userId, stdClass $data): Preferences
|
||||
{
|
||||
$this->processAccessCheck($userId);
|
||||
|
||||
if ($this->acl->getLevel(Preferences::ENTITY_TYPE, Table::ACTION_EDIT) === Table::LEVEL_NO) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$forbiddenAttributeList = $this->acl
|
||||
->getScopeForbiddenAttributeList(Preferences::ENTITY_TYPE, Table::ACTION_EDIT);
|
||||
|
||||
foreach ($forbiddenAttributeList as $attribute) {
|
||||
unset($data->$attribute);
|
||||
}
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
/** @var ?Preferences $entity */
|
||||
$entity = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$entity || !$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$entity->set($data);
|
||||
|
||||
$this->fieldValidationManager->process($entity, $data);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$entity->set(Field::NAME, $user->getName());
|
||||
|
||||
// @todo Remove.
|
||||
$entity->clear('smtpPassword');
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function resetToDefaults(string $userId): void
|
||||
{
|
||||
$this->processAccessCheck($userId);
|
||||
|
||||
$result = $this->getRepository()->resetToDefaults($userId);
|
||||
|
||||
if (!$result) {
|
||||
throw new NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function resetDashboard(string $userId): stdClass
|
||||
{
|
||||
$this->processAccessCheck($userId);
|
||||
|
||||
if ($this->acl->getLevel(Preferences::ENTITY_TYPE, Table::ACTION_EDIT) === Table::LEVEL_NO) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$preferences) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if ($user->isPortal()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$forbiddenAttributeList = $this->acl
|
||||
->getScopeForbiddenAttributeList(Preferences::ENTITY_TYPE, Table::ACTION_EDIT);
|
||||
|
||||
if (in_array('dashboardLayout', $forbiddenAttributeList)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$dashboardLayout = $this->config->get('dashboardLayout');
|
||||
$dashletsOptions = $this->config->get('dashletsOptions');
|
||||
|
||||
$preferences->set([
|
||||
'dashboardLayout' => $dashboardLayout,
|
||||
'dashletsOptions' => $dashletsOptions,
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($preferences);
|
||||
|
||||
return (object) [
|
||||
'dashboardLayout' => $preferences->get('dashboardLayout'),
|
||||
'dashletsOptions' => $preferences->get('dashletsOptions'),
|
||||
];
|
||||
}
|
||||
|
||||
private function getRepository(): Repository
|
||||
{
|
||||
/** @var Repository */
|
||||
return $this->entityManager->getRepository(Preferences::ENTITY_TYPE);
|
||||
}
|
||||
}
|
||||
404
application/Espo/Tools/App/SettingsService.php
Normal file
404
application/Espo/Tools/App/SettingsService.php
Normal file
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\App;
|
||||
|
||||
use Espo\Core\Mail\ConfigDataProvider as EmailConfigDataProvider;
|
||||
use Espo\Core\Utils\ThemeManager;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Settings;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Authentication\Util\MethodProvider as AuthenticationMethodProvider;
|
||||
use Espo\Core\ApplicationState;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\DataManager;
|
||||
use Espo\Core\FieldValidation\FieldValidationManager;
|
||||
use Espo\Core\Utils\Currency\DatabasePopulator as CurrencyDatabasePopulator;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
use Espo\Core\Utils\Config\Access;
|
||||
|
||||
use Espo\Entities\Portal;
|
||||
use Espo\Repositories\Portal as PortalRepository;
|
||||
|
||||
use stdClass;
|
||||
|
||||
class SettingsService
|
||||
{
|
||||
public function __construct(
|
||||
private ApplicationState $applicationState,
|
||||
private Config $config,
|
||||
private ConfigWriter $configWriter,
|
||||
private Metadata $metadata,
|
||||
private Acl $acl,
|
||||
private EntityManager $entityManager,
|
||||
private DataManager $dataManager,
|
||||
private FieldValidationManager $fieldValidationManager,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private Access $access,
|
||||
private AuthenticationMethodProvider $authenticationMethodProvider,
|
||||
private ThemeManager $themeManager,
|
||||
private Config\SystemConfig $systemConfig,
|
||||
private EmailConfigDataProvider $emailConfigDataProvider,
|
||||
private Acl\Cache\Clearer $aclCacheClearer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get config data.
|
||||
*/
|
||||
public function getConfigData(): stdClass
|
||||
{
|
||||
$data = $this->config->getAllNonInternalData();
|
||||
|
||||
$this->filterDataByAccess($data);
|
||||
$this->filterData($data);
|
||||
$this->loadAdditionalParams($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata to be used in config.
|
||||
*/
|
||||
public function getMetadataConfigData(): stdClass
|
||||
{
|
||||
$data = (object) [];
|
||||
|
||||
unset($data->loginView);
|
||||
|
||||
$loginView = $this->metadata->get(['clientDefs', 'App', 'loginView']);
|
||||
|
||||
if ($loginView) {
|
||||
$data->loginView = $loginView;
|
||||
}
|
||||
|
||||
$loginData = $this->getLoginData();
|
||||
|
||||
if ($loginData) {
|
||||
$data->loginData = (object) $loginData;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array{
|
||||
* handler: string,
|
||||
* fallback: bool,
|
||||
* data: stdClass,
|
||||
* method: string,
|
||||
* }
|
||||
*/
|
||||
private function getLoginData(): ?array
|
||||
{
|
||||
$method = $this->authenticationMethodProvider->get();
|
||||
|
||||
/** @var array<string, mixed> $mData */
|
||||
$mData = $this->metadata->get(['authenticationMethods', $method, 'login']) ?? [];
|
||||
|
||||
/** @var ?string $handler */
|
||||
$handler = $mData['handler'] ?? null;
|
||||
|
||||
if (!$handler) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$isProvider = $this->isPortalWithAuthenticationProvider();
|
||||
|
||||
if (!$isProvider && $this->applicationState->isPortal()) {
|
||||
/** @var ?bool $portal */
|
||||
$portal = $mData['portal'] ?? null;
|
||||
|
||||
if ($portal === null) {
|
||||
/** @var ?string $portalConfigParam */
|
||||
$portalConfigParam = $mData['portalConfigParam'] ?? null;
|
||||
|
||||
$portal = $portalConfigParam && $this->config->get($portalConfigParam);
|
||||
}
|
||||
|
||||
if (!$portal) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ?bool $fallback */
|
||||
$fallback = !$this->applicationState->isPortal() ?
|
||||
($mData['fallback'] ?? null) :
|
||||
false;
|
||||
|
||||
if ($fallback === null) {
|
||||
/** @var ?string $fallbackConfigParam */
|
||||
$fallbackConfigParam = $mData['fallbackConfigParam'] ?? null;
|
||||
|
||||
$fallback = $fallbackConfigParam && $this->config->get($fallbackConfigParam);
|
||||
}
|
||||
|
||||
if ($isProvider) {
|
||||
$fallback = false;
|
||||
}
|
||||
|
||||
/** @var stdClass $data */
|
||||
$data = (object) ($mData['data'] ?? []);
|
||||
|
||||
return [
|
||||
'handler' => $handler,
|
||||
'fallback' => $fallback,
|
||||
'method' => $method,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
private function isPortalWithAuthenticationProvider(): bool
|
||||
{
|
||||
if (!$this->applicationState->isPortal()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$portal = $this->applicationState->getPortal();
|
||||
|
||||
return (bool) $this->authenticationMethodProvider->getForPortal($portal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set config data.
|
||||
*
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
public function setConfigData(stdClass $data): void
|
||||
{
|
||||
$user = $this->applicationState->getUser();
|
||||
|
||||
if (!$user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$ignoreItemList = array_merge(
|
||||
$this->access->getSystemParamList(),
|
||||
$this->access->getReadOnlyParamList(),
|
||||
$this->isRestrictedMode() && !$user->isSuperAdmin() ?
|
||||
$this->access->getSuperAdminParamList() : []
|
||||
);
|
||||
|
||||
foreach ($ignoreItemList as $item) {
|
||||
unset($data->$item);
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getNewEntity(Settings::ENTITY_TYPE);
|
||||
|
||||
$entity->set($data);
|
||||
$entity->setAsNotNew();
|
||||
|
||||
$this->processValidation($entity, $data);
|
||||
|
||||
if (
|
||||
isset($data->useCache) &&
|
||||
$data->useCache !== $this->systemConfig->useCache()
|
||||
) {
|
||||
$this->dataManager->clearCache();
|
||||
}
|
||||
|
||||
$this->configWriter->setMultiple(get_object_vars($data));
|
||||
$this->configWriter->save();
|
||||
|
||||
if (isset($data->personNameFormat)) {
|
||||
$this->dataManager->clearCache();
|
||||
}
|
||||
|
||||
if (property_exists($data, 'baselineRoleId')) {
|
||||
$this->aclCacheClearer->clearForAllInternalUsers();
|
||||
}
|
||||
|
||||
if (isset($data->defaultCurrency) || isset($data->baseCurrency) || isset($data->currencyRates)) {
|
||||
$this->populateDatabaseWithCurrencyRates();
|
||||
}
|
||||
}
|
||||
|
||||
private function loadAdditionalParams(stdClass $data): void
|
||||
{
|
||||
if ($this->applicationState->isPortal()) {
|
||||
$portal = $this->applicationState->getPortal();
|
||||
|
||||
$this->getPortalRepository()->loadUrlField($portal);
|
||||
|
||||
$data->siteUrl = $portal->get('url');
|
||||
}
|
||||
|
||||
if (
|
||||
(
|
||||
$this->emailConfigDataProvider->getSystemOutboundAddress() ||
|
||||
$this->config->get('internalSmtpServer')
|
||||
) &&
|
||||
!$this->config->get('passwordRecoveryDisabled')
|
||||
) {
|
||||
$data->passwordRecoveryEnabled = true;
|
||||
}
|
||||
|
||||
$data->logoSrc = $this->themeManager->getLogoSrc();
|
||||
}
|
||||
|
||||
private function filterDataByAccess(stdClass $data): void
|
||||
{
|
||||
$user = $this->applicationState->getUser();
|
||||
|
||||
$ignoreItemList = [];
|
||||
|
||||
foreach ($this->access->getSystemParamList() as $item) {
|
||||
$ignoreItemList[] = $item;
|
||||
}
|
||||
|
||||
foreach ($this->access->getInternalParamList() as $item) {
|
||||
$ignoreItemList[] = $item;
|
||||
}
|
||||
|
||||
if (!$user->isAdmin() || $user->isSystem()) {
|
||||
foreach ($this->access->getAdminParamList() as $item) {
|
||||
$ignoreItemList[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/*if ($this->isRestrictedMode() && !$user->isSuperAdmin()) {
|
||||
// @todo Maybe add restriction level for non-super admins.
|
||||
}*/
|
||||
|
||||
foreach ($ignoreItemList as $item) {
|
||||
unset($data->$item);
|
||||
}
|
||||
|
||||
if ($user->isSystem()) {
|
||||
$globalItemList = $this->access->getGlobalParamList();
|
||||
|
||||
foreach (array_keys(get_object_vars($data)) as $item) {
|
||||
if (!in_array($item, $globalItemList)) {
|
||||
unset($data->$item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function filterEntityTypeParams(stdClass $data): void
|
||||
{
|
||||
$entityTypeListParamList = $this->metadata->get(['app', 'config', 'entityTypeListParamList']) ?? [];
|
||||
|
||||
/** @var string[] $scopeList */
|
||||
$scopeList = array_keys($this->metadata->get(['entityDefs'], []));
|
||||
|
||||
foreach ($scopeList as $scope) {
|
||||
if (!$this->metadata->get(['scopes', $scope, 'acl'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->acl->tryCheck($scope)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($entityTypeListParamList as $param) {
|
||||
$list = $data->$param ?? [];
|
||||
|
||||
foreach ($list as $i => $item) {
|
||||
if ($item === $scope) {
|
||||
unset($list[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
$data->$param = array_values($list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function populateDatabaseWithCurrencyRates(): void
|
||||
{
|
||||
$this->injectableFactory->create(CurrencyDatabasePopulator::class)->process();
|
||||
}
|
||||
|
||||
private function filterData(stdClass $data): void
|
||||
{
|
||||
$user = $this->applicationState->getUser();
|
||||
|
||||
if (!$user->isAdmin() && !$user->isSystem()) {
|
||||
$this->filterEntityTypeParams($data);
|
||||
}
|
||||
|
||||
$fieldDefs = $this->metadata->get(['entityDefs', 'Settings', 'fields']);
|
||||
|
||||
foreach ($fieldDefs as $field => $fieldParams) {
|
||||
if ($fieldParams['type'] === 'password') {
|
||||
unset($data->$field);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($data->useWebSocket)) {
|
||||
unset($data->webSocketUrl);
|
||||
}
|
||||
|
||||
if ($user->isSystem()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->acl->checkScope(Email::ENTITY_TYPE, Acl\Table::ACTION_CREATE) ||
|
||||
!$this->emailConfigDataProvider->isSystemOutboundAddressShared()
|
||||
) {
|
||||
unset($data->outboundEmailFromAddress);
|
||||
unset($data->outboundEmailFromName);
|
||||
unset($data->outboundEmailBccAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private function isRestrictedMode(): bool
|
||||
{
|
||||
return (bool) $this->config->get('restrictedMode');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
*/
|
||||
private function processValidation(Entity $entity, stdClass $data): void
|
||||
{
|
||||
$this->fieldValidationManager->process($entity, $data);
|
||||
}
|
||||
|
||||
private function getPortalRepository(): PortalRepository
|
||||
{
|
||||
/** @var PortalRepository */
|
||||
return $this->entityManager->getRepository(Portal::ENTITY_TYPE);
|
||||
}
|
||||
}
|
||||
72
application/Espo/Tools/AppSecret/SecretProvider.php
Normal file
72
application/Espo/Tools/AppSecret/SecretProvider.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\AppSecret;
|
||||
|
||||
use Espo\Core\Utils\Crypt;
|
||||
use Espo\Entities\AppSecret;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\Condition;
|
||||
use Espo\ORM\Query\Part\Expression;
|
||||
|
||||
/**
|
||||
* @since 9.0.0
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class SecretProvider
|
||||
{
|
||||
public function __construct(
|
||||
private Crypt $crypt,
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get an app secret value.
|
||||
*
|
||||
* @param string $name A secret name.
|
||||
*/
|
||||
public function get(string $name): ?string
|
||||
{
|
||||
$secret = $this->entityManager
|
||||
->getRDBRepositoryByClass(AppSecret::class)
|
||||
->where(
|
||||
Condition::equal(
|
||||
Expression::binary(Expression::column('name')),
|
||||
$name
|
||||
)
|
||||
)
|
||||
->findOne();
|
||||
|
||||
if (!$secret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->crypt->decrypt($secret->getValue());
|
||||
}
|
||||
}
|
||||
125
application/Espo/Tools/Attachment/AccessChecker.php
Normal file
125
application/Espo/Tools/Attachment/AccessChecker.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Entities\Settings;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
|
||||
class AccessChecker
|
||||
{
|
||||
/** @var string[] */
|
||||
private $adminOnlyHavingInlineAttachmentsEntityTypeList = ['TemplateManager'];
|
||||
|
||||
/** @var string[] */
|
||||
private $attachmentFieldTypeList = [
|
||||
FieldType::FILE,
|
||||
FieldType::IMAGE,
|
||||
FieldType::ATTACHMENT_MULTIPLE,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private $inlineAttachmentFieldTypeList = [
|
||||
FieldType::WYSIWYG,
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private $allowedRoleList = [
|
||||
Attachment::ROLE_ATTACHMENT,
|
||||
Attachment::ROLE_INLINE_ATTACHMENT,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private Acl $acl,
|
||||
private Metadata $metadata
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check access to a field and role allowance.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function check(FieldData $fieldData, string $role = Attachment::ROLE_ATTACHMENT): void
|
||||
{
|
||||
if (!in_array($role, $this->allowedRoleList)) {
|
||||
throw new Forbidden("Role not allowed.");
|
||||
}
|
||||
|
||||
$relatedEntityType = $fieldData->getParentType() ?? $fieldData->getRelatedType();
|
||||
$field = $fieldData->getField();
|
||||
|
||||
if (!$relatedEntityType) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (
|
||||
$this->user->isAdmin() &&
|
||||
$role === Attachment::ROLE_INLINE_ATTACHMENT &&
|
||||
in_array($relatedEntityType, $this->adminOnlyHavingInlineAttachmentsEntityTypeList)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fieldType = $this->metadata->get(['entityDefs', $relatedEntityType, 'fields', $field, 'type']);
|
||||
|
||||
if (!$fieldType) {
|
||||
throw new Forbidden("Field '$field' does not exist.");
|
||||
}
|
||||
|
||||
$fieldTypeList = $role === Attachment::ROLE_INLINE_ATTACHMENT ?
|
||||
$this->inlineAttachmentFieldTypeList :
|
||||
$this->attachmentFieldTypeList;
|
||||
|
||||
if (!in_array($fieldType, $fieldTypeList)) {
|
||||
throw new Forbidden("Field type '$fieldType' is not allowed for $role.");
|
||||
}
|
||||
|
||||
if ($this->user->isAdmin() && $relatedEntityType === Settings::ENTITY_TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->acl->checkScope($relatedEntityType, Table::ACTION_CREATE) &&
|
||||
!$this->acl->checkScope($relatedEntityType, Table::ACTION_EDIT)
|
||||
) {
|
||||
throw new Forbidden("No access to " . $relatedEntityType . ".");
|
||||
}
|
||||
|
||||
if (!$this->acl->checkField($relatedEntityType, $field, Table::ACTION_EDIT)) {
|
||||
throw new Forbidden("No access to field '$field'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
67
application/Espo/Tools/Attachment/Api/GetFile.php
Normal file
67
application/Espo/Tools/Attachment/Api/GetFile.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment\Api;
|
||||
|
||||
use Espo\Core\Api\Action as ActionAlias;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Attachment\Service;
|
||||
|
||||
/**
|
||||
* Download a file.
|
||||
*/
|
||||
class GetFile implements ActionAlias
|
||||
{
|
||||
public function __construct(private Service $service) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$fileData = $this->service->getFileData($id);
|
||||
|
||||
$response = ResponseComposer::empty()
|
||||
->setHeader('Content-Disposition', 'attachment; filename="' . $fileData->getName() . '"')
|
||||
->setHeader('Content-Length', (string) $fileData->getSize())
|
||||
->setBody($fileData->getStream());
|
||||
|
||||
if ($fileData->getType()) {
|
||||
$response->setHeader('Content-Type', $fileData->getType());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
59
application/Espo/Tools/Attachment/Api/PostChunk.php
Normal file
59
application/Espo/Tools/Attachment/Api/PostChunk.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Attachment\UploadService;
|
||||
|
||||
/**
|
||||
* Uploads attachment chunks.
|
||||
*/
|
||||
class PostChunk implements Action
|
||||
{
|
||||
public function __construct(private UploadService $uploadService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
$body = $request->getBodyContents();
|
||||
|
||||
if (!$id || !$body) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->uploadService->uploadChunk($id, $body);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
76
application/Espo/Tools/Attachment/Api/PostCopy.php
Normal file
76
application/Espo/Tools/Attachment/Api/PostCopy.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Tools\Attachment\FieldData;
|
||||
use Espo\Tools\Attachment\Service;
|
||||
|
||||
/**
|
||||
* Copies attachments.
|
||||
*/
|
||||
class PostCopy implements Action
|
||||
{
|
||||
public function __construct(private Service $service) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$field = $data->field ?? null;
|
||||
$parentType = $data->parentType ?? null;
|
||||
$relatedType = $data->relatedType ?? null;
|
||||
|
||||
if (!$field) {
|
||||
throw new BadRequest("No `field`.");
|
||||
}
|
||||
|
||||
try {
|
||||
$fieldData = new FieldData($field, $parentType, $relatedType);
|
||||
} catch (Error $e) {
|
||||
throw new BadRequest($e->getMessage());
|
||||
}
|
||||
|
||||
$attachment = $this->service->copy($id, $fieldData);
|
||||
|
||||
return ResponseComposer::json($attachment->getValueMap());
|
||||
}
|
||||
}
|
||||
71
application/Espo/Tools/Attachment/Api/PostFromImageUrl.php
Normal file
71
application/Espo/Tools/Attachment/Api/PostFromImageUrl.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Tools\Attachment\FieldData;
|
||||
use Espo\Tools\Attachment\UploadUrlService;
|
||||
|
||||
/**
|
||||
* Crates attachments from image URLs.
|
||||
*/
|
||||
class PostFromImageUrl implements Action
|
||||
{
|
||||
public function __construct(private UploadUrlService $uploadUrlService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$url = $data->url ?? null;
|
||||
$field = $data->field ?? null;
|
||||
$parentType = $data->parentType ?? null;
|
||||
$relatedType = $data->relatedType ?? null;
|
||||
|
||||
if (!$url || !$field) {
|
||||
throw new BadRequest("No `url` or `field`.");
|
||||
}
|
||||
|
||||
try {
|
||||
$fieldData = new FieldData($field, $parentType, $relatedType);
|
||||
} catch (Error $e) {
|
||||
throw new BadRequest($e->getMessage());
|
||||
}
|
||||
|
||||
$attachment = $this->uploadUrlService->uploadImage($url, $fieldData);
|
||||
|
||||
return ResponseComposer::json($attachment->getValueMap());
|
||||
}
|
||||
}
|
||||
161
application/Espo/Tools/Attachment/Checker.php
Normal file
161
application/Espo/Tools/Attachment/Checker.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Utils\File\MimeType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
|
||||
class Checker
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private MimeType $mimeType,
|
||||
private DetailsObtainer $detailsObtainer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Check a mine-type for allowance.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function checkType(Attachment $attachment): void
|
||||
{
|
||||
$field = $attachment->getTargetField();
|
||||
$entityType = $attachment->getParentType() ?? $attachment->getRelatedType();
|
||||
|
||||
if (!$field || !$entityType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->detailsObtainer->getFieldType($attachment) === FieldType::IMAGE ||
|
||||
$attachment->getRole() === Attachment::ROLE_INLINE_ATTACHMENT
|
||||
) {
|
||||
$this->checkTypeImage($attachment);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$extension = strtolower(DetailsObtainer::getFileExtension($attachment) ?? '');
|
||||
|
||||
$mimeType = $this->mimeType->getMimeTypeByExtension($extension) ??
|
||||
$attachment->getType();
|
||||
|
||||
/** @var string[] $accept */
|
||||
$accept = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'accept']) ?? [];
|
||||
|
||||
if ($accept === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$found = false;
|
||||
|
||||
foreach ($accept as $token) {
|
||||
if (strtolower($token) === '.' . $extension) {
|
||||
$found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($mimeType && MimeType::matchMimeTypeToAcceptToken($mimeType, $token)) {
|
||||
$found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
throw new ForbiddenSilent("Not allowed file type.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a mime-time for allowance for an image.
|
||||
*
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function checkTypeImage(Attachment $attachment, ?string $filePath = null): void
|
||||
{
|
||||
$extension = DetailsObtainer::getFileExtension($attachment) ?? '';
|
||||
|
||||
$mimeType = $this->mimeType->getMimeTypeByExtension($extension);
|
||||
|
||||
/** @var string[] $imageTypeList */
|
||||
$imageTypeList = $this->metadata->get(['app', 'image', 'allowedFileTypeList']) ?? [];
|
||||
|
||||
if (!in_array($mimeType, $imageTypeList)) {
|
||||
throw new ForbiddenSilent("Not allowed file type.");
|
||||
}
|
||||
|
||||
$setMimeType = $attachment->getType();
|
||||
|
||||
if (strtolower($setMimeType ?? '') !== $mimeType) {
|
||||
throw new ForbiddenSilent("Passed type does not correspond to extension.");
|
||||
}
|
||||
|
||||
$this->checkDetectedMimeType($attachment, $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkDetectedMimeType(Attachment $attachment, ?string $filePath = null): void
|
||||
{
|
||||
// ext-fileinfo required, otherwise bypass.
|
||||
if (!class_exists('\finfo') || !defined('FILEINFO_MIME_TYPE')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ?string $contents */
|
||||
$contents = $attachment->get('contents');
|
||||
|
||||
if (!$contents && !$filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
$extension = DetailsObtainer::getFileExtension($attachment) ?? '';
|
||||
|
||||
$mimeTypeList = $this->mimeType->getMimeTypeListByExtension($extension);
|
||||
|
||||
$fileInfo = new \finfo(FILEINFO_MIME_TYPE);
|
||||
|
||||
$detectedMimeType = $filePath ?
|
||||
$fileInfo->file($filePath) :
|
||||
$fileInfo->buffer($contents);
|
||||
|
||||
if (!in_array($detectedMimeType, $mimeTypeList)) {
|
||||
throw new ForbiddenSilent("Detected mime type does not correspond to extension.");
|
||||
}
|
||||
}
|
||||
}
|
||||
99
application/Espo/Tools/Attachment/DetailsObtainer.php
Normal file
99
application/Espo/Tools/Attachment/DetailsObtainer.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\Attachment;
|
||||
|
||||
class DetailsObtainer
|
||||
{
|
||||
private Metadata $metadata;
|
||||
private Config $config;
|
||||
|
||||
public function __construct(
|
||||
Metadata $metadata,
|
||||
Config $config
|
||||
) {
|
||||
$this->metadata = $metadata;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file extension.
|
||||
*/
|
||||
public static function getFileExtension(Attachment $attachment): ?string
|
||||
{
|
||||
$name = $attachment->getName() ?? '';
|
||||
|
||||
return array_slice(explode('.', $name), -1)[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an upload max size allowed for an attachment (depending on a field it's related to).
|
||||
*
|
||||
* @return int A size in bytes.
|
||||
*/
|
||||
public function getUploadMaxSize(Attachment $attachment): int
|
||||
{
|
||||
if ($attachment->getRole() === Attachment::ROLE_INLINE_ATTACHMENT) {
|
||||
return $this->config->get('inlineAttachmentUploadMaxSize') * 1024 * 1024;
|
||||
}
|
||||
|
||||
$field = $attachment->getTargetField();
|
||||
$parentType = $attachment->getParentType() ?? $attachment->getRelatedType();
|
||||
|
||||
if ($field && $parentType) {
|
||||
$maxSize = ($this->metadata
|
||||
->get(['entityDefs', $parentType, 'fields', $field, 'maxFileSize']) ?? 0) * 1024 * 1024;
|
||||
|
||||
if ($maxSize) {
|
||||
return $maxSize;
|
||||
}
|
||||
}
|
||||
|
||||
return (int) $this->config->get('attachmentUploadMaxSize', 0) * 1024 * 1024;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a field type (an attachment if related to another record through the field).
|
||||
*/
|
||||
public function getFieldType(Attachment $attachment): ?string
|
||||
{
|
||||
$field = $attachment->getTargetField();
|
||||
$entityType = $attachment->getParentType() ?? $attachment->getRelatedType();
|
||||
|
||||
if (!$field || !$entityType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']);
|
||||
}
|
||||
}
|
||||
74
application/Espo/Tools/Attachment/FieldData.php
Normal file
74
application/Espo/Tools/Attachment/FieldData.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class FieldData
|
||||
{
|
||||
private string $field;
|
||||
private ?string $parentType;
|
||||
private ?string $relatedType;
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function __construct(
|
||||
string $field,
|
||||
?string $parentType,
|
||||
?string $relatedType
|
||||
) {
|
||||
$this->field = $field;
|
||||
$this->parentType = $parentType;
|
||||
$this->relatedType = $relatedType;
|
||||
|
||||
if (!$parentType && !$relatedType) {
|
||||
throw new Error("No parentType and relatedType.");
|
||||
}
|
||||
}
|
||||
|
||||
public function getField(): string
|
||||
{
|
||||
return $this->field;
|
||||
}
|
||||
|
||||
public function getParentType(): ?string
|
||||
{
|
||||
return $this->parentType;
|
||||
}
|
||||
|
||||
public function getRelatedType(): ?string
|
||||
{
|
||||
return $this->relatedType;
|
||||
}
|
||||
}
|
||||
75
application/Espo/Tools/Attachment/FileData.php
Normal file
75
application/Espo/Tools/Attachment/FileData.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class FileData
|
||||
{
|
||||
private ?string $name;
|
||||
private ?string $type;
|
||||
private StreamInterface $stream;
|
||||
private int $size;
|
||||
|
||||
public function __construct(
|
||||
?string $name,
|
||||
?string $type,
|
||||
StreamInterface $stream,
|
||||
int $size
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->stream = $stream;
|
||||
$this->size = $size;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getStream(): StreamInterface
|
||||
{
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
}
|
||||
118
application/Espo/Tools/Attachment/Jobs/MoveToStorage.php
Normal file
118
application/Espo/Tools/Attachment/Jobs/MoveToStorage.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment\Jobs;
|
||||
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Core\Job\JobSchedulerFactory;
|
||||
|
||||
use Espo\Core\Field\DateTime;
|
||||
|
||||
use Espo\Core\FileStorage\Storages\EspoUploadDir;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\FileStorage\Manager as FileStorageManager;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Entities\Attachment;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class MoveToStorage implements Job
|
||||
{
|
||||
private const REMOVE_FILE_PERIOD = '3 hours';
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
private Config $config;
|
||||
|
||||
private FileStorageManager $fileStorageManager;
|
||||
|
||||
private JobSchedulerFactory $jobSchedulerFactory;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
Config $config,
|
||||
FileStorageManager $fileStorageManager,
|
||||
JobSchedulerFactory $jobSchedulerFactory
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->config = $config;
|
||||
$this->fileStorageManager = $fileStorageManager;
|
||||
$this->jobSchedulerFactory = $jobSchedulerFactory;
|
||||
}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$id = $data->getTargetId();
|
||||
|
||||
if (!$id) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
/** @var Attachment|null $attachment */
|
||||
$attachment = $this->entityManager->getEntityById(Attachment::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$attachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($attachment->getStorage() !== EspoUploadDir::NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaultFileStorage = $this->config->get('defaultFileStorage');
|
||||
|
||||
if (!$defaultFileStorage || $defaultFileStorage === EspoUploadDir::NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stream = $this->fileStorageManager->getStream($attachment);
|
||||
|
||||
$attachment->set('storage', $defaultFileStorage);
|
||||
|
||||
$this->fileStorageManager->putStream($attachment, $stream);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
$this->jobSchedulerFactory->create()
|
||||
->setClassName(RemoveUploadDirFile::class)
|
||||
->setData(
|
||||
Data::create()
|
||||
->withTargetId($attachment->getId())
|
||||
)
|
||||
->setTime(
|
||||
DateTime::createNow()
|
||||
->modify('+' . self::REMOVE_FILE_PERIOD)
|
||||
->toDateTime()
|
||||
)
|
||||
->schedule();
|
||||
}
|
||||
}
|
||||
102
application/Espo/Tools/Attachment/Jobs/RemoveUploadDirFile.php
Normal file
102
application/Espo/Tools/Attachment/Jobs/RemoveUploadDirFile.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment\Jobs;
|
||||
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
|
||||
use Espo\Core\FileStorage\Factory as FileStorageFactory;
|
||||
use Espo\Core\FileStorage\Storages\EspoUploadDir;
|
||||
use Espo\Core\FileStorage\Local;
|
||||
|
||||
use Espo\Entities\Attachment;
|
||||
|
||||
use Espo\Core\FileStorage\AttachmentEntityWrapper;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class RemoveUploadDirFile implements Job
|
||||
{
|
||||
private FileManager $fileManager;
|
||||
|
||||
private FileStorageFactory $fileStorageFactory;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(
|
||||
FileManager $fileManager,
|
||||
FileStorageFactory $fileStorageFactory,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->fileManager = $fileManager;
|
||||
$this->fileStorageFactory = $fileStorageFactory;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$id = $data->getTargetId();
|
||||
|
||||
if (!$id) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
/** @var Attachment|null $attachment */
|
||||
$attachment = $this->entityManager->getEntityById(Attachment::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$attachment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($attachment->getStorage() === EspoUploadDir::NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
$storage = $this->fileStorageFactory->create(EspoUploadDir::NAME);
|
||||
|
||||
if (!$storage instanceof Local) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$filePath = $storage->getLocalFilePath(
|
||||
new AttachmentEntityWrapper($attachment)
|
||||
);
|
||||
|
||||
if (!$this->fileManager->isFile($filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->fileManager->remove($filePath);
|
||||
}
|
||||
}
|
||||
117
application/Espo/Tools/Attachment/Service.php
Normal file
117
application/Espo/Tools/Attachment/Service.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\Attachment as AttachmentRepository;
|
||||
|
||||
class Service
|
||||
{
|
||||
private ServiceContainer $recordServiceContainer;
|
||||
private EntityManager $entityManager;
|
||||
private AccessChecker $accessChecker;
|
||||
|
||||
public function __construct(
|
||||
ServiceContainer $recordServiceContainer,
|
||||
EntityManager $entityManager,
|
||||
AccessChecker $accessChecker
|
||||
) {
|
||||
$this->recordServiceContainer = $recordServiceContainer;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->accessChecker = $accessChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file data (for downloading).
|
||||
*
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getFileData(string $id): FileData
|
||||
{
|
||||
/** @var ?Attachment $attachment */
|
||||
$attachment = $this->recordServiceContainer
|
||||
->get(Attachment::ENTITY_TYPE)
|
||||
->getEntity($id);
|
||||
|
||||
if (!$attachment) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
return new FileData(
|
||||
$attachment->getName(),
|
||||
$attachment->getType(),
|
||||
$this->getAttachmentRepository()->getStream($attachment),
|
||||
$this->getAttachmentRepository()->getSize($attachment)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy an attachment record (to reuse the same file w/o copying it in the storage).
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function copy(string $id, FieldData $data): Attachment
|
||||
{
|
||||
$this->accessChecker->check($data);
|
||||
|
||||
/** @var ?Attachment $attachment */
|
||||
$attachment = $this->recordServiceContainer
|
||||
->get(Attachment::ENTITY_TYPE)
|
||||
->getEntity($id);
|
||||
|
||||
if (!$attachment) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$copied = $this->getAttachmentRepository()->getCopiedAttachment($attachment);
|
||||
|
||||
$copied->set('parentType', $data->getParentType());
|
||||
$copied->set('relatedType', $data->getRelatedType());
|
||||
$copied->setTargetField($data->getField());
|
||||
$copied->setRole(Attachment::ROLE_ATTACHMENT);
|
||||
|
||||
$this->getAttachmentRepository()->save($copied);
|
||||
|
||||
return $copied;
|
||||
}
|
||||
|
||||
private function getAttachmentRepository(): AttachmentRepository
|
||||
{
|
||||
/** @var AttachmentRepository */
|
||||
return $this->entityManager->getRepositoryByClass(Attachment::class);
|
||||
}
|
||||
}
|
||||
179
application/Espo/Tools/Attachment/UploadService.php
Normal file
179
application/Espo/Tools/Attachment/UploadService.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\FileStorage\Storages\EspoUploadDir;
|
||||
use Espo\Core\Job\Job\Data as JobData;
|
||||
use Espo\Core\Job\JobSchedulerFactory;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\File\Manager as FileManager;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\Attachment as AttachmentRepository;
|
||||
use Espo\Tools\Attachment\Jobs\MoveToStorage;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
|
||||
class UploadService
|
||||
{
|
||||
private JobSchedulerFactory $jobSchedulerFactory;
|
||||
private ServiceContainer $recordServiceContainer;
|
||||
private Acl $acl;
|
||||
private EntityManager $entityManager;
|
||||
private FileManager $fileManager;
|
||||
private DetailsObtainer $detailsObtainer;
|
||||
private Checker $checker;
|
||||
|
||||
public function __construct(
|
||||
JobSchedulerFactory $jobSchedulerFactory,
|
||||
ServiceContainer $recordServiceContainer,
|
||||
Acl $acl,
|
||||
EntityManager $entityManager,
|
||||
FileManager $fileManager,
|
||||
DetailsObtainer $detailsObtainer,
|
||||
Checker $checker
|
||||
) {
|
||||
$this->jobSchedulerFactory = $jobSchedulerFactory;
|
||||
$this->recordServiceContainer = $recordServiceContainer;
|
||||
$this->acl = $acl;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->fileManager = $fileManager;
|
||||
$this->detailsObtainer = $detailsObtainer;
|
||||
$this->checker = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a chunk.
|
||||
*
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function uploadChunk(string $id, string $fileData): void
|
||||
{
|
||||
if (!$this->acl->checkScope(Attachment::ENTITY_TYPE, Table::ACTION_CREATE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @var ?Attachment $attachment */
|
||||
$attachment = $this->recordServiceContainer
|
||||
->get(Attachment::ENTITY_TYPE)
|
||||
->getEntity($id);
|
||||
|
||||
if (!$attachment) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$attachment->isBeingUploaded()) {
|
||||
throw new Forbidden("Attachment is not being-uploaded.");
|
||||
}
|
||||
|
||||
if ($attachment->getStorage() !== EspoUploadDir::NAME) {
|
||||
throw new Forbidden("Attachment storage is not 'EspoUploadDir'.");
|
||||
}
|
||||
|
||||
$arr = explode(';base64,', $fileData);
|
||||
|
||||
if (count($arr) < 2) {
|
||||
throw new BadRequest("Bad file data.");
|
||||
}
|
||||
|
||||
$contents = base64_decode($arr[1]);
|
||||
|
||||
$filePath = $this->getAttachmentRepository()->getFilePath($attachment);
|
||||
|
||||
$chunkSize = strlen($contents);
|
||||
|
||||
$actualFileSize = 0;
|
||||
|
||||
if ($this->fileManager->isFile($filePath)) {
|
||||
$actualFileSize = $this->fileManager->getSize($filePath);
|
||||
}
|
||||
|
||||
$maxFileSize = $this->detailsObtainer->getUploadMaxSize($attachment);
|
||||
|
||||
if ($actualFileSize + $chunkSize > $maxFileSize) {
|
||||
throw new Forbidden("Max attachment size exceeded.");
|
||||
}
|
||||
|
||||
$this->fileManager->appendContents($filePath, $contents);
|
||||
|
||||
if ($actualFileSize + $chunkSize > $attachment->getSize()) {
|
||||
throw new Error("File size mismatch.");
|
||||
}
|
||||
|
||||
$isLastChunk = $actualFileSize + $chunkSize === $attachment->getSize();
|
||||
|
||||
if (!$isLastChunk) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->detailsObtainer->getFieldType($attachment) === FieldType::IMAGE) {
|
||||
try {
|
||||
$this->checker->checkTypeImage($attachment, $filePath);
|
||||
} catch (Forbidden $e) {
|
||||
$this->entityManager->removeEntity($attachment);
|
||||
|
||||
throw new ForbiddenSilent($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$attachment->set('isBeingUploaded', false);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
$this->createJobMoveToStorage($attachment);
|
||||
}
|
||||
|
||||
private function getAttachmentRepository(): AttachmentRepository
|
||||
{
|
||||
/** @var AttachmentRepository */
|
||||
return $this->entityManager->getRepositoryByClass(Attachment::class);
|
||||
}
|
||||
|
||||
private function createJobMoveToStorage(Attachment $attachment): void
|
||||
{
|
||||
$this->jobSchedulerFactory
|
||||
->create()
|
||||
->setClassName(MoveToStorage::class)
|
||||
->setData(
|
||||
JobData::create()
|
||||
->withTargetId($attachment->getId())
|
||||
)
|
||||
->schedule();
|
||||
}
|
||||
}
|
||||
208
application/Espo/Tools/Attachment/UploadUrlService.php
Normal file
208
application/Espo/Tools/Attachment/UploadUrlService.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Attachment;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Utils\File\MimeType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Security\UrlCheck;
|
||||
use Espo\Entities\Attachment as Attachment;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\Attachment as AttachmentRepository;
|
||||
|
||||
class UploadUrlService
|
||||
{
|
||||
private AccessChecker $accessChecker;
|
||||
private Metadata $metadata;
|
||||
private EntityManager $entityManager;
|
||||
private MimeType $mimeType;
|
||||
private DetailsObtainer $detailsObtainer;
|
||||
|
||||
public function __construct(
|
||||
AccessChecker $accessChecker,
|
||||
Metadata $metadata,
|
||||
EntityManager $entityManager,
|
||||
MimeType $mimeType,
|
||||
DetailsObtainer $detailsObtainer,
|
||||
private UrlCheck $urlCheck
|
||||
) {
|
||||
$this->accessChecker = $accessChecker;
|
||||
$this->metadata = $metadata;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->mimeType = $mimeType;
|
||||
$this->detailsObtainer = $detailsObtainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an image from and URL and store as attachment.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
public function uploadImage(string $url, FieldData $data): Attachment
|
||||
{
|
||||
if (!$this->urlCheck->isNotInternalUrl($url)) {
|
||||
throw new ForbiddenSilent("Not allowed URL.");
|
||||
}
|
||||
|
||||
$attachment = $this->getAttachmentRepository()->getNew();
|
||||
|
||||
$this->accessChecker->check($data);
|
||||
|
||||
[$type, $contents] = $this->getImageDataByUrl($url) ?? [null, null];
|
||||
|
||||
if (!$type || !$contents) {
|
||||
throw new ErrorSilent("Bad image data.");
|
||||
}
|
||||
|
||||
$attachment->set([
|
||||
'name' => $url,
|
||||
'type' => $type,
|
||||
'contents' => $contents,
|
||||
'role' => Attachment::ROLE_ATTACHMENT,
|
||||
]);
|
||||
|
||||
$attachment->set('parentType', $data->getParentType());
|
||||
$attachment->set('relatedType', $data->getRelatedType());
|
||||
$attachment->set('field', $data->getField());
|
||||
|
||||
$size = mb_strlen($contents, '8bit');
|
||||
$maxSize = $this->detailsObtainer->getUploadMaxSize($attachment);
|
||||
|
||||
if ($maxSize && $size > $maxSize) {
|
||||
throw new Error("File size should not exceed {$maxSize}Mb.");
|
||||
}
|
||||
|
||||
$this->getAttachmentRepository()->save($attachment);
|
||||
|
||||
$attachment->clear('contents');
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return ?array{string, string} A type and contents.
|
||||
*/
|
||||
private function getImageDataByUrl(string $url): ?array
|
||||
{
|
||||
$type = null;
|
||||
|
||||
if (!function_exists('curl_init')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$opts = [];
|
||||
|
||||
$httpHeaders = [];
|
||||
$httpHeaders[] = 'Expect:';
|
||||
|
||||
$opts[\CURLOPT_URL] = $url;
|
||||
$opts[\CURLOPT_HTTPHEADER] = $httpHeaders;
|
||||
$opts[\CURLOPT_CONNECTTIMEOUT] = 10;
|
||||
$opts[\CURLOPT_TIMEOUT] = 10;
|
||||
$opts[\CURLOPT_HEADER] = true;
|
||||
$opts[\CURLOPT_VERBOSE] = true;
|
||||
$opts[\CURLOPT_SSL_VERIFYPEER] = true;
|
||||
$opts[\CURLOPT_SSL_VERIFYHOST] = 2;
|
||||
$opts[\CURLOPT_RETURNTRANSFER] = true;
|
||||
// Prevents Server Side Request Forgery by redirecting to an internal host.
|
||||
$opts[\CURLOPT_FOLLOWLOCATION] = false;
|
||||
$opts[\CURLOPT_MAXREDIRS] = 2;
|
||||
$opts[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
|
||||
$opts[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTPS | \CURLPROTO_HTTP;
|
||||
$opts[\CURLOPT_REDIR_PROTOCOLS] = \CURLPROTO_HTTPS;
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt_array($ch, $opts);
|
||||
|
||||
/** @var string|false $response */
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if ($response === false) {
|
||||
curl_close($ch);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$headerSize = curl_getinfo($ch, \CURLINFO_HEADER_SIZE);
|
||||
|
||||
$header = substr($response, 0, $headerSize);
|
||||
$body = substr($response, $headerSize);
|
||||
|
||||
$headLineList = explode("\n", $header);
|
||||
|
||||
foreach ($headLineList as $i => $line) {
|
||||
if ($i === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos(strtolower($line), strtolower('Content-Type:')) === 0) {
|
||||
$part = trim(substr($line, 13));
|
||||
|
||||
if ($part) {
|
||||
$type = trim(explode(";", $part)[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$type) {
|
||||
/** @var string $extension */
|
||||
$extension = preg_replace('#\?.*#', '', pathinfo($url, \PATHINFO_EXTENSION));
|
||||
|
||||
$type = $this->mimeType->getMimeTypeByExtension($extension);
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if (!$type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var string[] $imageTypeList */
|
||||
$imageTypeList = $this->metadata->get(['app', 'image', 'allowedFileTypeList']) ?? [];
|
||||
|
||||
if (!in_array($type, $imageTypeList)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [$type, $body];
|
||||
}
|
||||
|
||||
private function getAttachmentRepository(): AttachmentRepository
|
||||
{
|
||||
/** @var AttachmentRepository */
|
||||
return $this->entityManager->getRepositoryByClass(Attachment::class);
|
||||
}
|
||||
}
|
||||
147
application/Espo/Tools/Captcha/Checker.php
Normal file
147
application/Espo/Tools/Captcha/Checker.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Captcha;
|
||||
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Entities\Integration;
|
||||
use Espo\ORM\EntityManager;
|
||||
use RuntimeException;
|
||||
|
||||
class Checker
|
||||
{
|
||||
private const URL = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
private const SCORE_THRESHOLD = 0.2;
|
||||
private const TIMEOUT = 20;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Log $log,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function check(string $token, string $action): void
|
||||
{
|
||||
[$secret, $scoreThreshold] = $this->getCaptchaSecretKey();
|
||||
|
||||
if ($secret && $token === '') {
|
||||
throw new BadRequest("No captcha token.");
|
||||
}
|
||||
|
||||
if (!$secret) {
|
||||
throw new Forbidden("Captcha not configured.");
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, self::URL);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
|
||||
$data = [
|
||||
'secret' => $secret,
|
||||
'response' => $token,
|
||||
];
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, self::TIMEOUT);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if (!is_string($response)) {
|
||||
throw new RuntimeException("Bad CURL response.");
|
||||
}
|
||||
|
||||
$responseData = Json::decode($response, true);
|
||||
|
||||
if (!is_array($responseData)) {
|
||||
throw new RuntimeException("Bad response from ReCaptcha.");
|
||||
}
|
||||
|
||||
$success = $responseData['success'] ?? null;
|
||||
$score = $responseData['score'] ?? null;
|
||||
$resultAction = $responseData['action'] ?? null;
|
||||
|
||||
if (!$success) {
|
||||
$this->log->error("Captcha error; action: {action}; response: {response}", [
|
||||
'action' => $action,
|
||||
'response' => $response,
|
||||
]);
|
||||
|
||||
throw new Forbidden("ReCaptcha error.");
|
||||
}
|
||||
|
||||
if (!is_string($resultAction)) {
|
||||
throw new RuntimeException("No or bad action in ReCaptcha response.");
|
||||
}
|
||||
|
||||
if (!is_int($score) && !is_float($score)) {
|
||||
throw new RuntimeException("No score in ReCaptcha response.");
|
||||
}
|
||||
|
||||
if ($action !== $resultAction) {
|
||||
throw new Forbidden("ReCaptcha action mismatch.");
|
||||
}
|
||||
|
||||
if ($score < $scoreThreshold) {
|
||||
throw new Forbidden("ReCaptcha low score.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?string, ?int}
|
||||
*/
|
||||
private function getCaptchaSecretKey(): array
|
||||
{
|
||||
$entity = $this->entityManager
|
||||
->getRepositoryByClass(Integration::class)
|
||||
->getById('GoogleReCaptcha');
|
||||
|
||||
if (!$entity) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$secretKey = $entity->get('secretKey');
|
||||
$scoreThreshold = $entity->get('scoreThreshold') ?? self::SCORE_THRESHOLD;
|
||||
|
||||
if (!$secretKey) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [$secretKey, $scoreThreshold];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\CategoryTree\Move;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Templates\Entities\CategoryTree;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class LoopReferenceChecker
|
||||
{
|
||||
private const ATTR_PARENT_ID = 'parentId';
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function check(CategoryTree $entity, Entity $reference): void
|
||||
{
|
||||
$parentId = $reference->get(self::ATTR_PARENT_ID);
|
||||
|
||||
if (!$parentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($parentId === $entity->getId()) {
|
||||
throw new Forbidden("Cannot move. Circle reference.");
|
||||
}
|
||||
|
||||
$parent = $this->entityManager->getEntityById($entity->getEntityType(), $parentId);
|
||||
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->check($entity, $parent);
|
||||
}
|
||||
}
|
||||
42
application/Espo/Tools/CategoryTree/Move/MoveParams.php
Normal file
42
application/Espo/Tools/CategoryTree/Move/MoveParams.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\CategoryTree\Move;
|
||||
|
||||
readonly class MoveParams
|
||||
{
|
||||
public const TYPE_INTO = 0;
|
||||
public const TYPE_BEFORE = 1;
|
||||
public const TYPE_AFTER = 2;
|
||||
|
||||
public function __construct(
|
||||
public int $type,
|
||||
public ?string $referenceId = null,
|
||||
) {}
|
||||
}
|
||||
219
application/Espo/Tools/CategoryTree/MoveService.php
Normal file
219
application/Espo/Tools/CategoryTree/MoveService.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\CategoryTree;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Templates\Entities\CategoryTree;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\Expression as Expr;
|
||||
use Espo\ORM\Query\UpdateBuilder;
|
||||
use Espo\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Tools\CategoryTree\Move\LoopReferenceChecker;
|
||||
use Espo\Tools\CategoryTree\Move\MoveParams;
|
||||
|
||||
class MoveService
|
||||
{
|
||||
private const ATTR_PARENT_ID = 'parentId';
|
||||
private const ATTR_ORDER = 'order';
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private LoopReferenceChecker $loopReferenceChecker,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
public function move(CategoryTree $entity, MoveParams $params): void
|
||||
{
|
||||
$hasOrder = $entity->hasAttribute(self::ATTR_ORDER);
|
||||
|
||||
if (!$hasOrder && $params->type !== MoveParams::TYPE_INTO) {
|
||||
throw new Error("Order not supported.");
|
||||
}
|
||||
|
||||
$entityType = $entity->getEntityType();
|
||||
|
||||
$reference = null;
|
||||
|
||||
if ($params->referenceId) {
|
||||
$reference = $this->entityManager->getEntityById($entityType, $params->referenceId);
|
||||
|
||||
if (!$reference) {
|
||||
throw new NotFound("No reference record found.");
|
||||
}
|
||||
}
|
||||
|
||||
if ($params->type === MoveParams::TYPE_INTO) {
|
||||
$this->processInto($reference, $entity, $params);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$reference) {
|
||||
throw new Error("No reference.");
|
||||
}
|
||||
|
||||
$parentId = $reference->get(self::ATTR_PARENT_ID);
|
||||
|
||||
if ($parentId !== $entity->get(self::ATTR_PARENT_ID) && $parentId) {
|
||||
$parent = $this->entityManager->getEntityById($entityType, $parentId);
|
||||
|
||||
if ($parent && !$this->acl->checkEntityEdit($parent)) {
|
||||
throw new Forbidden("No edit access to target category.");
|
||||
}
|
||||
|
||||
if ($parent) {
|
||||
$this->checkReferenceNoLoop($parent, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
if ($params->type === MoveParams::TYPE_AFTER) {
|
||||
$this->processAfter($reference, $entity);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processBefore($reference, $entity);
|
||||
}
|
||||
|
||||
private function incrementAfter(Entity $reference): void
|
||||
{
|
||||
$update = UpdateBuilder::create()
|
||||
->in($reference->getEntityType())
|
||||
->where([
|
||||
self::ATTR_PARENT_ID => $reference->get(self::ATTR_PARENT_ID),
|
||||
self::ATTR_ORDER . '>' => $reference->get(self::ATTR_ORDER),
|
||||
])
|
||||
->set([
|
||||
self::ATTR_ORDER => Expr::add(Expr::column(self::ATTR_ORDER), 2)
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
private function decrementBefore(Entity $reference): void
|
||||
{
|
||||
$update = UpdateBuilder::create()
|
||||
->in($reference->getEntityType())
|
||||
->where([
|
||||
self::ATTR_PARENT_ID => $reference->get(self::ATTR_PARENT_ID),
|
||||
self::ATTR_ORDER . '<' => $reference->get(self::ATTR_ORDER),
|
||||
])
|
||||
->set([
|
||||
self::ATTR_ORDER => Expr::subtract(Expr::column(self::ATTR_ORDER), 2)
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
private function rearrange(Entity $reference): void
|
||||
{
|
||||
$entities = $this->entityManager
|
||||
->getRDBRepository($reference->getEntityType())
|
||||
->where([
|
||||
self::ATTR_PARENT_ID => $reference->get(self::ATTR_PARENT_ID),
|
||||
])
|
||||
->order(self::ATTR_ORDER)
|
||||
->find();
|
||||
|
||||
foreach ($entities as $i => $entity) {
|
||||
$entity->set(self::ATTR_ORDER, $i + 1);
|
||||
|
||||
$this->entityManager->saveEntity($entity, [SaveOption::SKIP_ALL => true]);
|
||||
}
|
||||
}
|
||||
|
||||
private function processAfter(Entity $reference, CategoryTree $entity): void
|
||||
{
|
||||
$this->incrementAfter($reference);
|
||||
|
||||
$order = ($reference->get(self::ATTR_ORDER) ?? 0) + 1;
|
||||
|
||||
$entity->set(self::ATTR_ORDER, $order);
|
||||
$entity->set(self::ATTR_PARENT_ID, $reference->get(self::ATTR_PARENT_ID));
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$this->rearrange($reference);
|
||||
}
|
||||
|
||||
private function processBefore(Entity $reference, CategoryTree $entity): void
|
||||
{
|
||||
$this->decrementBefore($reference);
|
||||
|
||||
$order = ($reference->get(self::ATTR_ORDER) ?? 0) - 1;
|
||||
|
||||
$entity->set(self::ATTR_ORDER, $order);
|
||||
$entity->set(self::ATTR_PARENT_ID, $reference->get(self::ATTR_PARENT_ID));
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$this->rearrange($reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function processInto(?Entity $reference, CategoryTree $entity, MoveParams $params): void
|
||||
{
|
||||
if ($reference && !$this->acl->checkEntityEdit($reference)) {
|
||||
throw new Forbidden("No edit access to target category.");
|
||||
}
|
||||
|
||||
if ($reference) {
|
||||
$this->checkReferenceNoLoop($reference, $entity);
|
||||
}
|
||||
|
||||
$entity->setMultiple([
|
||||
self::ATTR_PARENT_ID => $params->referenceId,
|
||||
self::ATTR_ORDER => null,
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkReferenceNoLoop(Entity $reference, CategoryTree $entity): void
|
||||
{
|
||||
$this->loopReferenceChecker->check($entity, $reference);
|
||||
}
|
||||
}
|
||||
132
application/Espo/Tools/CategoryTree/RebuildPaths.php
Normal file
132
application/Espo/Tools/CategoryTree/RebuildPaths.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\CategoryTree;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Repositories\CategoryTree;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* Rebuild category tree paths.
|
||||
*/
|
||||
class RebuildPaths
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(string $entityType): void
|
||||
{
|
||||
if (
|
||||
!$this->entityManager->hasRepository($entityType) ||
|
||||
!$this->entityManager->getRepository($entityType) instanceof CategoryTree
|
||||
) {
|
||||
throw new Error("Bad entity type.");
|
||||
}
|
||||
|
||||
$this->clearTable($entityType);
|
||||
|
||||
$this->processBranch($entityType, null);
|
||||
}
|
||||
|
||||
private function clearTable(string $entityType): void
|
||||
{
|
||||
$query = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->delete()
|
||||
->from($entityType . 'Path')
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($query);
|
||||
}
|
||||
|
||||
private function processBranch(string $entityType, ?string $parentId): void
|
||||
{
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->sth()
|
||||
->where(['parentId' => $parentId])
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
$this->processEntity($entity);
|
||||
}
|
||||
}
|
||||
|
||||
private function processEntity(Entity $entity): void
|
||||
{
|
||||
$parentId = $entity->get('parentId');
|
||||
$pathEntityType = $entity->getEntityType() . 'Path';
|
||||
|
||||
if ($parentId) {
|
||||
$subSelect1 = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->select()
|
||||
->from($pathEntityType)
|
||||
->select(['ascendorId', "'" . $entity->getId() . "'"])
|
||||
->where([
|
||||
'descendorId' => $parentId,
|
||||
])
|
||||
->build();
|
||||
|
||||
$insert = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->insert()
|
||||
->into($pathEntityType)
|
||||
->columns(['ascendorId', 'descendorId'])
|
||||
->valuesQuery($subSelect1)
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($insert);
|
||||
}
|
||||
|
||||
$insert = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->insert()
|
||||
->into($pathEntityType)
|
||||
->columns(['ascendorId', 'descendorId'])
|
||||
->values([
|
||||
'ascendorId' => $entity->getId(),
|
||||
'descendorId' => $entity->getId(),
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($insert);
|
||||
|
||||
$this->processBranch($entity->getEntityType(), $entity->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\CategoryTree\Record;
|
||||
|
||||
use Espo\Core\Select\Where\Item;
|
||||
|
||||
readonly class ReadTreeParams
|
||||
{
|
||||
public function __construct(
|
||||
public ?Item $where = null,
|
||||
public bool $onlyNotEmpty = false,
|
||||
public ?string $currentId = null,
|
||||
public ?int $maxDepth = null,
|
||||
public ?string $parentId = null,
|
||||
) {}
|
||||
}
|
||||
52
application/Espo/Tools/Currency/Api/Get.php
Normal file
52
application/Espo/Tools/Currency/Api/Get.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Currency\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Tools\Currency\RateService as Service;
|
||||
|
||||
/**
|
||||
* Gets rates.
|
||||
*/
|
||||
class Get implements Action
|
||||
{
|
||||
public function __construct(private Service $service)
|
||||
{}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$result = $this->service->get()->toAssoc();
|
||||
|
||||
return ResponseComposer::json($result);
|
||||
}
|
||||
}
|
||||
57
application/Espo/Tools/Currency/Api/PutUpdate.php
Normal file
57
application/Espo/Tools/Currency/Api/PutUpdate.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Currency\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Currency\Rates;
|
||||
use Espo\Tools\Currency\RateService as Service;
|
||||
|
||||
/**
|
||||
* Updates rates.
|
||||
*/
|
||||
class PutUpdate implements Action
|
||||
{
|
||||
public function __construct(private Service $service)
|
||||
{}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$rates = Rates::fromAssoc(get_object_vars($data), '___');
|
||||
|
||||
$this->service->set($rates);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Currency\Conversion;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Currency\Converter;
|
||||
use Espo\Core\Currency\Rates;
|
||||
use Espo\Core\Field\Currency;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* @implements EntityConverter<CoreEntity>
|
||||
*/
|
||||
class DefaultEntityConverter implements EntityConverter
|
||||
{
|
||||
public function __construct(
|
||||
private Converter $converter,
|
||||
private EntityManager $entityManager,
|
||||
private Metadata $metadata,
|
||||
private Acl $acl
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param CoreEntity $entity
|
||||
*/
|
||||
public function convert(Entity $entity, string $targetCurrency, Rates $rates): void
|
||||
{
|
||||
$entityDefs = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entity->getEntityType());
|
||||
|
||||
foreach ($this->getFieldList($entity->getEntityType()) as $field) {
|
||||
$disabled = $entityDefs->getField($field)->getParam('conversionDisabled');
|
||||
|
||||
if ($disabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $entity->getValueObject($field);
|
||||
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$value instanceof Currency) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
if ($targetCurrency === $value->getCode()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$convertedValue = $this->converter->convertWithRates($value, $targetCurrency, $rates);
|
||||
|
||||
$entity->setValueObject($field, $convertedValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getFieldList(string $entityType): array
|
||||
{
|
||||
$resultList = [];
|
||||
|
||||
/** @var string[] $requiredFieldList */
|
||||
$requiredFieldList = $this->metadata->get(['scopes', $entityType, 'currencyConversionAccessRequiredFieldList']);
|
||||
|
||||
$allFields = $requiredFieldList !== null;
|
||||
|
||||
$fieldDefsList = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->getFieldList();
|
||||
|
||||
foreach ($fieldDefsList as $fieldDefs) {
|
||||
$field = $fieldDefs->getName();
|
||||
$type = $fieldDefs->getType();
|
||||
|
||||
if ($type !== FieldType::CURRENCY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!$allFields &&
|
||||
!$this->acl->checkField($entityType, $field, Table::ACTION_EDIT)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resultList[] = $field;
|
||||
}
|
||||
|
||||
return $resultList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Currency\Conversion;
|
||||
|
||||
use Espo\Core\Currency\Rates;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* Converts entity currency values. Is not supposed to save the entity.
|
||||
*
|
||||
* @template TEntity of Entity
|
||||
*/
|
||||
interface EntityConverter
|
||||
{
|
||||
/**
|
||||
* @param TEntity $entity
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function convert(Entity $entity, string $targetCurrency, Rates $rates): void;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Currency\Conversion;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class EntityConverterFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private User $user,
|
||||
private Acl $acl
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return EntityConverter<Entity>
|
||||
*/
|
||||
public function create(string $entityType): EntityConverter
|
||||
{
|
||||
/** @var class-string<EntityConverter<Entity>> $className */
|
||||
$className = $this->metadata
|
||||
->get(['app', 'currencyConversion', 'entityConverterClassNameMap', $entityType]) ??
|
||||
DefaultEntityConverter::class;
|
||||
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(User::class, $this->user)
|
||||
->bindInstance(Acl::class, $this->acl)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding($className, $binding);
|
||||
}
|
||||
}
|
||||
122
application/Espo/Tools/Currency/RateService.php
Normal file
122
application/Espo/Tools/Currency/RateService.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Currency;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Currency\ConfigDataProvider;
|
||||
use Espo\Core\Currency\Rates;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
use Espo\Core\Utils\Currency\DatabasePopulator;
|
||||
|
||||
class RateService
|
||||
{
|
||||
private const SCOPE = 'Currency';
|
||||
|
||||
public function __construct(
|
||||
private ConfigWriter $configWriter,
|
||||
private Acl $acl,
|
||||
private DatabasePopulator $databasePopulator,
|
||||
private ConfigDataProvider $configDataProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function get(): Rates
|
||||
{
|
||||
if (!$this->acl->check(self::SCOPE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($this->acl->getLevel(self::SCOPE, Table::ACTION_READ) !== Table::LEVEL_YES) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$rates = Rates::create($this->configDataProvider->getBaseCurrency());
|
||||
|
||||
foreach ($this->configDataProvider->getCurrencyList() as $code) {
|
||||
$rates = $rates->withRate($code, $this->configDataProvider->getCurrencyRate($code));
|
||||
}
|
||||
|
||||
return $rates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function set(Rates $rates): void
|
||||
{
|
||||
if (!$this->acl->check(self::SCOPE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($this->acl->getLevel(self::SCOPE, Table::ACTION_EDIT) !== Table::LEVEL_YES) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$currencyList = $this->configDataProvider->getCurrencyList();
|
||||
$baseCurrency = $this->configDataProvider->getBaseCurrency();
|
||||
|
||||
$set = [];
|
||||
|
||||
foreach ($rates->toAssoc() as $key => $value) {
|
||||
if ($value < 0) {
|
||||
throw new BadRequest("Bad value.");
|
||||
}
|
||||
|
||||
if (!in_array($key, $currencyList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($key === $baseCurrency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$set[$key] = $value;
|
||||
}
|
||||
|
||||
foreach ($currencyList as $currency) {
|
||||
if ($currency === $baseCurrency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$set[$currency] ??= $this->configDataProvider->getCurrencyRate($currency);
|
||||
}
|
||||
|
||||
$this->configWriter->set('currencyRates', $set);
|
||||
$this->configWriter->save();
|
||||
|
||||
$this->databasePopulator->process();
|
||||
}
|
||||
}
|
||||
177
application/Espo/Tools/Dashboard/Service.php
Normal file
177
application/Espo/Tools/Dashboard/Service.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Dashboard;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Entities\DashboardTemplate;
|
||||
use Espo\Entities\Preferences;
|
||||
use Espo\Entities\Team;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class Service
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $userIdList
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function deployTemplateToUsers(string $id, array $userIdList, bool $append = false): void
|
||||
{
|
||||
$template = $this->entityManager->getEntityById(DashboardTemplate::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$template) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound("User not found.");
|
||||
}
|
||||
|
||||
if ($user->isPortal() || $user->isApi()) {
|
||||
throw new Forbidden("Not allowed user type.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$preferences) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->applyTemplate($preferences, $template, $append);
|
||||
|
||||
$this->entityManager->saveEntity($preferences);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function deployTemplateToTeam(string $id, string $teamId, bool $append = false): void
|
||||
{
|
||||
/** @var ?DashboardTemplate $template */
|
||||
$template = $this->entityManager->getEntityById(DashboardTemplate::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$template) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$team = $this->entityManager->getEntityById(Team::ENTITY_TYPE, $teamId);
|
||||
|
||||
if (!$team) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$userList = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->join(Field::TEAMS)
|
||||
->distinct()
|
||||
->where([
|
||||
Field::TEAMS . '.id' => $teamId,
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($userList as $user) {
|
||||
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $user->getId());
|
||||
|
||||
if (!$preferences) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->applyTemplate($preferences, $template, $append);
|
||||
|
||||
$this->entityManager->saveEntity($preferences);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyTemplate(Entity $preferences, DashboardTemplate $template, bool $append): void
|
||||
{
|
||||
if (!$append) {
|
||||
$preferences->set([
|
||||
'dashboardLayout' => $template->get('layout'),
|
||||
'dashletsOptions' => $template->get('dashletsOptions'),
|
||||
]);
|
||||
} else {
|
||||
$dashletsOptions = $preferences->get('dashletsOptions');
|
||||
|
||||
if (!$dashletsOptions) {
|
||||
$dashletsOptions = (object) [];
|
||||
}
|
||||
|
||||
$dashboardLayout = $preferences->get('dashboardLayout');
|
||||
|
||||
if (!$dashboardLayout) {
|
||||
$dashboardLayout = [];
|
||||
}
|
||||
|
||||
foreach ($template->get('layout') as $item) {
|
||||
$exists = false;
|
||||
|
||||
foreach ($dashboardLayout as $k => $item2) {
|
||||
if (isset($item->id) && isset($item2->id)) {
|
||||
if ($item->id === $item2->id) {
|
||||
$exists = true;
|
||||
$dashboardLayout[$k] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$exists) {
|
||||
$dashboardLayout[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($template->get('dashletsOptions') as $id => $item) {
|
||||
$dashletsOptions->$id = $item;
|
||||
}
|
||||
|
||||
$preferences->set([
|
||||
'dashboardLayout' => $dashboardLayout,
|
||||
'dashletsOptions' => $dashletsOptions,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
177
application/Espo/Tools/DataPrivacy/Erasor.php
Normal file
177
application/Espo/Tools/DataPrivacy/Erasor.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\DataPrivacy;
|
||||
|
||||
use Espo\Core\Acl\Permission;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\FieldProcessing\EmailAddress\AccessChecker as EmailAddressAccessChecker;
|
||||
use Espo\Core\FieldProcessing\PhoneNumber\AccessChecker as PhoneNumberAccessChecker;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Record\ServiceContainer as RecordServiceContainer;
|
||||
|
||||
use Espo\Core\Di;
|
||||
use Espo\Entities\Attachment;
|
||||
|
||||
class Erasor implements
|
||||
|
||||
Di\AclAware,
|
||||
Di\AclManagerAware,
|
||||
Di\MetadataAware,
|
||||
Di\ServiceFactoryAware,
|
||||
Di\EntityManagerAware,
|
||||
Di\FieldUtilAware,
|
||||
Di\UserAware
|
||||
{
|
||||
use Di\AclSetter;
|
||||
use Di\AclManagerSetter;
|
||||
use Di\MetadataSetter;
|
||||
use Di\ServiceFactorySetter;
|
||||
use Di\EntityManagerSetter;
|
||||
use Di\FieldUtilSetter;
|
||||
use Di\UserSetter;
|
||||
|
||||
public function __construct(
|
||||
private RecordServiceContainer $recordServiceContainer,
|
||||
private EmailAddressAccessChecker $emailAddressAccessChecker,
|
||||
private PhoneNumberAccessChecker $phoneNumberAccessChecker
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string[] $fieldList
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function erase(string $entityType, string $id, array $fieldList): void
|
||||
{
|
||||
if ($this->acl->getPermissionLevel(Permission::DATA_PRIVACY) === Table::LEVEL_NO) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$service = $this->recordServiceContainer->get($entityType);
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->acl->check($entity, Table::ACTION_EDIT)) {
|
||||
throw new Forbidden("No edit access.");
|
||||
}
|
||||
|
||||
$forbiddenFieldList = $this->acl->getScopeForbiddenFieldList($entityType, Table::ACTION_EDIT);
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
if (in_array($field, $forbiddenFieldList)) {
|
||||
throw new Forbidden("Field '$field' is forbidden to edit.");
|
||||
}
|
||||
}
|
||||
|
||||
$service->loadAdditionalFields($entity);
|
||||
|
||||
$fieldUtil = $this->fieldUtil;
|
||||
|
||||
foreach ($fieldList as $field) {
|
||||
$type = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']);
|
||||
|
||||
$attributeList = $fieldUtil->getActualAttributeList($entityType, $field);
|
||||
|
||||
if ($type === FieldType::EMAIL) {
|
||||
$emailAddressList = $entity->get('emailAddresses');
|
||||
|
||||
foreach ($emailAddressList as $emailAddress) {
|
||||
if (
|
||||
$this->emailAddressAccessChecker
|
||||
->checkEdit($this->user, $emailAddress, $entity)
|
||||
) {
|
||||
$emailAddress->set(Field::NAME, 'ERASED:' . $emailAddress->getId());
|
||||
$emailAddress->set('optOut', true);
|
||||
$this->entityManager->saveEntity($emailAddress);
|
||||
}
|
||||
}
|
||||
|
||||
$entity->clear($field);
|
||||
$entity->clear($field . 'Data');
|
||||
|
||||
continue;
|
||||
} else if ($type === FieldType::PHONE) {
|
||||
$phoneNumberList = $entity->get('phoneNumbers');
|
||||
|
||||
foreach ($phoneNumberList as $phoneNumber) {
|
||||
if (
|
||||
$this->phoneNumberAccessChecker
|
||||
->checkEdit($this->user, $phoneNumber, $entity)
|
||||
) {
|
||||
$phoneNumber->set(Field::NAME, 'ERASED:' . $phoneNumber->getId());
|
||||
|
||||
$this->entityManager->saveEntity($phoneNumber);
|
||||
}
|
||||
}
|
||||
|
||||
$entity->clear($field);
|
||||
$entity->clear($field . 'Data');
|
||||
|
||||
continue;
|
||||
} else if ($type === FieldType::FILE || $type === FieldType::IMAGE) {
|
||||
$attachmentId = $entity->get($field . 'Id');
|
||||
|
||||
if ($attachmentId) {
|
||||
$attachment = $this->entityManager->getEntityById(Attachment::ENTITY_TYPE, $attachmentId);
|
||||
|
||||
if ($attachment) {
|
||||
$this->entityManager->removeEntity($attachment);
|
||||
}
|
||||
}
|
||||
} else if ($type === FieldType::ATTACHMENT_MULTIPLE) {
|
||||
$attachmentList = $entity->get($field);
|
||||
|
||||
foreach ($attachmentList as $attachment) {
|
||||
$this->entityManager->removeEntity($attachment);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($attributeList as $attribute) {
|
||||
if (
|
||||
in_array($entity->getAttributeType($attribute), [$entity::VARCHAR, $entity::TEXT]) &&
|
||||
$entity->get($attribute)
|
||||
) {
|
||||
$entity->set($attribute, null);
|
||||
} else {
|
||||
$entity->set($attribute, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
}
|
||||
}
|
||||
349
application/Espo/Tools/DynamicLogic/ConditionChecker.php
Normal file
349
application/Espo/Tools/DynamicLogic/ConditionChecker.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\DynamicLogic;
|
||||
|
||||
use Espo\Core\Field\Date;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Utils\DateTime\SystemClock;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\DynamicLogic\ConditionChecker\Options;
|
||||
use Espo\Tools\DynamicLogic\Exceptions\BadCondition;
|
||||
|
||||
use Psr\Clock\ClockInterface;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* @since 9.1.0
|
||||
*/
|
||||
class ConditionChecker
|
||||
{
|
||||
/**
|
||||
* Use `ConditionCheckerFactory` instead.
|
||||
*/
|
||||
public function __construct(
|
||||
private Entity $entity,
|
||||
private ?User $user = null,
|
||||
private Options $options = new Options(),
|
||||
private ClockInterface $clock = new SystemClock(),
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadCondition
|
||||
*/
|
||||
public function check(Item $item): bool
|
||||
{
|
||||
$type = $item->type;
|
||||
$value = $item->value;
|
||||
|
||||
if ($type === Type::And) {
|
||||
if (!is_array($value)) {
|
||||
throw new BadCondition();
|
||||
}
|
||||
|
||||
foreach ($value as $subItem) {
|
||||
if (!$subItem instanceof Item) {
|
||||
throw new BadCondition();
|
||||
}
|
||||
|
||||
if (!$this->check($subItem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type === Type::Or) {
|
||||
if (!is_array($value)) {
|
||||
throw new BadCondition();
|
||||
}
|
||||
|
||||
foreach ($value as $subItem) {
|
||||
if (!$subItem instanceof Item) {
|
||||
throw new BadCondition();
|
||||
}
|
||||
|
||||
if ($this->check($subItem)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === Type::Not) {
|
||||
if (!$value instanceof Item) {
|
||||
throw new BadCondition();
|
||||
}
|
||||
|
||||
return !$this->check($value);
|
||||
}
|
||||
|
||||
if (!$item->attribute) {
|
||||
throw new BadCondition("No attribute.");
|
||||
}
|
||||
|
||||
$setValue = $this->getAttributeValue($item->attribute);
|
||||
|
||||
if ($type === Type::Equals) {
|
||||
return $setValue === $value;
|
||||
}
|
||||
|
||||
if ($type === Type::NotEquals) {
|
||||
return $setValue !== $value;
|
||||
}
|
||||
|
||||
if ($type === Type::IsEmpty) {
|
||||
return $setValue === [] ||
|
||||
$setValue === null ||
|
||||
$setValue === false ||
|
||||
$setValue === '';
|
||||
}
|
||||
|
||||
if ($type === Type::IsNotEmpty) {
|
||||
return !(
|
||||
$setValue === [] ||
|
||||
$setValue === null ||
|
||||
$setValue === false ||
|
||||
$setValue === ''
|
||||
);
|
||||
}
|
||||
|
||||
if ($type === Type::IsTrue) {
|
||||
return (bool) $setValue;
|
||||
}
|
||||
|
||||
if ($type === Type::IsFalse) {
|
||||
return !$setValue;
|
||||
}
|
||||
|
||||
if ($type === Type::Contains) {
|
||||
if (is_string($setValue) && is_string($value)) {
|
||||
return str_contains($setValue, $value);
|
||||
}
|
||||
|
||||
if (is_array($setValue)) {
|
||||
return in_array($value, $setValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === Type::NotContains) {
|
||||
if (is_string($setValue) && is_string($value)) {
|
||||
return !str_contains($setValue, $value);
|
||||
}
|
||||
|
||||
if (is_array($setValue)) {
|
||||
return !in_array($value, $setValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type === Type::Has) {
|
||||
if (is_array($setValue)) {
|
||||
return in_array($value, $setValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === Type::NotHas) {
|
||||
if (is_array($setValue)) {
|
||||
return !in_array($value, $setValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type === Type::StartsWith) {
|
||||
if (is_string($setValue) && is_string($value)) {
|
||||
return str_starts_with($setValue, $value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === Type::EndsWith) {
|
||||
if (is_string($setValue) && is_string($value)) {
|
||||
return str_ends_with($setValue, $value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === Type::Matches) {
|
||||
if (is_string($setValue) && is_string($value)) {
|
||||
return (bool) preg_match($value, $setValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === Type::GreaterThan) {
|
||||
return $setValue > $value;
|
||||
}
|
||||
|
||||
if ($type === Type::LessThan) {
|
||||
return $setValue < $value;
|
||||
}
|
||||
|
||||
if ($type === Type::GreaterThanOrEquals) {
|
||||
return $setValue >= $value;
|
||||
}
|
||||
|
||||
if ($type === Type::LessThanOrEquals) {
|
||||
return $setValue <= $value;
|
||||
}
|
||||
|
||||
if ($type === Type::In) {
|
||||
if (is_array($value)) {
|
||||
return in_array($setValue, $value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === Type::NotIn) {
|
||||
if (is_array($value)) {
|
||||
return !in_array($setValue, $value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$setValue || !is_string($setValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === Type::IsToday) {
|
||||
if (strlen($setValue) > 10) {
|
||||
$setDateTime = DateTime::fromDateTime($this->createDateTime($setValue));
|
||||
|
||||
$todayStart = $this->createNow()
|
||||
->withTimezone($this->getTimeZone())
|
||||
->withTime(0, 0);
|
||||
|
||||
$todayEnd = $todayStart->addDays(1);
|
||||
|
||||
return $todayStart->isLessThanOrEqualTo($setDateTime) && $setDateTime->isLessThan($todayEnd);
|
||||
}
|
||||
|
||||
$setDate = Date::fromDateTime($this->createDateTime($setValue));
|
||||
$today = $this->createToday();
|
||||
|
||||
return $setDate->isEqualTo($today);
|
||||
}
|
||||
|
||||
if ($type === Type::InFuture) {
|
||||
if (strlen($setValue) > 10) {
|
||||
$setDateTime = DateTime::fromDateTime($this->createDateTime($setValue));
|
||||
|
||||
return $setDateTime->isGreaterThan($this->createNow());
|
||||
}
|
||||
|
||||
$setDate = Date::fromDateTime($this->createDateTime($setValue));
|
||||
$today = $this->createToday();
|
||||
|
||||
return $setDate->isGreaterThan($today);
|
||||
}
|
||||
|
||||
if ($type === Type::InPast) {
|
||||
if (strlen($setValue) > 10) {
|
||||
$setDateTime = DateTime::fromDateTime($this->createDateTime($setValue));
|
||||
|
||||
return $setDateTime->isLessThan($this->createNow());
|
||||
}
|
||||
|
||||
$setDate = Date::fromDateTime($this->createDateTime($setValue));
|
||||
$today = $this->createToday();
|
||||
|
||||
return $setDate->isLessThan($today);
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line deadCode.unreachable */
|
||||
throw new BadCondition("Unimplemented type '$type->value'.");
|
||||
}
|
||||
|
||||
private function getAttributeValue(string $attribute): mixed
|
||||
{
|
||||
if (str_starts_with($attribute, '$')) {
|
||||
if (!$this->user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($attribute === '$user.id') {
|
||||
return $this->user->getId();
|
||||
}
|
||||
|
||||
if ($attribute === '$user.teamsIds') {
|
||||
return $this->user->getTeamIdList();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->entity->get($attribute);
|
||||
}
|
||||
|
||||
private function getTimeZone(): DateTimeZone
|
||||
{
|
||||
return $this->options->timezone;
|
||||
}
|
||||
|
||||
private function createDateTime(string $value): DateTimeImmutable
|
||||
{
|
||||
try {
|
||||
$setDateTime = new DateTimeImmutable($value, $this->getTimeZone());
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $setDateTime;
|
||||
}
|
||||
|
||||
private function createNow(): DateTime
|
||||
{
|
||||
return DateTime::fromDateTime($this->clock->now());
|
||||
}
|
||||
|
||||
private function createToday(): Date
|
||||
{
|
||||
$dateTime = $this->clock
|
||||
->now()
|
||||
->setTimezone($this->getTimeZone());
|
||||
|
||||
return Date::fromDateTime($dateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\DynamicLogic\ConditionChecker;
|
||||
|
||||
use DateTimeZone;
|
||||
|
||||
readonly class Options
|
||||
{
|
||||
public function __construct(
|
||||
public DateTimeZone $timezone = new DateTimeZone('UTC'),
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\DynamicLogic;
|
||||
|
||||
use DateTimeZone;
|
||||
use Espo\Core\Utils\Config\ApplicationConfig;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Tools\DynamicLogic\ConditionChecker\Options;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @since 9.1.0
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class ConditionCheckerFactory
|
||||
{
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private ApplicationConfig $applicationConfig,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Entity $entity An entity to check.
|
||||
*/
|
||||
public function create(Entity $entity): ConditionChecker
|
||||
{
|
||||
try {
|
||||
$timezone = new DateTimeZone($this->applicationConfig->getTimeZone());
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException('', 0, $e);
|
||||
}
|
||||
|
||||
return new ConditionChecker(
|
||||
entity: $entity,
|
||||
user: $this->user,
|
||||
options: new Options(
|
||||
timezone: $timezone,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\DynamicLogic\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class BadCondition extends Exception
|
||||
{}
|
||||
110
application/Espo/Tools/DynamicLogic/Item.php
Normal file
110
application/Espo/Tools/DynamicLogic/Item.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\DynamicLogic;
|
||||
|
||||
use Espo\Tools\DynamicLogic\Exceptions\BadCondition;
|
||||
use stdClass;
|
||||
|
||||
readonly class Item
|
||||
{
|
||||
public function __construct(
|
||||
public Type $type,
|
||||
public mixed $value,
|
||||
public ?string $attribute = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param stdClass[] $rawItems
|
||||
* @throws BadCondition
|
||||
*/
|
||||
public static function fromGroupDefinition(array $rawItems): Item
|
||||
{
|
||||
return new Item(
|
||||
type: Type::And,
|
||||
value: array_map(fn ($it) => self::fromItemDefinition($it), $rawItems),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadCondition
|
||||
*/
|
||||
public static function fromItemDefinition(stdClass $rawItem): Item
|
||||
{
|
||||
$type = $rawItem->type ?? null;
|
||||
$attribute = $rawItem->attribute ?? null;
|
||||
$value = $rawItem->value ?? null;
|
||||
|
||||
if (!$type || !is_string($type)) {
|
||||
throw new BadCondition("No type.");
|
||||
}
|
||||
|
||||
if ($type === 'has') {
|
||||
$type = 'contains';
|
||||
}
|
||||
|
||||
if ($type === Type::And->value || $type === Type::Or->value) {
|
||||
if (!is_array($value)) {
|
||||
throw new BadCondition("Non-array value.");
|
||||
}
|
||||
|
||||
foreach ($value as $it) {
|
||||
if (!$it instanceof stdClass) {
|
||||
throw new BadCondition("Bad group item value.");
|
||||
}
|
||||
}
|
||||
|
||||
return new Item(
|
||||
type: Type::from($type),
|
||||
value: array_map(fn ($it) => self::fromItemDefinition($it), $value),
|
||||
);
|
||||
}
|
||||
|
||||
if ($type === Type::Not->value) {
|
||||
if (!$value instanceof stdClass) {
|
||||
throw new BadCondition("Bad not item value.");
|
||||
}
|
||||
|
||||
return new Item(
|
||||
type: Type::from($type),
|
||||
value: self::fromItemDefinition($value),
|
||||
);
|
||||
}
|
||||
|
||||
if ($attribute !== null && !is_string($attribute)) {
|
||||
throw new BadCondition("No attribute.");
|
||||
}
|
||||
|
||||
return new Item(
|
||||
type: Type::from($type),
|
||||
value: $value,
|
||||
attribute: $attribute,
|
||||
);
|
||||
}
|
||||
}
|
||||
59
application/Espo/Tools/DynamicLogic/Type.php
Normal file
59
application/Espo/Tools/DynamicLogic/Type.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\DynamicLogic;
|
||||
|
||||
enum Type: string
|
||||
{
|
||||
case And = 'and';
|
||||
case Or = 'or';
|
||||
case Not = 'not';
|
||||
case Equals = 'equals';
|
||||
case NotEquals = 'notEquals';
|
||||
case IsEmpty = 'isEmpty';
|
||||
case IsNotEmpty = 'isNotEmpty';
|
||||
case IsTrue = 'isTrue';
|
||||
case IsFalse = 'isFalse';
|
||||
case Contains = 'contains';
|
||||
case Has = 'has';
|
||||
case NotContains = 'notContains';
|
||||
case NotHas = 'notHas';
|
||||
case StartsWith = 'startsWith';
|
||||
case EndsWith = 'endsWith';
|
||||
case Matches = 'matches';
|
||||
case GreaterThan = 'greaterThan';
|
||||
case LessThan = 'lessThan';
|
||||
case GreaterThanOrEquals = 'greaterThanOrEquals';
|
||||
case LessThanOrEquals = 'lessThanOrEquals';
|
||||
case In = 'in';
|
||||
case NotIn = 'notIn';
|
||||
case IsToday = 'isToday';
|
||||
case InFuture = 'inFuture';
|
||||
case InPast = 'inPast';
|
||||
}
|
||||
399
application/Espo/Tools/Email/AddressService.php
Normal file
399
application/Espo/Tools/Email/AddressService.php
Normal file
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Select\SelectBuilderFactory;
|
||||
use Espo\Core\Select\Text\MetadataProvider as TextMetadataProvider;
|
||||
use Espo\Core\Templates\Entities\Company;
|
||||
use Espo\Core\Templates\Entities\Person;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\Modules\Crm\Entities\Lead;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
use RuntimeException;
|
||||
|
||||
class AddressService
|
||||
{
|
||||
private const ERASED_PREFIX = 'ERASED:';
|
||||
|
||||
private const ATTR_EMAIL_ADDRESS = 'emailAddress';
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private Acl $acl,
|
||||
private Metadata $metadata,
|
||||
private SelectBuilderFactory $selectBuilderFactory,
|
||||
private EntityManager $entityManager,
|
||||
private User $user,
|
||||
private TextMetadataProvider $textMetadataProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function searchInEntityType(string $entityType, string $query, int $limit): array
|
||||
{
|
||||
if (!in_array($entityType, $this->getHavingEmailAddressEntityTypeList())) {
|
||||
throw new NotFound("No 'email' field.");
|
||||
}
|
||||
|
||||
if (!$this->acl->checkScope($entityType, Acl\Table::ACTION_READ)) {
|
||||
throw new Forbidden("No access to $entityType.");
|
||||
}
|
||||
|
||||
if (!$this->acl->checkField($entityType, 'email')) {
|
||||
throw new Forbidden("No access to field 'email' in $entityType.");
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
$this->findInAddressBookByEntityType($query, $limit, $entityType, $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function searchInAddressBook(string $query, int $limit, bool $onlyActual = false): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$entityTypeList = $this->config->get('emailAddressLookupEntityTypeList') ?? [];
|
||||
|
||||
$allEntityTypeList = $this->getHavingEmailAddressEntityTypeList();
|
||||
|
||||
foreach ($entityTypeList as $entityType) {
|
||||
if (!in_array($entityType, $allEntityTypeList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->acl->checkScope($entityType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->findInAddressBookByEntityType($query, $limit, $entityType, $result, $onlyActual);
|
||||
}
|
||||
|
||||
$this->findInInboundEmail($query, $result);
|
||||
|
||||
$finalResult = [];
|
||||
|
||||
foreach ($result as $item) {
|
||||
foreach ($finalResult as $item1) {
|
||||
if ($item['emailAddress'] == $item1['emailAddress']) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$finalResult[] = $item;
|
||||
}
|
||||
|
||||
usort($finalResult, function ($item1, $item2) use ($query) {
|
||||
if (!str_contains($query, '@')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$p1 = strpos($item1['emailAddress'], $query);
|
||||
$p2 = strpos($item2['emailAddress'], $query);
|
||||
|
||||
if ($p1 === 0 && $p2 !== 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($p1 !== 0 && $p2 !== 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($p1 !== 0 && $p2 === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return $finalResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getHavingEmailAddressEntityTypeList(): array
|
||||
{
|
||||
$list = [
|
||||
Account::ENTITY_TYPE,
|
||||
Contact::ENTITY_TYPE,
|
||||
Lead::ENTITY_TYPE,
|
||||
User::ENTITY_TYPE,
|
||||
];
|
||||
|
||||
$scopeDefs = $this->metadata->get(['scopes']);
|
||||
|
||||
foreach ($scopeDefs as $scope => $defs) {
|
||||
if (
|
||||
empty($defs['disabled']) &&
|
||||
!empty($defs['type']) &&
|
||||
(
|
||||
$defs['type'] === Person::TEMPLATE_TYPE ||
|
||||
$defs['type'] === Company::TEMPLATE_TYPE
|
||||
)
|
||||
) {
|
||||
$list[] = $scope;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $result
|
||||
*/
|
||||
private function findInAddressBookByEntityType(
|
||||
string $filter,
|
||||
int $limit,
|
||||
string $entityType,
|
||||
array &$result,
|
||||
bool $onlyActual = false
|
||||
): void {
|
||||
|
||||
$textFilter = null;
|
||||
$whereClause = [];
|
||||
|
||||
$byEmailAddress = false;
|
||||
|
||||
if (str_contains($filter, '@')) {
|
||||
$byEmailAddress = true;
|
||||
}
|
||||
|
||||
if (
|
||||
!$byEmailAddress &&
|
||||
mb_strlen($filter) < (int) $this->config->get('fullTextSearchMinLength') &&
|
||||
$this->hasFullTextSearch($entityType)
|
||||
) {
|
||||
$byEmailAddress = true;
|
||||
}
|
||||
|
||||
if ($byEmailAddress) {
|
||||
$whereClause = [
|
||||
'emailAddress*' => $filter . '%',
|
||||
];
|
||||
} else {
|
||||
$textFilter = $filter;
|
||||
}
|
||||
|
||||
$selectBuilder = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from($entityType)
|
||||
->withAccessControlFilter();
|
||||
|
||||
if ($textFilter) {
|
||||
$selectBuilder->withTextFilter($textFilter);
|
||||
}
|
||||
|
||||
try {
|
||||
$builder = $selectBuilder
|
||||
->buildQueryBuilder()
|
||||
->where($whereClause)
|
||||
->order('name')
|
||||
->limit(0, $limit);
|
||||
} catch (BadRequest|Forbidden $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
if ($entityType === User::ENTITY_TYPE) {
|
||||
$this->handleQueryBuilderUser($builder);
|
||||
}
|
||||
|
||||
$select = [
|
||||
Field::ID,
|
||||
'emailAddress',
|
||||
Field::NAME,
|
||||
];
|
||||
|
||||
if (
|
||||
$this->metadata->get(['entityDefs', $entityType, 'fields', Field::NAME, 'type']) === FieldType::PERSON_NAME
|
||||
) {
|
||||
$select[] = 'firstName';
|
||||
$select[] = 'lastName';
|
||||
}
|
||||
|
||||
$builder->select($select);
|
||||
|
||||
$collection = $this->entityManager
|
||||
->getRDBRepository($entityType)
|
||||
->clone($builder->build())
|
||||
->find();
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
$emailAddress = $entity->get(self::ATTR_EMAIL_ADDRESS);
|
||||
|
||||
$emailAddressData = $this->getEmailAddressRepository()->getEmailAddressData($entity);
|
||||
|
||||
$skipPrimaryEmailAddress = false;
|
||||
|
||||
if (!$emailAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($emailAddress, self::ERASED_PREFIX)) {
|
||||
$skipPrimaryEmailAddress = true;
|
||||
}
|
||||
|
||||
if ($onlyActual) {
|
||||
if ($entity->get('emailAddressIsOptedOut')) {
|
||||
$skipPrimaryEmailAddress = true;
|
||||
}
|
||||
|
||||
foreach ($emailAddressData as $item) {
|
||||
if ($emailAddress !== $item->emailAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($item->invalid)) {
|
||||
$skipPrimaryEmailAddress = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$skipPrimaryEmailAddress) {
|
||||
$result[] = [
|
||||
'emailAddress' => $emailAddress,
|
||||
'entityName' => $entity->get(Field::NAME),
|
||||
'entityType' => $entityType,
|
||||
'entityId' => $entity->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($emailAddressData as $item) {
|
||||
if ($emailAddress === $item->emailAddress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($item->emailAddress, self::ERASED_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($onlyActual) {
|
||||
if (!empty($item->invalid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($item->optOut)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'emailAddress' => $item->emailAddress,
|
||||
'entityName' => $entity->get(Field::NAME),
|
||||
'entityType' => $entityType,
|
||||
'entityId' => $entity->getId(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $result
|
||||
*/
|
||||
private function findInInboundEmail(string $query, array &$result): void
|
||||
{
|
||||
if ($this->user->isPortal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$list = $this->entityManager
|
||||
->getRDBRepository(InboundEmail::ENTITY_TYPE)
|
||||
->select([
|
||||
'id',
|
||||
'name',
|
||||
'emailAddress',
|
||||
])
|
||||
->where([
|
||||
'emailAddress*' => $query . '%',
|
||||
])
|
||||
->order('name')
|
||||
->find();
|
||||
|
||||
foreach ($list as $item) {
|
||||
$result[] = [
|
||||
'emailAddress' => $item->getEmailAddress(),
|
||||
'entityName' => $item->getName(),
|
||||
'entityType' => InboundEmail::ENTITY_TYPE,
|
||||
'entityId' => $item->getId(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function hasFullTextSearch(string $entityType): bool
|
||||
{
|
||||
return $this->textMetadataProvider->hasFullTextSearch($entityType);
|
||||
}
|
||||
|
||||
private function handleQueryBuilderUser(SelectBuilder $queryBuilder): void
|
||||
{
|
||||
/*if ($this->acl->getPermissionLevel('portalPermission') === Table::LEVEL_NO) {
|
||||
$queryBuilder->where([
|
||||
'type!=' => User::TYPE_PORTAL,
|
||||
]);
|
||||
}*/
|
||||
|
||||
$queryBuilder->where([
|
||||
'isActive' => true,
|
||||
'type!=' => [
|
||||
User::TYPE_PORTAL,
|
||||
User::TYPE_API,
|
||||
User::TYPE_SYSTEM,
|
||||
User::TYPE_SUPER_ADMIN,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function getEmailAddressRepository(): EmailAddressRepository
|
||||
{
|
||||
/** @var EmailAddressRepository */
|
||||
return $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE);
|
||||
}
|
||||
}
|
||||
65
application/Espo/Tools/Email/Api/DeleteInboxImportant.php
Normal file
65
application/Espo/Tools/Email/Api/DeleteInboxImportant.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Email\InboxService;
|
||||
|
||||
/**
|
||||
* Unmark emails as important.
|
||||
*/
|
||||
class DeleteInboxImportant implements Action
|
||||
{
|
||||
public function __construct(private InboxService $inboxService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$ids = $data->ids ?? null;
|
||||
$id = $data->id ?? null;
|
||||
|
||||
if ($ids === null && is_string($id)) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
if (!is_array($ids)) {
|
||||
throw new BadRequest("No `ids`.");
|
||||
}
|
||||
|
||||
$this->inboxService->markAsNotImportantIdList($ids);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
65
application/Espo/Tools/Email/Api/DeleteInboxInTrash.php
Normal file
65
application/Espo/Tools/Email/Api/DeleteInboxInTrash.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Email\InboxService;
|
||||
|
||||
/**
|
||||
* Retrieves emails from trash.
|
||||
*/
|
||||
class DeleteInboxInTrash implements Action
|
||||
{
|
||||
public function __construct(private InboxService $inboxService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$ids = $data->ids ?? null;
|
||||
$id = $data->id ?? null;
|
||||
|
||||
if ($ids === null && is_string($id)) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
if (!is_array($ids)) {
|
||||
throw new BadRequest("No `ids`.");
|
||||
}
|
||||
|
||||
$this->inboxService->retrieveFromTrashIdList($ids);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
65
application/Espo/Tools/Email/Api/DeleteInboxRead.php
Normal file
65
application/Espo/Tools/Email/Api/DeleteInboxRead.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Email\InboxService;
|
||||
|
||||
/**
|
||||
* Unmark emails as read.
|
||||
*/
|
||||
class DeleteInboxRead implements Action
|
||||
{
|
||||
public function __construct(private InboxService $inboxService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$ids = $data->ids ?? null;
|
||||
$id = $data->id ?? null;
|
||||
|
||||
if ($ids === null && is_string($id)) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
if (!is_array($ids)) {
|
||||
throw new BadRequest("No `ids`.");
|
||||
}
|
||||
|
||||
$this->inboxService->markAsNotReadIdList($ids);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
63
application/Espo/Tools/Email/Api/GetInsertFieldData.php
Normal file
63
application/Espo/Tools/Email/Api/GetInsertFieldData.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Entities\Email as EmailEntity;
|
||||
use Espo\Tools\EmailTemplate\InsertField\Service as InsertFieldService;
|
||||
|
||||
class GetInsertFieldData implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private InsertFieldService $service,
|
||||
private Acl $acl
|
||||
) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
if (!$this->acl->checkScope(EmailEntity::ENTITY_TYPE, Table::ACTION_CREATE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$data = $this->service->getData(
|
||||
$request->getQueryParam('parentType'),
|
||||
$request->getQueryParam('parentId'),
|
||||
$request->getQueryParam('to')
|
||||
);
|
||||
|
||||
return ResponseComposer::json($data);
|
||||
}
|
||||
}
|
||||
48
application/Espo/Tools/Email/Api/GetNotReadCounts.php
Normal file
48
application/Espo/Tools/Email/Api/GetNotReadCounts.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Tools\Email\InboxService;
|
||||
|
||||
class GetNotReadCounts implements Action
|
||||
{
|
||||
public function __construct(private InboxService $inboxService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $this->inboxService->getFoldersNotReadCounts();
|
||||
|
||||
return ResponseComposer::json($data);
|
||||
}
|
||||
}
|
||||
87
application/Espo/Tools/Email/Api/PostAttachmentsCopy.php
Normal file
87
application/Espo/Tools/Email/Api/PostAttachmentsCopy.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Tools\Attachment\FieldData;
|
||||
use Espo\Tools\Email\Service;
|
||||
|
||||
/**
|
||||
* Copies email attachments.
|
||||
*/
|
||||
class PostAttachmentsCopy implements Action
|
||||
{
|
||||
public function __construct(private Service $service) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$field = $data->field ?? null;
|
||||
$parentType = $data->parentType ?? null;
|
||||
$relatedType = $data->relatedType ?? null;
|
||||
|
||||
if (!$field) {
|
||||
throw new BadRequest("No `field`.");
|
||||
}
|
||||
|
||||
try {
|
||||
$fieldData = new FieldData($field, $parentType, $relatedType);
|
||||
} catch (Error $e) {
|
||||
throw new BadRequest($e->getMessage());
|
||||
}
|
||||
|
||||
$list = $this->service->copyAttachments($id, $fieldData);
|
||||
|
||||
$ids = array_map(fn ($item) => $item->getId(), $list);
|
||||
|
||||
$names = (object) [];
|
||||
|
||||
foreach ($list as $item) {
|
||||
$names->{$item->getId()} = $item->getName();
|
||||
}
|
||||
|
||||
return ResponseComposer::json([
|
||||
'ids' => $ids,
|
||||
'names' => $names,
|
||||
]);
|
||||
}
|
||||
}
|
||||
75
application/Espo/Tools/Email/Api/PostFolder.php
Normal file
75
application/Espo/Tools/Email/Api/PostFolder.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Email\InboxService;
|
||||
|
||||
/**
|
||||
* Moves emails to a folder.
|
||||
*/
|
||||
class PostFolder implements Action
|
||||
{
|
||||
public function __construct(private InboxService $inboxService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$folderId = $request->getRouteParam('folderId');
|
||||
|
||||
if (!$folderId) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$ids = $data->ids ?? null;
|
||||
$id = $data->id ?? null;
|
||||
|
||||
if ($ids === null && is_string($id)) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
if (!is_array($ids)) {
|
||||
throw new BadRequest("No `ids`.");
|
||||
}
|
||||
|
||||
if (count($ids) === 1) {
|
||||
$this->inboxService->moveToFolder($ids[0], $folderId);
|
||||
}
|
||||
|
||||
$this->inboxService->moveToFolderIdList($ids, $folderId);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
82
application/Espo/Tools/Email/Api/PostImportEml.php
Normal file
82
application/Espo/Tools/Email/Api/PostImportEml.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Tools\Email\ImportEmlService;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class PostImportEml implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private Acl $acl,
|
||||
private User $user,
|
||||
private ImportEmlService $service,
|
||||
) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$this->checkAccess();
|
||||
|
||||
$fileId = $request->getParsedBody()->fileId ?? null;
|
||||
|
||||
if (!is_string($fileId)) {
|
||||
throw new BadRequest("No 'fileId'.");
|
||||
}
|
||||
|
||||
$email = $this->service->import($fileId, $this->user->getId());
|
||||
|
||||
return ResponseComposer::json(['id' => $email->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkAccess(): void
|
||||
{
|
||||
if (!$this->acl->checkScope(Email::ENTITY_TYPE, Acl\Table::ACTION_CREATE)) {
|
||||
throw new Forbidden("No 'create' access.");
|
||||
}
|
||||
|
||||
if (!$this->acl->checkScope('Import')) {
|
||||
throw new Forbidden("No access to 'Import'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
65
application/Espo/Tools/Email/Api/PostInboxImportant.php
Normal file
65
application/Espo/Tools/Email/Api/PostInboxImportant.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Email\InboxService;
|
||||
|
||||
/**
|
||||
* Marks emails as important.
|
||||
*/
|
||||
class PostInboxImportant implements Action
|
||||
{
|
||||
public function __construct(private InboxService $inboxService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$ids = $data->ids ?? null;
|
||||
$id = $data->id ?? null;
|
||||
|
||||
if ($ids === null && is_string($id)) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
if (!is_array($ids)) {
|
||||
throw new BadRequest("No `ids`.");
|
||||
}
|
||||
|
||||
$this->inboxService->markAsImportantIdList($ids);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
65
application/Espo/Tools/Email/Api/PostInboxInTrash.php
Normal file
65
application/Espo/Tools/Email/Api/PostInboxInTrash.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Email\InboxService;
|
||||
|
||||
/**
|
||||
* Moves emails to trash.
|
||||
*/
|
||||
class PostInboxInTrash implements Action
|
||||
{
|
||||
public function __construct(private InboxService $inboxService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$ids = $data->ids ?? null;
|
||||
$id = $data->id ?? null;
|
||||
|
||||
if ($ids === null && is_string($id)) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
if (!is_array($ids)) {
|
||||
throw new BadRequest("No `ids`.");
|
||||
}
|
||||
|
||||
$this->inboxService->moveToTrashIdList($ids);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
71
application/Espo/Tools/Email/Api/PostInboxRead.php
Normal file
71
application/Espo/Tools/Email/Api/PostInboxRead.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\Email\InboxService;
|
||||
|
||||
/**
|
||||
* Marks emails as read.
|
||||
*/
|
||||
class PostInboxRead implements Action
|
||||
{
|
||||
public function __construct(private InboxService $inboxService) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (!empty($data->all)) {
|
||||
$this->inboxService->markAllAsRead();
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
|
||||
$ids = $data->ids ?? null;
|
||||
$id = $data->id ?? null;
|
||||
|
||||
if ($ids === null && is_string($id)) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
if (!is_array($ids)) {
|
||||
throw new BadRequest("No `ids`.");
|
||||
}
|
||||
|
||||
$this->inboxService->markAsReadIdList($ids);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
118
application/Espo/Tools/Email/Api/PostSendTest.php
Normal file
118
application/Espo/Tools/Email/Api/PostSendTest.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Tools\Email\SendService;
|
||||
use Espo\Tools\Email\TestSendData;
|
||||
|
||||
/**
|
||||
* Sends test emails.
|
||||
*/
|
||||
class PostSendTest implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private SendService $sendService,
|
||||
private Acl $acl
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
if (!$this->acl->checkScope(Email::ENTITY_TYPE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$type = $data->type ?? null;
|
||||
$id = $data->id ?? null;
|
||||
$server = $data->server ?? null;
|
||||
$port = $data->port ?? null;
|
||||
$username = $data->username ?? null;
|
||||
$password = $data->password ?? null;
|
||||
$auth = $data->auth ?? null;
|
||||
$authMechanism = $data->authMechanism ?? null;
|
||||
$security = $data->security ?? null;
|
||||
$userId = $data->userId ?? null;
|
||||
$fromAddress = $data->fromAddress ?? null;
|
||||
$fromName = $data->fromName ?? null;
|
||||
$emailAddress = $data->emailAddress ?? null;
|
||||
|
||||
if (!is_string($server)) {
|
||||
throw new BadRequest("No `server`");
|
||||
}
|
||||
|
||||
if (!is_int($port)) {
|
||||
throw new BadRequest("No or bad `port`.");
|
||||
}
|
||||
|
||||
if (!is_string($emailAddress)) {
|
||||
throw new BadRequest("No `emailAddress`.");
|
||||
}
|
||||
|
||||
$smtpParams = SmtpParams
|
||||
::create($server, $port)
|
||||
->withSecurity($security)
|
||||
->withFromName($fromName)
|
||||
->withFromAddress($fromAddress)
|
||||
->withAuth($auth);
|
||||
|
||||
if ($auth) {
|
||||
$smtpParams = $smtpParams
|
||||
->withUsername($username)
|
||||
->withPassword($password)
|
||||
->withAuthMechanism($authMechanism);
|
||||
}
|
||||
|
||||
$data = new TestSendData($emailAddress, $type, $id, $userId);
|
||||
|
||||
$this->sendService->sendTestEmail($smtpParams, $data);
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
}
|
||||
177
application/Espo/Tools/Email/Api/PostUsers.php
Normal file
177
application/Espo/Tools/Email/Api/PostUsers.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email\Api;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Core\Notification\UserEnabledChecker;
|
||||
use Espo\Core\Record\EntityProvider;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Notification;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class PostUsers implements Action
|
||||
{
|
||||
public function __construct(
|
||||
private EntityProvider $entityProvider,
|
||||
private Acl $acl,
|
||||
private EntityManager $entityManager,
|
||||
private UserEnabledChecker $userEnabledChecker,
|
||||
private User $user,
|
||||
) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$id = $request->getRouteParam('id') ?? throw new RuntimeException();
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$email = $this->getEmail($id);
|
||||
|
||||
$foreignIds = [];
|
||||
|
||||
if (isset($data->id)) {
|
||||
$foreignIds[] = $data->id;
|
||||
}
|
||||
|
||||
if (isset($data->ids) && is_array($data->ids)) {
|
||||
foreach ($data->ids as $foreignId) {
|
||||
$foreignIds[] = $foreignId;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($foreignIds as $foreignId) {
|
||||
if (!is_string($foreignId)) {
|
||||
throw new BadRequest("Bad ID.");
|
||||
}
|
||||
}
|
||||
|
||||
$relation = $this->entityManager->getRelation($email, 'users');
|
||||
|
||||
foreach ($this->getUsers($foreignIds) as $user) {
|
||||
if ($relation->isRelated($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relation->relate($user);
|
||||
|
||||
if ($this->user->getId() === $user->getId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->processNotify($email, $user);
|
||||
}
|
||||
|
||||
return ResponseComposer::json(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getEmail(string $id): Email
|
||||
{
|
||||
$email = $this->entityProvider->getByClass(Email::class, $id);
|
||||
|
||||
if (!$this->acl->checkEntityEdit($email)) {
|
||||
throw new Forbidden("No edit access to email.");
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $foreignIds
|
||||
* @return User[]
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getUsers(array $foreignIds): iterable
|
||||
{
|
||||
/** @var iterable<User> $users */
|
||||
$users = $this->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
->where([Attribute::ID => $foreignIds])
|
||||
->find();
|
||||
|
||||
if (is_countable($users) && count($users) !== count($foreignIds)) {
|
||||
throw new NotFound("Users not found.");
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
if (!$this->acl->checkAssignmentPermission($user)) {
|
||||
throw new Forbidden("No assignment permission to user.");
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityRead($user)) {
|
||||
throw new Forbidden("No access to user.");
|
||||
}
|
||||
|
||||
if (!$user->isRegular() && !$user->isAdmin()) {
|
||||
throw new Forbidden("Only regular and admin users allowed.");
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
private function processNotify(Email $email, User $user): void
|
||||
{
|
||||
if (!$this->userEnabledChecker->checkAssignment(Email::ENTITY_TYPE, $user->getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = $this->entityManager->getRDBRepositoryByClass(Notification::class)->getNew();
|
||||
|
||||
$notification
|
||||
->setType('EmailInbox')
|
||||
->setRelated(LinkParent::createFromEntity($email))
|
||||
->setUserId($user->getId())
|
||||
->setData([
|
||||
'emailName' => $email->getSubject(),
|
||||
'userId' => $this->user->getId(),
|
||||
'userName' => $this->user->getName(),
|
||||
]);
|
||||
|
||||
$this->entityManager->saveEntity($notification);
|
||||
}
|
||||
}
|
||||
69
application/Espo/Tools/Email/EmailAddressEntityPair.php
Normal file
69
application/Espo/Tools/Email/EmailAddressEntityPair.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
use Espo\Core\Field\EmailAddress;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Entity;
|
||||
use stdClass;
|
||||
|
||||
class EmailAddressEntityPair
|
||||
{
|
||||
private EmailAddress $emailAddress;
|
||||
private Entity $entity;
|
||||
|
||||
public function __construct(
|
||||
EmailAddress $emailAddress,
|
||||
Entity $entity
|
||||
) {
|
||||
$this->emailAddress = $emailAddress;
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
public function getEmailAddress(): EmailAddress
|
||||
{
|
||||
return $this->emailAddress;
|
||||
}
|
||||
|
||||
public function getEntity(): Entity
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
public function getValueMap(): stdClass
|
||||
{
|
||||
return (object) [
|
||||
'emailAddress' => $this->emailAddress->getAddress(),
|
||||
'name' => $this->entity->get(Field::NAME),
|
||||
'entityId' => $this->entity->getId(),
|
||||
'entityType' => $this->entity->getEntityType(),
|
||||
];
|
||||
}
|
||||
}
|
||||
41
application/Espo/Tools/Email/Folder.php
Normal file
41
application/Espo/Tools/Email/Folder.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
class Folder
|
||||
{
|
||||
public const ALL = 'all';
|
||||
public const INBOX = 'inbox';
|
||||
public const SENT = 'sent';
|
||||
public const DRAFTS = 'drafts';
|
||||
public const IMPORTANT = 'important';
|
||||
public const ARCHIVE = 'archive';
|
||||
public const TRASH = 'trash';
|
||||
}
|
||||
132
application/Espo/Tools/Email/ImportEmlService.php
Normal file
132
application/Espo/Tools/Email/ImportEmlService.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\FileStorage\Manager;
|
||||
use Espo\Core\Mail\Importer;
|
||||
use Espo\Core\Mail\Importer\Data;
|
||||
use Espo\Core\Mail\MessageWrapper;
|
||||
use Espo\Core\Mail\Parsers\MailMimeParser;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class ImportEmlService
|
||||
{
|
||||
public function __construct(
|
||||
private Importer $importer,
|
||||
private Importer\DuplicateFinder $duplicateFinder,
|
||||
private EntityManager $entityManager,
|
||||
private Manager $fileStorageManager,
|
||||
private MailMimeParser $parser,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Import an EML.
|
||||
*
|
||||
* @param string $fileId An attachment ID.
|
||||
* @param ?string $userId A user ID to relate an email with.
|
||||
* @return Email An Email.
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
* @throws Conflict
|
||||
*/
|
||||
public function import(string $fileId, ?string $userId = null): Email
|
||||
{
|
||||
$attachment = $this->getAttachment($fileId);
|
||||
$contents = $this->fileStorageManager->getContents($attachment);
|
||||
|
||||
$message = new MessageWrapper(1, null, $this->parser, $contents);
|
||||
|
||||
$this->checkDuplicate($message);
|
||||
|
||||
$email = $this->importer->import($message, Data::create());
|
||||
|
||||
if (!$email) {
|
||||
throw new Error("Could not import.");
|
||||
}
|
||||
|
||||
if ($userId) {
|
||||
$this->entityManager->getRDBRepositoryByClass(Email::class)
|
||||
->getRelation($email, 'users')
|
||||
->relateById($userId);
|
||||
}
|
||||
|
||||
$this->entityManager->removeEntity($attachment);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getAttachment(string $fileId): Attachment
|
||||
{
|
||||
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getById($fileId);
|
||||
|
||||
if (!$attachment) {
|
||||
throw new NotFound("Attachment not found.");
|
||||
}
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Conflict
|
||||
*/
|
||||
private function checkDuplicate(MessageWrapper $message): void
|
||||
{
|
||||
$messageId = $this->parser->getMessageId($message);
|
||||
|
||||
if (!$messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
|
||||
$email->setMessageId($messageId);
|
||||
|
||||
$duplicate = $this->duplicateFinder->find($email, $message);
|
||||
|
||||
if (!$duplicate) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw Conflict::createWithBody(
|
||||
'Email is already imported.',
|
||||
Error\Body::create()->withMessageTranslation('alreadyImported', Email::ENTITY_TYPE, [
|
||||
'id' => $duplicate->getId(),
|
||||
'link' => '#Email/view/' . $duplicate->getId(),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
719
application/Espo/Tools/Email/InboxService.php
Normal file
719
application/Espo/Tools/Email/InboxService.php
Normal file
@@ -0,0 +1,719 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error\Body;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Select\SelectBuilderFactory;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\WebSocket\Submission as WebSocketSubmission;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailFolder;
|
||||
use Espo\Entities\GroupEmailFolder;
|
||||
use Espo\Entities\Notification;
|
||||
use Espo\Entities\Team;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\ORM\Query\Part\Condition;
|
||||
use Espo\ORM\Query\Part\Expression;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
class InboxService
|
||||
{
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private EntityManager $entityManager,
|
||||
private AclManager $aclManager,
|
||||
private Log $log,
|
||||
private SelectBuilderFactory $selectBuilderFactory,
|
||||
private WebSocketSubmission $webSocketSubmission,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string[] $idList
|
||||
*/
|
||||
public function moveToFolderIdList(array $idList, ?string $folderId, ?string $userId = null): void
|
||||
{
|
||||
foreach ($idList as $id) {
|
||||
try {
|
||||
$this->moveToFolder($id, $folderId, $userId);
|
||||
} catch (Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function moveToFolder(string $id, ?string $folderId, ?string $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? $this->user->getId();
|
||||
|
||||
if ($folderId === Folder::INBOX) {
|
||||
$folderId = null;
|
||||
}
|
||||
|
||||
$email = $this->getEmail($id);
|
||||
$user = $this->getUser($userId);
|
||||
|
||||
if ($email->getGroupFolder()) {
|
||||
$this->checkCurrentGroupFolder($email->getGroupFolder()->getId(), $user);
|
||||
}
|
||||
|
||||
if ($folderId && str_starts_with($folderId, 'group:')) {
|
||||
try {
|
||||
$this->moveToGroupFolder($email, substr($folderId, 6), $user);
|
||||
} catch (Exception $e) {
|
||||
$this->log->debug("Could not move email to group folder. {message}", ['message' => $e->getMessage()]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($folderId === Folder::ARCHIVE) {
|
||||
$this->moveToArchive($email, $user);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($email->getGroupFolder()) {
|
||||
if (!$this->aclManager->checkEntityEdit($user, $email)) {
|
||||
throw Forbidden::createWithBody(
|
||||
"Cannot move out from group folder. No edit access to email.",
|
||||
Body::create()->withMessageTranslation('groupMoveOutNoEditAccess', 'Email')
|
||||
);
|
||||
}
|
||||
|
||||
$email
|
||||
->setGroupFolder(null)
|
||||
->setGroupStatusFolder(null);
|
||||
|
||||
$this->entityManager->saveEntity($email);
|
||||
}
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set([
|
||||
'folderId' => $folderId,
|
||||
'inTrash' => false,
|
||||
'inArchive' => false,
|
||||
])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'emailId' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function moveToGroupFolder(Email $email, string $folderId, User $user): void
|
||||
{
|
||||
$folder = $this->getGroupFolder($folderId);
|
||||
|
||||
if (!$this->aclManager->checkEntityRead($user, $folder)) {
|
||||
throw new Forbidden("Cannot move to group folder. No access to folder.");
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkEntityEdit($user, $email)) {
|
||||
throw Forbidden::createWithBody(
|
||||
"Cannot move to group folder. No edit access to email.",
|
||||
Body::create()->withMessageTranslation('groupMoveToNoEditAccess', 'Email')
|
||||
);
|
||||
}
|
||||
|
||||
$email
|
||||
->setGroupFolder($folder)
|
||||
->setGroupStatusFolder(null);
|
||||
|
||||
$this->applyGroupFolder($email, $folder);
|
||||
|
||||
$this->entityManager->saveEntity($email);
|
||||
|
||||
$this->retrieveFromArchive($email, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $idList
|
||||
*/
|
||||
public function moveToTrashIdList(array $idList, ?string $userId = null): bool
|
||||
{
|
||||
foreach ($idList as $id) {
|
||||
try {
|
||||
$this->moveToTrash($id, $userId);
|
||||
} catch (Exception) {}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $idList
|
||||
*/
|
||||
public function retrieveFromTrashIdList(array $idList, ?string $userId = null): void
|
||||
{
|
||||
foreach ($idList as $id) {
|
||||
try {
|
||||
$this->retrieveFromTrash($id, $userId);
|
||||
} catch (Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function moveToTrash(string $id, ?string $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? $this->user->getId();
|
||||
|
||||
$email = $this->getEmail($id);
|
||||
$user = $this->getUser($userId);
|
||||
|
||||
if ($email->getGroupFolder()) {
|
||||
$folder = $this->getGroupFolder($email->getGroupFolder()->getId());
|
||||
|
||||
if (!$this->aclManager->checkEntityRead($user, $folder)) {
|
||||
throw Forbidden::createWithBody(
|
||||
"Cannot move email from group folder to trash. No access to group folder.",
|
||||
Body::create()->withMessageTranslation('groupFolderNoAccess', 'Email')
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkEntityEdit($user, $email)) {
|
||||
throw Forbidden::createWithBody(
|
||||
"Cannot move email from group folder to trash.",
|
||||
Body::create()->withMessageTranslation('groupMoveToTrashNoEditAccess', 'Email')
|
||||
);
|
||||
}
|
||||
|
||||
$email->setGroupStatusFolder(Email::GROUP_STATUS_FOLDER_TRASH);
|
||||
$this->entityManager->saveEntity($email);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set(['inTrash' => true])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'emailId' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
|
||||
$this->markNotificationAsRead($id, $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function retrieveFromTrash(string $id, ?string $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? $this->user->getId();
|
||||
|
||||
$email = $this->getEmail($id);
|
||||
$user = $this->getUser($userId);
|
||||
|
||||
if ($email->getGroupFolder()) {
|
||||
$folder = $this->getGroupFolder($email->getGroupFolder()->getId());
|
||||
|
||||
if (!$this->aclManager->checkEntityEdit($user, $email)) {
|
||||
throw Forbidden::createWithBody(
|
||||
"Cannot retrieve group folder email from trash. No edit to email.",
|
||||
Body::create()->withMessageTranslation('notEditAccess', 'Email')
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->aclManager->checkEntityRead($user, $folder)) {
|
||||
throw Forbidden::createWithBody(
|
||||
"Cannot retrieve group folder email from trash. No access to group folder.",
|
||||
Body::create()->withMessageTranslation('groupFolderNoAccess', 'Email')
|
||||
);
|
||||
}
|
||||
|
||||
$email->setGroupStatusFolder(null);
|
||||
|
||||
$this->entityManager->saveEntity($email);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set(['inTrash' => false])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'emailId' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $idList
|
||||
*/
|
||||
public function markAsReadIdList(array $idList, ?string $userId = null): void
|
||||
{
|
||||
foreach ($idList as $id) {
|
||||
$this->markAsRead($id, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $idList
|
||||
*/
|
||||
public function markAsNotReadIdList(array $idList, ?string $userId = null): void
|
||||
{
|
||||
foreach ($idList as $id) {
|
||||
$this->markAsNotRead($id, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
public function markAsRead(string $id, ?string $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? $this->user->getId();
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set(['isRead' => true])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'emailId' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
|
||||
$this->markNotificationAsRead($id, $userId);
|
||||
}
|
||||
|
||||
public function markAsNotRead(string $id, ?string $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? $this->user->getId();
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set(['isRead' => false])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'emailId' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $idList
|
||||
*/
|
||||
public function markAsImportantIdList(array $idList, ?string $userId = null): void
|
||||
{
|
||||
foreach ($idList as $id) {
|
||||
$this->markAsImportant($id, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $idList
|
||||
*/
|
||||
public function markAsNotImportantIdList(array $idList, ?string $userId = null): void
|
||||
{
|
||||
foreach ($idList as $id) {
|
||||
$this->markAsNotImportant($id, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
public function markAsImportant(string $id, ?string $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? $this->user->getId();
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set(['isImportant' => true])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'emailId' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
public function markAsNotImportant(string $id, ?string $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? $this->user->getId();
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set(['isImportant' => false])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'emailId' => $id,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
public function markAllAsRead(?string $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? $this->user->getId();
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set(['isRead' => true])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'isRead' => false,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
|
||||
$update = $this
|
||||
->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Notification::ENTITY_TYPE)
|
||||
->set(['read' => true])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $userId,
|
||||
'relatedType' => Email::ENTITY_TYPE,
|
||||
'read' => false,
|
||||
'type' => Notification::TYPE_EMAIL_RECEIVED,
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
|
||||
$this->submitNotificationWebSocket($userId);
|
||||
}
|
||||
|
||||
public function markNotificationAsRead(string $id, string $userId): void
|
||||
{
|
||||
$notification = $this->entityManager
|
||||
->getRDBRepositoryByClass(Notification::class)
|
||||
->where([
|
||||
'userId' => $userId,
|
||||
'relatedType' => Email::ENTITY_TYPE,
|
||||
'relatedId' => $id,
|
||||
'read' => false,
|
||||
'type' => Notification::TYPE_EMAIL_RECEIVED,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notification->setRead();
|
||||
$this->entityManager->saveEntity($notification);
|
||||
|
||||
$this->submitNotificationWebSocket($userId);
|
||||
}
|
||||
|
||||
private function submitNotificationWebSocket(string $userId): void
|
||||
{
|
||||
$this->webSocketSubmission->submit('newNotification', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
public function getFoldersNotReadCounts(): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
$selectBuilder = $this->selectBuilderFactory
|
||||
->create()
|
||||
->from(Email::ENTITY_TYPE)
|
||||
->withAccessControlFilter();
|
||||
|
||||
$draftsSelectBuilder = clone $selectBuilder;
|
||||
|
||||
$selectBuilder->withWhere(
|
||||
WhereItem::fromRaw([
|
||||
'type' => 'isTrue',
|
||||
'attribute' => 'isNotRead',
|
||||
])
|
||||
);
|
||||
|
||||
$folderIdList = [Folder::INBOX, Folder::DRAFTS];
|
||||
|
||||
$emailFolderList = $this->entityManager
|
||||
->getRDBRepository(EmailFolder::ENTITY_TYPE)
|
||||
->where([
|
||||
'assignedUserId' => $this->user->getId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($emailFolderList as $folder) {
|
||||
$folderIdList[] = $folder->getId();
|
||||
}
|
||||
|
||||
$groupFolderList = $this->entityManager
|
||||
->getRDBRepositoryByClass(GroupEmailFolder::class)
|
||||
->distinct()
|
||||
->leftJoin(Field::TEAMS)
|
||||
->where(
|
||||
$this->user->isAdmin() ?
|
||||
['id!=' => null] :
|
||||
['teams.id' => $this->user->getTeamIdList()]
|
||||
)
|
||||
->find();
|
||||
|
||||
foreach ($groupFolderList as $folder) {
|
||||
$folderIdList[] = 'group:' . $folder->getId();
|
||||
}
|
||||
|
||||
foreach ($folderIdList as $folderId) {
|
||||
$itemSelectBuilder = clone $selectBuilder;
|
||||
|
||||
if ($folderId === Folder::DRAFTS) {
|
||||
$itemSelectBuilder = clone $draftsSelectBuilder;
|
||||
}
|
||||
|
||||
$itemSelectBuilder->withWhere(
|
||||
WhereItem::fromRaw([
|
||||
'type' => 'inFolder',
|
||||
'attribute' => 'folderId',
|
||||
'value' => $folderId,
|
||||
])
|
||||
);
|
||||
|
||||
try {
|
||||
$data[$folderId] = $this->entityManager
|
||||
->getRDBRepository(Email::ENTITY_TYPE)
|
||||
->clone($itemSelectBuilder->build())
|
||||
->count();
|
||||
} catch (BadRequest|Forbidden $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function moveToArchive(Email $email, User $user): void
|
||||
{
|
||||
if (!$this->aclManager->checkEntityRead($user, $email)) {
|
||||
throw new Forbidden("No read access to email.");
|
||||
}
|
||||
|
||||
if ($email->getGroupFolder()) {
|
||||
if (!$this->aclManager->checkEntityEdit($user, $email)) {
|
||||
throw Forbidden::createWithBody(
|
||||
"Cannot move from group folder to Archive. No edit access to email.",
|
||||
Body::create()->withMessageTranslation('groupMoveToArchiveNoEditAccess', 'Email')
|
||||
);
|
||||
}
|
||||
|
||||
$email->setGroupStatusFolder(Email::GROUP_STATUS_FOLDER_ARCHIVE);
|
||||
$this->entityManager->saveEntity($email);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set([
|
||||
'folderId' => null,
|
||||
'inArchive' => true,
|
||||
'inTrash' => false,
|
||||
])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $user->getId(),
|
||||
'emailId' => $email->getId(),
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
public function retrieveFromArchive(Email $email, User $user): void
|
||||
{
|
||||
$update = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->update()
|
||||
->in(Email::RELATIONSHIP_EMAIL_USER)
|
||||
->set([
|
||||
'folderId' => null,
|
||||
'inArchive' => false,
|
||||
])
|
||||
->where([
|
||||
Attribute::DELETED => false,
|
||||
'userId' => $user->getId(),
|
||||
'emailId' => $email->getId(),
|
||||
])
|
||||
->build();
|
||||
|
||||
$this->entityManager->getQueryExecutor()->execute($update);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function checkCurrentGroupFolder(string $folderId, User $user): void
|
||||
{
|
||||
$folder = $this->entityManager->getEntityById(GroupEmailFolder::ENTITY_TYPE, $folderId);
|
||||
|
||||
if ($folder && !$this->aclManager->checkEntityRead($user, $folder)) {
|
||||
throw new Forbidden("No access to current group folder.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getUser(string $userId): User
|
||||
{
|
||||
$user = $userId === $this->user->getId() ?
|
||||
$this->user :
|
||||
$this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound("User not found.");
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getEmail(string $id): Email
|
||||
{
|
||||
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getById($id);
|
||||
|
||||
if (!$email) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function getGroupFolder(string $folderId): GroupEmailFolder
|
||||
{
|
||||
$folder = $this->entityManager->getRDBRepositoryByClass(GroupEmailFolder::class)->getById($folderId);
|
||||
|
||||
if (!$folder) {
|
||||
throw new NotFound("Group folder not found.");
|
||||
}
|
||||
|
||||
return $folder;
|
||||
}
|
||||
|
||||
private function applyGroupFolder(Email $email, GroupEmailFolder $folder): void
|
||||
{
|
||||
if (!$folder->getTeams()->getCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($folder->getTeams()->getIdList() as $teamId) {
|
||||
$email->addTeamId($teamId);
|
||||
}
|
||||
|
||||
$users = $this->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
->select([Attribute::ID])
|
||||
->where([
|
||||
'type' => [User::TYPE_REGULAR, User::TYPE_ADMIN],
|
||||
'isActive' => true,
|
||||
])
|
||||
->where(
|
||||
Condition::in(
|
||||
Expression::column(Attribute::ID),
|
||||
SelectBuilder::create()
|
||||
->from(Team::RELATIONSHIP_TEAM_USER)
|
||||
->select('userId')
|
||||
->where(['teamId' => $folder->getTeams()->getIdList()])
|
||||
->build()
|
||||
)
|
||||
)
|
||||
->find();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$email->addUserId($user->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
604
application/Espo/Tools/Email/SendService.php
Normal file
604
application/Espo/Tools/Email/SendService.php
Normal file
@@ -0,0 +1,604 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\FieldValidation\FieldValidationManager;
|
||||
use Espo\Core\Mail\Account\Account;
|
||||
use Espo\Core\Mail\Account\GroupAccount\Account as GroupAccount;
|
||||
use Espo\Core\Mail\Account\GroupAccount\AccountFactory as GroupAccountFactory;
|
||||
use Espo\Core\Mail\Account\GroupAccount\Service as GroupAccountService;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\Account as PersonalAccount;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\AccountFactory as PersonalAccountFactory;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\Service as PersonalAccountService;
|
||||
use Espo\Core\Mail\Account\SendingAccountProvider;
|
||||
use Espo\Core\Mail\ConfigDataProvider;
|
||||
use Espo\Core\Mail\EmailSender;
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Core\Mail\Sender;
|
||||
use Espo\Core\Mail\SenderParams;
|
||||
use Espo\Core\Mail\Smtp\HandlerProcessor;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Core\Name\Link;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Json;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailAccount;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\CaseObj;
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Stream\Service as StreamService;
|
||||
use Exception;
|
||||
use LogicException;
|
||||
|
||||
use const FILTER_VALIDATE_EMAIL;
|
||||
|
||||
/**
|
||||
* Email sending service.
|
||||
*/
|
||||
class SendService
|
||||
{
|
||||
private const LINK_EMAIL_ADDRESSES = Link::EMAIL_ADDRESSES;
|
||||
|
||||
/** @var string[] */
|
||||
private array $notAllowedStatusList = [
|
||||
Email::STATUS_ARCHIVED,
|
||||
Email::STATUS_SENT,
|
||||
Email::STATUS_BEING_IMPORTED,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private EntityManager $entityManager,
|
||||
private FieldValidationManager $fieldValidationManager,
|
||||
private EmailSender $emailSender,
|
||||
private StreamService $streamService,
|
||||
private Config $config,
|
||||
private Log $log,
|
||||
private Acl $acl,
|
||||
private SendingAccountProvider $accountProvider,
|
||||
private PersonalAccountService $personalAccountService,
|
||||
private GroupAccountService $groupAccountService,
|
||||
private HandlerProcessor $handlerProcessor,
|
||||
private PersonalAccountFactory $personalAccountFactory,
|
||||
private GroupAccountFactory $groupAccountFactory,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Send an email entity.
|
||||
*
|
||||
* @params Email $entity An email entity.
|
||||
* @params ?User $user A user from what to send.
|
||||
*
|
||||
* @throws BadRequest If not valid.
|
||||
* @throws SendingError On error while sending.
|
||||
* @throws NoSmtp No SMTP settings.
|
||||
* @throws Error An error.
|
||||
*/
|
||||
public function send(Email $entity, ?User $user = null): void
|
||||
{
|
||||
if (in_array($entity->getStatus(), $this->notAllowedStatusList)) {
|
||||
throw new Error("Can't send email with status `{$entity->getStatus()}`.");
|
||||
}
|
||||
|
||||
if (!$this->fieldValidationManager->check($entity, 'to', 'required')) {
|
||||
$entity->setStatus(Email::STATUS_DRAFT);
|
||||
|
||||
$this->entityManager->saveEntity($entity, [SaveOption::SILENT => true]);
|
||||
|
||||
throw new BadRequest("Empty To address.");
|
||||
}
|
||||
|
||||
$systemIsShared = $this->configDataProvider->isSystemOutboundAddressShared();
|
||||
$systemFromName = $this->config->get('outboundEmailFromName');
|
||||
$systemFromAddress = $this->configDataProvider->getSystemOutboundAddress();
|
||||
|
||||
$sender = $this->emailSender->create();
|
||||
|
||||
$userAddressList = [];
|
||||
|
||||
if ($user) {
|
||||
// @todo Use getEmailAddressGroup.
|
||||
/** @var Collection<EmailAddress> $emailAddressCollection */
|
||||
$emailAddressCollection = $this->entityManager
|
||||
->getRelation($user, self::LINK_EMAIL_ADDRESSES)
|
||||
->find();
|
||||
|
||||
foreach ($emailAddressCollection as $ea) {
|
||||
$userAddressList[] = $ea->getLower();
|
||||
}
|
||||
}
|
||||
|
||||
$originalFromAddress = $entity->getFromAddress();
|
||||
|
||||
if (!$originalFromAddress) {
|
||||
throw new Error("Email sending: Can't send with empty 'from' address.");
|
||||
}
|
||||
|
||||
$fromAddress = strtolower($originalFromAddress);
|
||||
|
||||
$isUserAddress = in_array($fromAddress, $userAddressList);
|
||||
$isSystemAddress = $fromAddress === strtolower($systemFromAddress ?? '');
|
||||
|
||||
$smtpParams = null;
|
||||
$personalAccount = null;
|
||||
$groupAccount = null;
|
||||
$params = SenderParams::create();
|
||||
|
||||
if ($user && $isUserAddress) {
|
||||
[$smtpParams, $personalAccount] = $this->getPersonalAccount($user, $originalFromAddress);
|
||||
}
|
||||
|
||||
if ($user && $smtpParams) {
|
||||
$sender->withSmtpParams($smtpParams);
|
||||
}
|
||||
|
||||
if (!$smtpParams) {
|
||||
[$smtpParams, $groupAccount] = $this->getGroupAccount($user, $originalFromAddress);
|
||||
|
||||
if ($smtpParams) {
|
||||
$sender->withSmtpParams($smtpParams);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$smtpParams && $isSystemAddress) {
|
||||
$params = $params->withFromName($systemFromName);
|
||||
}
|
||||
|
||||
// Otherwise, allow users to send from the system SMTP with their own from-address.
|
||||
if (!$smtpParams && !$systemIsShared) {
|
||||
if ($isSystemAddress) {
|
||||
throw new NoSmtp("Can not use system SMTP. System SMTP is not shared.");
|
||||
}
|
||||
|
||||
throw new NoSmtp("No SMTP params for $fromAddress.");
|
||||
}
|
||||
|
||||
if (
|
||||
!$smtpParams &&
|
||||
$user &&
|
||||
in_array($fromAddress, $userAddressList)
|
||||
) {
|
||||
$params = $params->withFromName($user->getName());
|
||||
}
|
||||
|
||||
$parent = null;
|
||||
$parentId = $entity->getParentId();
|
||||
$parentType = $entity->getParentType();
|
||||
|
||||
if ($parentType && $parentId) {
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
$params = $this->applyParent($parent, $params);
|
||||
}
|
||||
|
||||
$this->validateEmailAddresses($entity);
|
||||
|
||||
$messageContainer = new Sender\MessageContainer();
|
||||
|
||||
if (
|
||||
$groupAccount instanceof GroupAccount && $groupAccount->storeSentEmails() ||
|
||||
$personalAccount instanceof PersonalAccount && $personalAccount->storeSentEmails()
|
||||
) {
|
||||
$sender->withMessageContainer($messageContainer);
|
||||
}
|
||||
|
||||
$this->applyReplied($entity, $sender);
|
||||
|
||||
$sender->withParams($params);
|
||||
|
||||
try {
|
||||
$sender->send($entity);
|
||||
} catch (Exception $e) {
|
||||
$entity->setStatus(Email::STATUS_DRAFT);
|
||||
|
||||
$this->entityManager->saveEntity($entity, [SaveOption::SILENT => true]);
|
||||
|
||||
$this->log->error("Email sending: " . $e->getMessage(), ['exception' => $e]);
|
||||
|
||||
$errorData = [
|
||||
'id' => $entity->getId(),
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
|
||||
throw ErrorSilent::createWithBody('sendingFail', Json::encode($errorData));
|
||||
}
|
||||
|
||||
if ($groupAccount) {
|
||||
$groupAccountId = $groupAccount->getId();
|
||||
|
||||
if ($groupAccountId) {
|
||||
$entity->addLinkMultipleId('inboundEmails', $groupAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
if ($personalAccount) {
|
||||
$personalAccountId = $personalAccount->getId();
|
||||
|
||||
if ($personalAccountId) {
|
||||
$entity->addLinkMultipleId('emailAccounts', $personalAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($entity, [Email::SAVE_OPTION_IS_JUST_SENT => true]);
|
||||
|
||||
$this->store($messageContainer, $groupAccount, $personalAccount);
|
||||
|
||||
if ($parent) {
|
||||
$this->streamService->noteEmailSent($parent, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
private function applyParent(?Entity $parent, SenderParams $params): SenderParams
|
||||
{
|
||||
// @todo Refactor. Move to a separate class? Make extensible?
|
||||
if ($parent instanceof CaseObj) {
|
||||
$inboundEmailId = $parent->getInboundEmailId();
|
||||
|
||||
if (!$inboundEmailId) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$inboundEmail = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmail::class)
|
||||
->getById($inboundEmailId);
|
||||
|
||||
if (!$inboundEmail || !$inboundEmail->getReplyToAddress()) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$params = $params->withReplyToAddress($inboundEmail->getReplyToAddress());
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function store(
|
||||
Sender\MessageContainer $messageContainer,
|
||||
?Account $groupAccount,
|
||||
?Account $personalAccount,
|
||||
): void {
|
||||
|
||||
$message = $messageContainer->message;
|
||||
|
||||
if (!$message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($groupAccount instanceof GroupAccount && $groupAccount->storeSentEmails()) {
|
||||
$id = $groupAccount->getId() ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->groupAccountService->storeSentMessage($id, $message);
|
||||
} catch (Exception $e) {
|
||||
$text = "Could not store sent email; group account {$groupAccount->getId()}); " .
|
||||
$e->getMessage() . ".";
|
||||
|
||||
$this->log->error($text, ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($personalAccount instanceof PersonalAccount && $personalAccount->storeSentEmails()) {
|
||||
$id = $personalAccount->getId() ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->personalAccountService->storeSentMessage($id, $message);
|
||||
} catch (Exception $e) {
|
||||
$text = "Could not store sent email; personal account {$personalAccount->getId()}; " .
|
||||
$e->getMessage() . ".";
|
||||
|
||||
$this->log->error($text, ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?SmtpParams, ?Account}
|
||||
*/
|
||||
private function getPersonalAccount(User $user, string $emailAddress): array
|
||||
{
|
||||
$personalAccount = $this->accountProvider->getPersonal($user, $emailAddress);
|
||||
|
||||
if (!$personalAccount) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
if (!$personalAccount->isAvailableForSending()) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$smtpParams = $personalAccount->getSmtpParams();
|
||||
|
||||
if (!$smtpParams) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$smtpParams = $smtpParams->withFromName($user->getName());
|
||||
|
||||
return [$smtpParams, $personalAccount];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?SmtpParams, ?Account}
|
||||
*/
|
||||
private function getGroupAccount(?User $user, string $emailAddress): array
|
||||
{
|
||||
$groupAccount = $user ?
|
||||
$this->accountProvider->getShared($user, $emailAddress) :
|
||||
$this->accountProvider->getGroup($emailAddress);
|
||||
|
||||
if (!$groupAccount) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
$smtpParams = $groupAccount->getSmtpParams();
|
||||
|
||||
if (!$smtpParams) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [$smtpParams, $groupAccount];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
public function sendTestEmail(SmtpParams $params, TestSendData $data): void
|
||||
{
|
||||
$emailAddress = $data->getEmailAddress();
|
||||
$userId = $data->getUserId();
|
||||
$type = $data->getType();
|
||||
$id = $data->getId();
|
||||
|
||||
if ($params->getPassword() === null) {
|
||||
$params = $params->withPassword(
|
||||
$this->obtainSendTestEmailPassword($type, $id)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
$userId &&
|
||||
$userId !== $this->user->getId() &&
|
||||
!$this->user->isAdmin()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $userId ?
|
||||
$this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId) :
|
||||
null;
|
||||
|
||||
if ($userId && !$user) {
|
||||
throw new NotFound("User not found.");
|
||||
}
|
||||
|
||||
/** @var Email $email */
|
||||
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
|
||||
|
||||
$email
|
||||
->setSubject('EspoCRM: Test Email')
|
||||
->setIsHtml(false)
|
||||
->addToAddress($emailAddress);
|
||||
|
||||
$handlerClassName = null;
|
||||
|
||||
if ($type === 'emailAccount' && $id) {
|
||||
/** @var ?EmailAccount $emailAccount */
|
||||
$emailAccount = $this->entityManager->getEntityById(EmailAccount::ENTITY_TYPE, $id);
|
||||
|
||||
$handlerClassName = $emailAccount?->getSmtpHandlerClassName();
|
||||
}
|
||||
|
||||
if ($type === 'inboundEmail' && $id) {
|
||||
/** @var ?InboundEmail $inboundEmail */
|
||||
$inboundEmail = $this->entityManager->getEntityById(InboundEmail::ENTITY_TYPE, $id);
|
||||
|
||||
if ($inboundEmail) {
|
||||
$handlerClassName = $inboundEmail->getSmtpHandlerClassName();
|
||||
}
|
||||
}
|
||||
|
||||
if ($handlerClassName && $id) {
|
||||
$params = $this->handlerProcessor->handle($handlerClassName, $params, $id);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->emailSender
|
||||
->withSmtpParams($params)
|
||||
->send($email);
|
||||
} catch (Exception $e) {
|
||||
$this->log->warning("Email sending:" . $e->getMessage() . "; " . $e->getCode());
|
||||
|
||||
if ($e instanceof SendingError) {
|
||||
throw ErrorSilent::createWithBody(
|
||||
'sendingFail',
|
||||
Error\Body::create()
|
||||
->withMessageTranslation($e->getMessage(), Email::ENTITY_TYPE)
|
||||
);
|
||||
}
|
||||
|
||||
$errorData = ['message' => $e->getMessage()];
|
||||
|
||||
throw ErrorSilent::createWithBody('sendingFail', Json::encode($errorData));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function validateEmailAddresses(Email $entity): void
|
||||
{
|
||||
$from = $entity->getFromAddress();
|
||||
|
||||
if ($from) {
|
||||
if (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new Error('From email address is not valid.');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entity->getToAddressList() as $address) {
|
||||
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new Error('To email address is not valid.');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entity->getCcAddressList() as $address) {
|
||||
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new Error('CC email address is not valid.');
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entity->getBccAddressList() as $address) {
|
||||
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new Error('BCC email address is not valid.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user personal SMTP params.
|
||||
*/
|
||||
public function getUserSmtpParams(string $userId): ?SmtpParams
|
||||
{
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$address = $user->getEmailAddress();
|
||||
|
||||
if (!$address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$account = $this->accountProvider->getPersonal($user, $address);
|
||||
|
||||
if (!$account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$smtpParams = $account->getSmtpParams();
|
||||
|
||||
return $smtpParams
|
||||
?->withFromName($user->getName())
|
||||
->withFromAddress($address);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
private function obtainSendTestEmailPassword(?string $type, ?string $id): ?string
|
||||
{
|
||||
if ($type === 'emailAccount') {
|
||||
if (!$this->acl->checkScope(EmailAccount::ENTITY_TYPE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$personalAccount = $this->personalAccountFactory->create($id);
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
$personalAccount->getUser()->getId() !== $this->user->getId()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$smtpParams = $personalAccount->getSmtpParams();
|
||||
|
||||
return $smtpParams?->getPassword();
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($type === 'inboundEmail') {
|
||||
if (!$id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$smtpParams = $this->groupAccountFactory
|
||||
->create($id)
|
||||
->getSmtpParams();
|
||||
|
||||
return $smtpParams?->getPassword();
|
||||
}
|
||||
|
||||
return $this->config->get('smtpPassword');
|
||||
}
|
||||
|
||||
private function applyReplied(Email $entity, Sender $sender): void
|
||||
{
|
||||
$replied = $entity->getReplied();
|
||||
|
||||
if ($replied && $replied->getMessageId()) {
|
||||
$sender->withAddedHeader('In-Reply-To', $replied->getMessageId());
|
||||
$sender->withAddedHeader('References', $replied->getMessageId());
|
||||
}
|
||||
|
||||
if ($replied && $replied->getGroupFolder()) {
|
||||
$entity->setGroupFolder($replied->getGroupFolder());
|
||||
}
|
||||
}
|
||||
}
|
||||
119
application/Espo/Tools/Email/Service.php
Normal file
119
application/Espo/Tools/Email/Service.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\Attachment as AttachmentRepository;
|
||||
use Espo\Tools\Attachment\AccessChecker as AttachmentAccessChecker;
|
||||
use Espo\Tools\Attachment\FieldData;
|
||||
|
||||
class Service
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private AttachmentAccessChecker $attachmentAccessChecker;
|
||||
private ServiceContainer $serviceContainer;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
AttachmentAccessChecker $attachmentAccessChecker,
|
||||
ServiceContainer $serviceContainer
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->attachmentAccessChecker = $attachmentAccessChecker;
|
||||
$this->serviceContainer = $serviceContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy email attachments for re-using (e.g. in a forward email).
|
||||
*
|
||||
* @return Attachment[]
|
||||
* @throws NotFound
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function copyAttachments(string $id, FieldData $fieldData): array
|
||||
{
|
||||
/** @var ?Email $entity */
|
||||
$entity = $this->serviceContainer
|
||||
->get(Email::ENTITY_TYPE)
|
||||
->getEntity($id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$this->attachmentAccessChecker->check($fieldData);
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($entity->getAttachmentIdList() as $attachmentId) {
|
||||
$attachment = $this->copyAttachment($attachmentId, $fieldData);
|
||||
|
||||
if ($attachment) {
|
||||
$list[] = $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function copyAttachment(string $attachmentId, FieldData $fieldData): ?Attachment
|
||||
{
|
||||
/** @var ?Attachment $attachment */
|
||||
$attachment = $this->entityManager
|
||||
->getRDBRepositoryByClass(Attachment::class)
|
||||
->getById($attachmentId);
|
||||
|
||||
if (!$attachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$copied = $this->getAttachmentRepository()->getCopiedAttachment($attachment);
|
||||
|
||||
$copied->set('parentType', $fieldData->getParentType());
|
||||
$copied->set('relatedType', $fieldData->getRelatedType());
|
||||
$copied->setTargetField($fieldData->getField());
|
||||
$copied->setRole(Attachment::ROLE_ATTACHMENT);
|
||||
|
||||
$this->getAttachmentRepository()->save($copied);
|
||||
|
||||
return $copied;
|
||||
}
|
||||
|
||||
private function getAttachmentRepository(): AttachmentRepository
|
||||
{
|
||||
/** @var AttachmentRepository */
|
||||
return $this->entityManager->getRepositoryByClass(Attachment::class);
|
||||
}
|
||||
}
|
||||
71
application/Espo/Tools/Email/TestSendData.php
Normal file
71
application/Espo/Tools/Email/TestSendData.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
class TestSendData
|
||||
{
|
||||
private string $emailAddress;
|
||||
private ?string $type;
|
||||
private ?string $id;
|
||||
private ?string $userId;
|
||||
|
||||
public function __construct(
|
||||
string $emailAddress,
|
||||
?string $type,
|
||||
?string $id,
|
||||
?string $userId
|
||||
) {
|
||||
$this->emailAddress = $emailAddress;
|
||||
$this->type = $type;
|
||||
$this->id = $id;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function getEmailAddress(): string
|
||||
{
|
||||
return $this->emailAddress;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUserId(): ?string
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
}
|
||||
168
application/Espo/Tools/Email/Util.php
Normal file
168
application/Espo/Tools/Email/Util.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\Email;
|
||||
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
class Util
|
||||
{
|
||||
static public function parseFromName(string $string): string
|
||||
{
|
||||
$fromName = '';
|
||||
|
||||
if ($string && stripos($string, '<') !== false) {
|
||||
/** @var string $replacedString */
|
||||
$replacedString = preg_replace('/(<.*>)/', '', $string);
|
||||
|
||||
$fromName = trim($replacedString, '" ');
|
||||
}
|
||||
|
||||
return $fromName;
|
||||
}
|
||||
|
||||
static public function parseFromAddress(string $string): string
|
||||
{
|
||||
if (!$string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (stripos($string, '<') !== false) {
|
||||
$fromAddress = '';
|
||||
|
||||
if (preg_match('/<(.*)>/', $string, $matches)) {
|
||||
$fromAddress = trim($matches[1]);
|
||||
}
|
||||
|
||||
return $fromAddress;
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*/
|
||||
static public function stripHtml(string $string): string
|
||||
{
|
||||
if (!$string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$converter = new HtmlConverter();
|
||||
$converter->setOptions([
|
||||
'remove_nodes' => 'img',
|
||||
'strip_tags' => true,
|
||||
]);
|
||||
|
||||
$string = $converter->convert($string) ?: '';
|
||||
|
||||
$string = (string) preg_replace('~\R~u', "\r\n", $string);
|
||||
|
||||
$reList = [
|
||||
'&(quot|#34);',
|
||||
'&(amp|#38);',
|
||||
'&(lt|#60);',
|
||||
'&(gt|#62);',
|
||||
'&(nbsp|#160);',
|
||||
'&(iexcl|#161);',
|
||||
'&(cent|#162);',
|
||||
'&(pound|#163);',
|
||||
'&(copy|#169);',
|
||||
'&(reg|#174);',
|
||||
];
|
||||
|
||||
$replaceList = [
|
||||
'',
|
||||
'&',
|
||||
'<',
|
||||
'>',
|
||||
' ',
|
||||
'¡',
|
||||
'¢',
|
||||
'£',
|
||||
'©',
|
||||
'®',
|
||||
];
|
||||
|
||||
foreach ($reList as $i => $re) {
|
||||
$string = (string) mb_ereg_replace($re, $replaceList[$i], $string, 'i');
|
||||
}
|
||||
|
||||
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a quote part in a plain text.
|
||||
*
|
||||
* @since 9.0.0
|
||||
*/
|
||||
static public function stripPlainTextQuotePart(string $string): string
|
||||
{
|
||||
if (!$string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = preg_split("/\r\n|\n|\r/", $string);
|
||||
|
||||
if (!is_array($lines)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$endIndex = count($lines) - 1;
|
||||
|
||||
for ($i = count($lines) - 1; $i >= 0; $i--) {
|
||||
$line = $lines[$i];
|
||||
|
||||
if (str_starts_with($line, '>') || $line === '') {
|
||||
$endIndex = $i;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$lines = array_slice($lines, 0, $endIndex);
|
||||
|
||||
if (count($lines) > 2) {
|
||||
$lastIndex = count($lines) - 1;
|
||||
|
||||
if (str_ends_with($lines[$lastIndex], ':') && $lines[$lastIndex - 1] === '') {
|
||||
$lines = array_slice($lines, 0, count($lines) - 2);
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\r\n", $lines);
|
||||
}
|
||||
}
|
||||
98
application/Espo/Tools/EmailAddress/Api/GetSearch.php
Normal file
98
application/Espo/Tools/EmailAddress/Api/GetSearch.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailAddress\Api;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Tools\Email\AddressService;
|
||||
|
||||
/**
|
||||
* Searches email addresses.
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class GetSearch implements Action
|
||||
{
|
||||
private const ADDRESS_MAX_SIZE = 50;
|
||||
|
||||
public function __construct(
|
||||
private AddressService $service,
|
||||
private Acl $acl,
|
||||
private Config\ApplicationConfig $applicationConfig,
|
||||
) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
if (!$this->acl->checkScope(Email::ENTITY_TYPE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$entityType = $request->getQueryParam('entityType');
|
||||
$q = $request->getQueryParam('q');
|
||||
$onlyActual = $request->getQueryParam('onlyActual') === 'true';
|
||||
$maxSize = intval($request->getQueryParam('maxSize'));
|
||||
|
||||
if (!$entityType && !$this->acl->checkScope(Email::ENTITY_TYPE, Acl\Table::ACTION_CREATE)) {
|
||||
throw new Forbidden("No 'create' access for Email.");
|
||||
}
|
||||
|
||||
if ($entityType && !$this->acl->checkScope($entityType, Acl\Table::ACTION_READ)) {
|
||||
throw new Forbidden("No 'read' access for entity type.");
|
||||
}
|
||||
|
||||
if (is_string($q)) {
|
||||
$q = trim($q);
|
||||
}
|
||||
|
||||
if (!$q) {
|
||||
throw new BadRequest("No `q` parameter.");
|
||||
}
|
||||
|
||||
if (!$maxSize || $maxSize > self::ADDRESS_MAX_SIZE) {
|
||||
$maxSize = $this->applicationConfig->getRecordsPerPage();
|
||||
}
|
||||
|
||||
if ($entityType) {
|
||||
$result = $this->service->searchInEntityType($entityType, $q, $maxSize);
|
||||
|
||||
return ResponseComposer::json($result);
|
||||
}
|
||||
|
||||
$result = $this->service->searchInAddressBook($q, $maxSize, $onlyActual);
|
||||
|
||||
return ResponseComposer::json($result);
|
||||
}
|
||||
}
|
||||
90
application/Espo/Tools/EmailAddress/EntityLookup.php
Normal file
90
application/Espo/Tools/EmailAddress/EntityLookup.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailAddress;
|
||||
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Entity lookup by an email address.
|
||||
*/
|
||||
class EntityLookup
|
||||
{
|
||||
private EmailAddressRepository $internalRepository;
|
||||
|
||||
public function __construct(
|
||||
private Repository $repository,
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$repository = $entityManager->getRDBRepository(EmailAddress::ENTITY_TYPE);
|
||||
|
||||
if (!$repository instanceof EmailAddressRepository) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$this->internalRepository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find entities by an email address.
|
||||
*
|
||||
* @param string $address An email address.
|
||||
* @return Entity[]
|
||||
*/
|
||||
public function find(string $address): array
|
||||
{
|
||||
$emailAddress = $this->repository->getByAddress($address);
|
||||
|
||||
if (!$emailAddress) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->internalRepository->getEntityListByAddressId($emailAddress->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a first entity by an email address.
|
||||
*
|
||||
* @param string $address An email address.
|
||||
* @param string[] $order An order entity type list.
|
||||
*/
|
||||
public function findOne(string $address, ?array $order = null): ?Entity
|
||||
{
|
||||
if ($order) {
|
||||
$this->internalRepository->getEntityByAddress($address, null, $order);
|
||||
}
|
||||
|
||||
return $this->internalRepository->getEntityByAddress($address);
|
||||
}
|
||||
}
|
||||
62
application/Espo/Tools/EmailAddress/Repository.php
Normal file
62
application/Espo/Tools/EmailAddress/Repository.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailAddress;
|
||||
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* An email address repository.
|
||||
*/
|
||||
class Repository
|
||||
{
|
||||
private EmailAddressRepository $repository;
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
{
|
||||
$repository = $entityManager->getRDBRepository(EmailAddress::ENTITY_TYPE);
|
||||
|
||||
if (!$repository instanceof EmailAddressRepository) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an email address entity by a string address.
|
||||
*/
|
||||
public function getByAddress(string $address): ?EmailAddress
|
||||
{
|
||||
return $this->repository->getByAddress($address);
|
||||
}
|
||||
}
|
||||
136
application/Espo/Tools/EmailFolder/GroupFolderService.php
Normal file
136
application/Espo/Tools/EmailFolder/GroupFolderService.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailFolder;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Entities\GroupEmailFolder;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class GroupFolderService
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private Acl $acl;
|
||||
|
||||
public function __construct(EntityManager $entityManager, Acl $acl)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->acl = $acl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function moveUp(string $id): void
|
||||
{
|
||||
/** @var ?GroupEmailFolder $entity */
|
||||
$entity = $this->entityManager->getEntityById(GroupEmailFolder::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityEdit($entity)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$currentIndex = $entity->getOrder();
|
||||
|
||||
if (!is_int($currentIndex)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$previousEntity = $this->entityManager
|
||||
->getRDBRepositoryByClass(GroupEmailFolder::class)
|
||||
->where([
|
||||
'order<' => $currentIndex,
|
||||
])
|
||||
->order('order', true)
|
||||
->findOne();
|
||||
|
||||
if (!$previousEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set('order', $previousEntity->getOrder());
|
||||
|
||||
$previousEntity->set('order', $currentIndex);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
$this->entityManager->saveEntity($previousEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function moveDown(string $id): void
|
||||
{
|
||||
/** @var ?GroupEmailFolder $entity */
|
||||
$entity = $this->entityManager->getEntityById(GroupEmailFolder::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
if (!$this->acl->checkEntityEdit($entity)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$currentIndex = $entity->getOrder();
|
||||
|
||||
if (!is_int($currentIndex)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$nextEntity = $this->entityManager
|
||||
->getRDBRepositoryByClass(GroupEmailFolder::class)
|
||||
->where([
|
||||
'order>' => $currentIndex,
|
||||
])
|
||||
->order('order', false)
|
||||
->findOne();
|
||||
|
||||
if (!$nextEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set('order', $nextEntity->getOrder());
|
||||
|
||||
$nextEntity->set('order', $currentIndex);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
$this->entityManager->saveEntity($nextEntity);
|
||||
}
|
||||
}
|
||||
250
application/Espo/Tools/EmailFolder/Service.php
Normal file
250
application/Espo/Tools/EmailFolder/Service.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailFolder;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\ForbiddenSilent;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailFolder;
|
||||
use Espo\Entities\GroupEmailFolder;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityCollection;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Email\Folder;
|
||||
|
||||
class Service
|
||||
{
|
||||
/** @var string[] */
|
||||
protected $systemFolderList = [
|
||||
Folder::INBOX,
|
||||
Folder::IMPORTANT,
|
||||
Folder::SENT,
|
||||
];
|
||||
/** @var string[] */
|
||||
protected $systemFolderEndList = [
|
||||
Folder::ARCHIVE,
|
||||
Folder::DRAFTS,
|
||||
Folder::TRASH,
|
||||
];
|
||||
|
||||
private const FOLDER_MAX_COUNT = 100;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Acl $acl,
|
||||
private Config $config,
|
||||
private User $user,
|
||||
private Language $language
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<array<string, mixed>>
|
||||
* @throws ForbiddenSilent
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function listAll(?string $userId = null)
|
||||
{
|
||||
if (
|
||||
$userId &&
|
||||
$userId !== $this->user->getId() &&
|
||||
!$this->user->isAdmin()
|
||||
) {
|
||||
throw new ForbiddenSilent();
|
||||
}
|
||||
|
||||
$userId ??= $this->user->getId();
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $userId === $this->user->getId() ?
|
||||
$this->user :
|
||||
$this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$limit = $this->config->get('emailFolderMaxCount') ?? self::FOLDER_MAX_COUNT;
|
||||
|
||||
$folderList = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailFolder::class)
|
||||
->where(['assignedUserId' => $userId])
|
||||
->order('order')
|
||||
->limit(0, $limit)
|
||||
->find();
|
||||
|
||||
$groupFolderList = $this->entityManager
|
||||
->getRDBRepositoryByClass(GroupEmailFolder::class)
|
||||
->distinct()
|
||||
->leftJoin(Field::TEAMS)
|
||||
->where(
|
||||
$user->isAdmin() ?
|
||||
['id!=' => null] :
|
||||
['teams.id' => $user->getTeamIdList()]
|
||||
)
|
||||
->order('order')
|
||||
->limit(0, $limit)
|
||||
->find();
|
||||
|
||||
/** @var EntityCollection<GroupEmailFolder|EmailFolder> $list */
|
||||
$list = new EntityCollection();
|
||||
|
||||
foreach ($this->systemFolderList as $name) {
|
||||
$folder = $this->entityManager->getNewEntity(EmailFolder::ENTITY_TYPE);
|
||||
|
||||
$folder->set(Field::NAME, $this->language->translate($name, 'presetFilters', Email::ENTITY_TYPE));
|
||||
$folder->set(Field::ID, $name);
|
||||
|
||||
$list[] = $folder;
|
||||
}
|
||||
|
||||
foreach ($folderList as $folder) {
|
||||
$list[] = $folder;
|
||||
}
|
||||
|
||||
foreach ($groupFolderList as $folder) {
|
||||
$list[] = $folder;
|
||||
}
|
||||
|
||||
foreach ($this->systemFolderEndList as $name) {
|
||||
$folder = $this->entityManager->getNewEntity(EmailFolder::ENTITY_TYPE);
|
||||
|
||||
$folder->set(Field::NAME, $this->language->translate($name, 'presetFilters', Email::ENTITY_TYPE));
|
||||
$folder->set(Field::ID, $name);
|
||||
|
||||
$list[] = $folder;
|
||||
}
|
||||
|
||||
$finalList = [];
|
||||
|
||||
foreach ($list as $item) {
|
||||
$attributes = get_object_vars($item->getValueMap());
|
||||
|
||||
if ($item instanceof GroupEmailFolder) {
|
||||
$attributes['id'] = 'group:' . $item->getId();
|
||||
}
|
||||
|
||||
$finalList[] = $attributes;
|
||||
}
|
||||
|
||||
return $finalList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function moveUp(string $id): void
|
||||
{
|
||||
$entity = $this->entityManager->getEntityById(EmailFolder::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->acl->check($entity, 'edit')) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$currentIndex = $entity->get('order');
|
||||
|
||||
if (!is_int($currentIndex)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$previousEntity = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailFolder::class)
|
||||
->where([
|
||||
'order<' => $currentIndex,
|
||||
'assignedUserId' => $entity->get('assignedUserId'),
|
||||
])
|
||||
->order('order', true)
|
||||
->findOne();
|
||||
|
||||
if (!$previousEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set('order', $previousEntity->get('order'));
|
||||
$previousEntity->set('order', $currentIndex);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
$this->entityManager->saveEntity($previousEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function moveDown(string $id): void
|
||||
{
|
||||
$entity = $this->entityManager->getEntityById(EmailFolder::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$this->acl->checkEntityEdit($entity)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$currentIndex = $entity->get('order');
|
||||
|
||||
if (!is_int($currentIndex)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$nextEntity = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailFolder::class)
|
||||
->where([
|
||||
'order>' => $currentIndex,
|
||||
'assignedUserId' => $entity->get('assignedUserId'),
|
||||
])
|
||||
->order('order', false)
|
||||
->findOne();
|
||||
|
||||
if (!$nextEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity->set('order', $nextEntity->get('order'));
|
||||
$nextEntity->set('order', $currentIndex);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
$this->entityManager->saveEntity($nextEntity);
|
||||
}
|
||||
}
|
||||
211
application/Espo/Tools/EmailNotification/AssignmentProcessor.php
Normal file
211
application/Espo/Tools/EmailNotification/AssignmentProcessor.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailNotification;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Entity;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Preferences;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Htmlizer\Htmlizer;
|
||||
use Espo\Core\Htmlizer\HtmlizerFactory as HtmlizerFactory;
|
||||
use Espo\Core\Mail\EmailSender as EmailSender;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\TemplateFileManager;
|
||||
use Espo\Core\Utils\Util;
|
||||
|
||||
use Exception;
|
||||
use LogicException;
|
||||
|
||||
class AssignmentProcessor
|
||||
{
|
||||
private ?Htmlizer $htmlizer = null;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private HtmlizerFactory $htmlizerFactory,
|
||||
private EmailSender $emailSender,
|
||||
private Config $config,
|
||||
private TemplateFileManager $templateFileManager,
|
||||
private Metadata $metadata,
|
||||
private Language $language,
|
||||
private Log $log
|
||||
) {}
|
||||
|
||||
public function process(AssignmentProcessorData $data): void
|
||||
{
|
||||
$userId = $data->getUserId();
|
||||
$assignerUserId = $data->getAssignerUserId();
|
||||
$entityId = $data->getEntityId();
|
||||
$entityType = $data->getEntityType();
|
||||
|
||||
if (
|
||||
!$userId ||
|
||||
!$assignerUserId ||
|
||||
!$entityId ||
|
||||
!$entityType
|
||||
) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->isPortal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$preferences) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$preferences->get('receiveAssignmentEmailNotifications')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ignoreList = $preferences->get('assignmentEmailNotificationsIgnoreEntityTypeList') ?? [];
|
||||
|
||||
if (in_array($entityType, $ignoreList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$assignerUser = $this->entityManager->getEntityById(User::ENTITY_TYPE, $assignerUserId);
|
||||
|
||||
$entity = $this->entityManager->getEntityById($entityType, $entityId);
|
||||
|
||||
if (!$entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$assignerUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$entity instanceof Entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loadParentNameFields($entity);
|
||||
|
||||
if (!$entity->hasLinkMultipleField(Field::ASSIGNED_USERS)) {
|
||||
if ($entity->get('assignedUserId') !== $userId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$emailAddress = $user->get('emailAddress');
|
||||
|
||||
if (!$emailAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Email $email */
|
||||
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('assignment', 'subject', $entity->getEntityType());
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('assignment', 'body', $entity->getEntityType());
|
||||
|
||||
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
|
||||
|
||||
$recordUrl = rtrim($this->config->get('siteUrl'), '/') .
|
||||
'/#' . $entity->getEntityType() . '/view/' . $entity->getId();
|
||||
|
||||
$templateData = [
|
||||
'userName' => $user->get(Field::NAME),
|
||||
'assignerUserName' => $assignerUser->get(Field::NAME),
|
||||
'recordUrl' => $recordUrl,
|
||||
'entityType' => $this->language->translateLabel($entity->getEntityType(), 'scopeNames'),
|
||||
];
|
||||
|
||||
$templateData['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($templateData['entityType']);
|
||||
|
||||
$subject = $this->getHtmlizer()->render(
|
||||
$entity,
|
||||
$subjectTpl,
|
||||
'assignment-email-subject-' . $entity->getEntityType(),
|
||||
$templateData,
|
||||
true
|
||||
);
|
||||
|
||||
$body = $this->getHtmlizer()->render(
|
||||
$entity,
|
||||
$bodyTpl,
|
||||
'assignment-email-body-' . $entity->getEntityType(),
|
||||
$templateData,
|
||||
true
|
||||
);
|
||||
|
||||
$email->set([
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
'isHtml' => true,
|
||||
'to' => $emailAddress,
|
||||
'isSystem' => true,
|
||||
'parentId' => $entity->getId(),
|
||||
'parentType' => $entity->getEntityType(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->emailSender->send($email);
|
||||
} catch (Exception $e) {
|
||||
$this->log->error('EmailNotification: [' . $e->getCode() . '] ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function getHtmlizer(): Htmlizer
|
||||
{
|
||||
if (!$this->htmlizer) {
|
||||
$this->htmlizer = $this->htmlizerFactory->create(true);
|
||||
}
|
||||
|
||||
return $this->htmlizer;
|
||||
}
|
||||
|
||||
private function loadParentNameFields(Entity $entity): void
|
||||
{
|
||||
$fieldDefs = $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields'], []);
|
||||
|
||||
foreach ($fieldDefs as $field => $defs) {
|
||||
if (isset($defs['type']) && $defs['type'] == FieldType::LINK_PARENT) {
|
||||
$entity->loadParentNameField($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailNotification;
|
||||
|
||||
class AssignmentProcessorData
|
||||
{
|
||||
private ?string $userId = null;
|
||||
|
||||
private ?string $assignerUserId = null;
|
||||
|
||||
private ?string $entityId = null;
|
||||
|
||||
private ?string $entityType = null;
|
||||
|
||||
public function getUserId(): ?string
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getAssignerUserId(): ?string
|
||||
{
|
||||
return $this->assignerUserId;
|
||||
}
|
||||
|
||||
public function getEntityId(): ?string
|
||||
{
|
||||
return $this->entityId;
|
||||
}
|
||||
|
||||
public function getEntityType(): ?string
|
||||
{
|
||||
return $this->entityType;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function withUserId(string $userId): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->userId = $userId;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withAssignerUserId(string $assignerUserId): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->assignerUserId = $assignerUserId;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withEntityId(string $entityId): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->entityId = $entityId;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withEntityType(string $entityType): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->entityType = $entityType;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
143
application/Espo/Tools/EmailNotification/HookProcessor.php
Normal file
143
application/Espo/Tools/EmailNotification/HookProcessor.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailNotification;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\ApplicationState;
|
||||
use Espo\Core\Job\QueueName;
|
||||
use Espo\Core\Job\JobSchedulerFactory;
|
||||
use Espo\Tools\EmailNotification\Jobs\NotifyAboutAssignment;
|
||||
|
||||
class HookProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private ApplicationState $applicationState,
|
||||
private JobSchedulerFactory $jobSchedulerFactory
|
||||
) {}
|
||||
|
||||
public function afterSave(Entity $entity): void
|
||||
{
|
||||
if (!$entity instanceof CoreEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->checkToProcess($entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($entity->has('assignedUsersIds')) {
|
||||
$this->processMultiple($entity);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $entity->get('assignedUserId');
|
||||
|
||||
if (
|
||||
!$userId ||
|
||||
!$entity->isAttributeChanged('assignedUserId') ||
|
||||
!$this->isNotSelfAssignment($entity, $userId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->createJob($entity, $userId);
|
||||
}
|
||||
|
||||
private function processMultiple(CoreEntity $entity): void
|
||||
{
|
||||
$userIdList = $entity->getLinkMultipleIdList(Field::ASSIGNED_USERS);
|
||||
$fetchedAssignedUserIdList = $entity->getFetched(Field::ASSIGNED_USERS . 'Ids') ?? [];
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
if (
|
||||
in_array($userId, $fetchedAssignedUserIdList) ||
|
||||
!$this->isNotSelfAssignment($entity, $userId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createJob($entity, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkToProcess(CoreEntity $entity): bool
|
||||
{
|
||||
if (!$this->config->get('assignmentEmailNotifications')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasAssignedUserField =
|
||||
$entity->has('assignedUserId') ||
|
||||
$entity->hasLinkMultipleField(Field::ASSIGNED_USERS) &&
|
||||
$entity->has('assignedUsersIds');
|
||||
|
||||
if (!$hasAssignedUserField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(
|
||||
$entity->getEntityType(),
|
||||
$this->config->get('assignmentEmailNotificationsEntityList') ?? []
|
||||
);
|
||||
}
|
||||
|
||||
private function isNotSelfAssignment(Entity $entity, string $assignedUserId): bool
|
||||
{
|
||||
if ($entity->hasAttribute('createdById') && $entity->hasAttribute('modifiedById')) {
|
||||
if ($entity->isNew()) {
|
||||
return $assignedUserId !== $entity->get('createdById');
|
||||
}
|
||||
|
||||
return $assignedUserId !== $entity->get('modifiedById');
|
||||
}
|
||||
|
||||
return $assignedUserId !== $this->applicationState->getUserId();
|
||||
}
|
||||
|
||||
private function createJob(Entity $entity, string $userId): void
|
||||
{
|
||||
$this->jobSchedulerFactory
|
||||
->create()
|
||||
->setClassName(NotifyAboutAssignment::class)
|
||||
->setQueue(QueueName::E0)
|
||||
->setData([
|
||||
'userId' => $userId,
|
||||
'assignerUserId' => $this->applicationState->getUserId(),
|
||||
'entityId' => $entity->getId(),
|
||||
'entityType' => $entity->getEntityType(),
|
||||
])
|
||||
->schedule();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailNotification\Jobs;
|
||||
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
|
||||
use Espo\Tools\EmailNotification\AssignmentProcessor;
|
||||
use Espo\Tools\EmailNotification\AssignmentProcessorData;
|
||||
|
||||
class NotifyAboutAssignment implements Job
|
||||
{
|
||||
public function __construct(private AssignmentProcessor $assignmentProcessor)
|
||||
{}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$this->assignmentProcessor->process(
|
||||
AssignmentProcessorData::create()
|
||||
->withAssignerUserId($data->get('assignerUserId'))
|
||||
->withEntityId($data->get('entityId'))
|
||||
->withEntityType($data->get('entityType'))
|
||||
->withUserId($data->get('userId'))
|
||||
);
|
||||
}
|
||||
}
|
||||
896
application/Espo/Tools/EmailNotification/Processor.php
Normal file
896
application/Espo/Tools/EmailNotification/Processor.php
Normal file
@@ -0,0 +1,896 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailNotification;
|
||||
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Name\Link;
|
||||
use Espo\Core\Notification\EmailNotificationHandler;
|
||||
use Espo\Core\Mail\SenderParams;
|
||||
use Espo\Core\Utils\Config\ApplicationConfig;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Entities\Note;
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\Repositories\Portal as PortalRepository;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Notification;
|
||||
use Espo\Entities\Portal;
|
||||
use Espo\Entities\Preferences;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\SelectBuilder as SelectBuilder;
|
||||
use Espo\Core\Htmlizer\Htmlizer;
|
||||
use Espo\Core\Htmlizer\HtmlizerFactory as HtmlizerFactory;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Mail\EmailSender as EmailSender;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\TemplateFileManager;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Tools\Stream\NoteAccessControl;
|
||||
|
||||
use Michelf\Markdown;
|
||||
|
||||
use Exception;
|
||||
use DateTime;
|
||||
use Throwable;
|
||||
|
||||
class Processor
|
||||
{
|
||||
private const HOURS_THRESHOLD = 5;
|
||||
private const PROCESS_MAX_COUNT = 200;
|
||||
|
||||
private const TYPE_STATUS = 'Status';
|
||||
|
||||
private ?Htmlizer $htmlizer = null;
|
||||
|
||||
/** @var array<string,?EmailNotificationHandler> */
|
||||
private $emailNotificationEntityHandlerHash = [];
|
||||
/** @var array<string,?Portal> */
|
||||
private $userIdPortalCacheMap = [];
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private HtmlizerFactory $htmlizerFactory,
|
||||
private EmailSender $emailSender,
|
||||
private Config $config,
|
||||
private InjectableFactory $injectableFactory,
|
||||
private TemplateFileManager $templateFileManager,
|
||||
private Metadata $metadata,
|
||||
private Language $language,
|
||||
private Log $log,
|
||||
private NoteAccessControl $noteAccessControl,
|
||||
private ApplicationConfig $applicationConfig,
|
||||
) {}
|
||||
|
||||
public function process(): void
|
||||
{
|
||||
$mentionEmailNotifications = $this->config->get('mentionEmailNotifications');
|
||||
$streamEmailNotifications = $this->config->get('streamEmailNotifications');
|
||||
$portalStreamEmailNotifications = $this->config->get('portalStreamEmailNotifications');
|
||||
|
||||
$typeList = [];
|
||||
|
||||
if ($mentionEmailNotifications) {
|
||||
$typeList[] = Notification::TYPE_MENTION_IN_POST;
|
||||
}
|
||||
|
||||
if ($streamEmailNotifications || $portalStreamEmailNotifications) {
|
||||
$typeList[] = Notification::TYPE_NOTE;
|
||||
}
|
||||
|
||||
if (empty($typeList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fromDt = new DateTime();
|
||||
$fromDt->modify('-' . self::HOURS_THRESHOLD . ' hours');
|
||||
|
||||
$where = [
|
||||
'createdAt>' => $fromDt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT),
|
||||
'read' => false,
|
||||
'emailIsProcessed' => false,
|
||||
];
|
||||
|
||||
$delay = $this->config->get('emailNotificationsDelay');
|
||||
|
||||
if ($delay) {
|
||||
$delayDt = new DateTime();
|
||||
$delayDt->modify('-' . $delay . ' seconds');
|
||||
|
||||
$where[] = ['createdAt<' => $delayDt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)];
|
||||
}
|
||||
|
||||
$queryList = [];
|
||||
|
||||
foreach ($typeList as $type) {
|
||||
$itemBuilder = null;
|
||||
|
||||
if ($type === Notification::TYPE_MENTION_IN_POST) {
|
||||
$itemBuilder = $this->getNotificationQueryBuilderMentionInPost();
|
||||
}
|
||||
|
||||
if ($type === Notification::TYPE_NOTE) {
|
||||
$itemBuilder = $this->getNotificationQueryBuilderNote();
|
||||
}
|
||||
|
||||
if (!$itemBuilder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemBuilder->where($where);
|
||||
|
||||
$queryList[] = $itemBuilder->build();
|
||||
}
|
||||
|
||||
$builder = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->union()
|
||||
->order('number')
|
||||
->limit(0, self::PROCESS_MAX_COUNT);
|
||||
|
||||
foreach ($queryList as $query) {
|
||||
$builder->query($query);
|
||||
}
|
||||
|
||||
$unionQuery = $builder->build();
|
||||
|
||||
$sql = $this->entityManager
|
||||
->getQueryComposer()
|
||||
->compose($unionQuery);
|
||||
|
||||
/** @var Collection<Notification> $notifications */
|
||||
$notifications = $this->entityManager
|
||||
->getRDBRepository(Notification::ENTITY_TYPE)
|
||||
->findBySql($sql);
|
||||
|
||||
foreach ($notifications as $notification) {
|
||||
$notification->set('emailIsProcessed', true);
|
||||
|
||||
$type = $notification->getType();
|
||||
|
||||
try {
|
||||
if ($type === Notification::TYPE_NOTE) {
|
||||
$this->processNotificationNote($notification);
|
||||
} else if ($type === Notification::TYPE_MENTION_IN_POST) {
|
||||
$this->processNotificationMentionInPost($notification);
|
||||
} else {
|
||||
// For bc.
|
||||
$methodName = 'processNotification' . ucfirst($type ?? 'Dummy');
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
$this->$methodName($notification);
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($notification);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getNotificationQueryBuilderMentionInPost(): SelectBuilder
|
||||
{
|
||||
return $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->select()
|
||||
->from(Notification::ENTITY_TYPE)
|
||||
->where([
|
||||
'type' => Notification::TYPE_MENTION_IN_POST,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getNotificationQueryBuilderNote(): SelectBuilder
|
||||
{
|
||||
$builder = $this->entityManager
|
||||
->getQueryBuilder()
|
||||
->select()
|
||||
->from(Notification::ENTITY_TYPE)
|
||||
->join(Note::ENTITY_TYPE, 'note', ['note.id:' => 'relatedId'])
|
||||
->join('user')
|
||||
->where([
|
||||
'type' => Notification::TYPE_NOTE,
|
||||
'relatedType' => Note::ENTITY_TYPE,
|
||||
'note.type' => $this->getNoteNotificationTypeList(),
|
||||
]);
|
||||
|
||||
$entityList = $this->config->get('streamEmailNotificationsEntityList');
|
||||
|
||||
if (empty($entityList)) {
|
||||
$builder->where([
|
||||
'relatedParentType' => null,
|
||||
]);
|
||||
} else {
|
||||
$builder->where([
|
||||
'OR' => [
|
||||
[
|
||||
'relatedParentType' => $entityList,
|
||||
],
|
||||
[
|
||||
'relatedParentType' => null,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$forInternal = $this->config->get('streamEmailNotifications');
|
||||
$forPortal = $this->config->get('portalStreamEmailNotifications');
|
||||
|
||||
if ($forInternal && !$forPortal) {
|
||||
$builder->where([
|
||||
'user.type!=' => User::TYPE_PORTAL,
|
||||
]);
|
||||
} else if (!$forInternal && $forPortal) {
|
||||
$builder->where([
|
||||
'user.type' => User::TYPE_PORTAL,
|
||||
]);
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
protected function processNotificationMentionInPost(Notification $notification): void
|
||||
{
|
||||
if (!$notification->get('userId')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $notification->get('userId');
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailAddress = $user->get('emailAddress');
|
||||
|
||||
if (!$emailAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$preferences) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$preferences->get('receiveMentionEmailNotifications')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$notification->getRelated() || $notification->getRelated()->getEntityType() !== Note::ENTITY_TYPE) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var ?Note $note */
|
||||
$note = $this->entityManager->getEntityById(Note::ENTITY_TYPE, $notification->getRelated()->getId());
|
||||
|
||||
if (!$note) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent = null;
|
||||
|
||||
$parentId = $note->getParentId();
|
||||
$parentType = $note->getParentType();
|
||||
|
||||
$data = [];
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['url'] = "{$this->getSiteUrl($user)}/#$parentType/view/$parentId";
|
||||
$data['parentName'] = $parent->get(Field::NAME);
|
||||
$data['parentType'] = $parentType;
|
||||
$data['parentId'] = $parentId;
|
||||
} else {
|
||||
$data['url'] = $this->getSiteUrl($user) . '/#Notification';
|
||||
}
|
||||
|
||||
$data['userName'] = $note->get('createdByName');
|
||||
|
||||
$post = Markdown::defaultTransform(
|
||||
$note->get('post') ?? ''
|
||||
);
|
||||
|
||||
$data['post'] = $post;
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('mention', 'subject');
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('mention', 'body');
|
||||
|
||||
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
|
||||
|
||||
$subject = $this->getHtmlizer()->render($note, $subjectTpl, 'mention-email-subject', $data, true);
|
||||
$body = $this->getHtmlizer()->render($note, $bodyTpl, 'mention-email-body', $data, true);
|
||||
|
||||
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
|
||||
|
||||
$email
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->setIsHtml()
|
||||
->addToAddress($emailAddress);
|
||||
$email->set('isSystem', true);
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
$email->setParent(LinkParent::create($parentType, $parentId));
|
||||
}
|
||||
|
||||
$senderParams = SenderParams::create();
|
||||
|
||||
if ($parent && $parentType) {
|
||||
$handler = $this->getHandler('mention', $parentType);
|
||||
|
||||
if ($handler) {
|
||||
$handler->prepareEmail($email, $parent, $user);
|
||||
|
||||
$senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams;
|
||||
}
|
||||
}
|
||||
|
||||
$sender = $this->emailSender
|
||||
->withParams($senderParams);
|
||||
|
||||
if ($note->getType() !== Note::TYPE_POST) {
|
||||
$sender = $sender->withAddedHeader('Auto-Submitted', 'auto-generated');
|
||||
}
|
||||
|
||||
try {
|
||||
$sender->send($email);
|
||||
} catch (Exception $e) {
|
||||
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processNotificationNote(Notification $notification): void
|
||||
{
|
||||
if (
|
||||
!$notification->getRelated() ||
|
||||
$notification->getRelated()->getEntityType() !== Note::ENTITY_TYPE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$noteId = $notification->getRelated()->getId();
|
||||
|
||||
$note = $this->entityManager->getRDBRepositoryByClass(Note::class)->getById($noteId);
|
||||
|
||||
if (
|
||||
!$note ||
|
||||
!in_array($note->getType(), $this->getNoteNotificationTypeList()) ||
|
||||
!$notification->getUserId()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $notification->getUserId();
|
||||
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$user->getEmailAddress()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$preferences = $this->entityManager->getEntityById(Preferences::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$preferences) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$preferences->get('receiveStreamEmailNotifications')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $note->getType();
|
||||
|
||||
if ($type === Note::TYPE_POST) {
|
||||
$this->processNotificationNotePost($note, $user);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === Note::TYPE_UPDATE && isset($note->getData()->value)) {
|
||||
$this->processNotificationNoteStatus($note, $user);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($type === Note::TYPE_EMAIL_RECEIVED) {
|
||||
$this->processNotificationNoteEmailReceived($note, $user);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** For bc. */
|
||||
$methodName = 'processNotificationNote' . $type;
|
||||
|
||||
if (method_exists($this, $methodName)) {
|
||||
$this->$methodName($note, $user);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getHandler(string $type, string $entityType): ?EmailNotificationHandler
|
||||
{
|
||||
$key = $type . '-' . $entityType;
|
||||
|
||||
if (!array_key_exists($key, $this->emailNotificationEntityHandlerHash)) {
|
||||
$this->emailNotificationEntityHandlerHash[$key] = null;
|
||||
|
||||
/** @var ?class-string<EmailNotificationHandler> $className */
|
||||
$className = $this->metadata
|
||||
->get(['notificationDefs', $entityType, 'emailNotificationHandlerClassNameMap', $type]);
|
||||
|
||||
if ($className && class_exists($className)) {
|
||||
$handler = $this->injectableFactory->create($className);
|
||||
|
||||
$this->emailNotificationEntityHandlerHash[$key] = $handler;
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection PhpExpressionAlwaysNullInspection */
|
||||
return $this->emailNotificationEntityHandlerHash[$key];
|
||||
}
|
||||
|
||||
protected function processNotificationNotePost(Note $note, User $user): void
|
||||
{
|
||||
$parentId = $note->getParentId();
|
||||
$parentType = $note->getParentType();
|
||||
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
|
||||
if (!$emailAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
$data['userName'] = $note->get('createdByName');
|
||||
|
||||
$post = Markdown::defaultTransform($note->getPost() ?? '');
|
||||
|
||||
$data['post'] = $post;
|
||||
|
||||
$parent = null;
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['url'] = "{$this->getSiteUrl($user)}/#$parentType/view/$parentId";
|
||||
$data['parentName'] = $parent->get(Field::NAME);
|
||||
$data['parentType'] = $parentType;
|
||||
$data['parentId'] = $parentId;
|
||||
|
||||
$data['name'] = $data['parentName'];
|
||||
|
||||
$data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames');
|
||||
$data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']);
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('notePost', 'subject', $parentType);
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('notePost', 'body', $parentType);
|
||||
|
||||
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
|
||||
|
||||
$subject = $this->getHtmlizer()->render(
|
||||
$note,
|
||||
$subjectTpl,
|
||||
'note-post-email-subject-' . $parentType,
|
||||
$data,
|
||||
true
|
||||
);
|
||||
|
||||
$body = $this->getHtmlizer()->render(
|
||||
$note,
|
||||
$bodyTpl,
|
||||
'note-post-email-body-' . $parentType,
|
||||
$data,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
$data['url'] = "{$this->getSiteUrl($user)}/#Notification";
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('notePostNoParent', 'subject');
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('notePostNoParent', 'body');
|
||||
|
||||
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
|
||||
|
||||
$subject = $this->getHtmlizer()->render($note, $subjectTpl, 'note-post-email-subject', $data, true);
|
||||
$body = $this->getHtmlizer()->render($note, $bodyTpl, 'note-post-email-body', $data, true);
|
||||
}
|
||||
|
||||
/** @var Email $email */
|
||||
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
|
||||
|
||||
$email
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->setIsHtml()
|
||||
->addToAddress($emailAddress);
|
||||
|
||||
$email->set('isSystem', true);
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
$email->setParent(LinkParent::create($parentType, $parentId));
|
||||
}
|
||||
|
||||
$senderParams = SenderParams::create();
|
||||
|
||||
if ($parent) {
|
||||
$handler = $this->getHandler('notePost', $parent->getEntityType());
|
||||
|
||||
if ($handler) {
|
||||
$handler->prepareEmail($email, $parent, $user);
|
||||
|
||||
$senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->emailSender
|
||||
->withParams($senderParams)
|
||||
->send($email);
|
||||
} catch (Exception $e) {
|
||||
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getSiteUrl(User $user): string
|
||||
{
|
||||
$portal = null;
|
||||
|
||||
if (!$user->isPortal()) {
|
||||
return $this->applicationConfig->getSiteUrl();
|
||||
}
|
||||
|
||||
if (!array_key_exists($user->getId(), $this->userIdPortalCacheMap)) {
|
||||
$this->userIdPortalCacheMap[$user->getId()] = null;
|
||||
|
||||
$portalIdList = $user->getLinkMultipleIdList('portals');
|
||||
|
||||
$defaultPortalId = $this->config->get('defaultPortalId');
|
||||
|
||||
$portalId = null;
|
||||
|
||||
if (in_array($defaultPortalId, $portalIdList)) {
|
||||
$portalId = $defaultPortalId;
|
||||
} else if (count($portalIdList)) {
|
||||
$portalId = $portalIdList[0];
|
||||
}
|
||||
|
||||
if ($portalId) {
|
||||
/** @var ?Portal $portal */
|
||||
$portal = $this->entityManager->getEntityById(Portal::ENTITY_TYPE, $portalId);
|
||||
}
|
||||
|
||||
if ($portal) {
|
||||
$this->getPortalRepository()->loadUrlField($portal);
|
||||
|
||||
$this->userIdPortalCacheMap[$user->getId()] = $portal;
|
||||
}
|
||||
} else {
|
||||
$portal = $this->userIdPortalCacheMap[$user->getId()];
|
||||
}
|
||||
|
||||
if ($portal) {
|
||||
return rtrim($portal->get('url'), '/');
|
||||
}
|
||||
|
||||
return $this->applicationConfig->getSiteUrl();
|
||||
}
|
||||
|
||||
protected function processNotificationNoteStatus(Note $note, User $user): void
|
||||
{
|
||||
$this->noteAccessControl->apply($note, $user);
|
||||
|
||||
$parentId = $note->getParentId();
|
||||
$parentType = $note->getParentType();
|
||||
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
|
||||
if (!$emailAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
if (!$parentId || !$parentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note->loadParentNameField('superParent');
|
||||
|
||||
$data['url'] = "{$this->getSiteUrl($user)}/#$parentType/view/$parentId";
|
||||
$data['parentName'] = $parent->get(Field::NAME);
|
||||
$data['parentType'] = $parentType;
|
||||
$data['parentId'] = $parentId;
|
||||
$data['superParentName'] = $note->get('superParentName');
|
||||
$data['superParentType'] = $note->getSuperParentType();
|
||||
$data['superParentId'] = $note->getSuperParentId();
|
||||
$data['name'] = $data['parentName'];
|
||||
$data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames');
|
||||
$data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']);
|
||||
|
||||
$noteData = $note->getData();
|
||||
|
||||
$value = $noteData->value ?? null;
|
||||
$field = $this->metadata->get("scopes.$parentType.statusField");
|
||||
|
||||
if ($value === null || !$field || !is_string($field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['value'] = $value;
|
||||
$data['field'] = $field;
|
||||
$data['valueTranslated'] = $this->language->translateOption($value, $field, $parentType);
|
||||
$data['fieldTranslated'] = $this->language->translateLabel($field, 'fields', $parentType);
|
||||
$data['fieldTranslatedLowerCase'] = Util::mbLowerCaseFirst($data['fieldTranslated']);
|
||||
|
||||
$data['userName'] = $note->get('createdByName');
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('noteStatus', 'subject', $parentType);
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('noteStatus', 'body', $parentType);
|
||||
|
||||
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
|
||||
|
||||
$subject = $this->getHtmlizer()->render(
|
||||
entity: $note,
|
||||
template: $subjectTpl,
|
||||
cacheId: 'note-status-email-subject',
|
||||
additionalData: $data,
|
||||
skipLinks: true,
|
||||
);
|
||||
|
||||
$body = $this->getHtmlizer()->render(
|
||||
entity: $note,
|
||||
template: $bodyTpl,
|
||||
cacheId: 'note-status-email-body',
|
||||
additionalData: $data,
|
||||
skipLinks: true,
|
||||
);
|
||||
|
||||
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
|
||||
|
||||
$email
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->setIsHtml()
|
||||
->addToAddress($emailAddress)
|
||||
->setParent(LinkParent::create($parentType, $parentId));
|
||||
|
||||
$email->set('isSystem', true);
|
||||
|
||||
$senderParams = SenderParams::create();
|
||||
|
||||
$handler = $this->getHandler('status', $parentType);
|
||||
|
||||
if ($handler) {
|
||||
$handler->prepareEmail($email, $parent, $user);
|
||||
|
||||
$senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams;
|
||||
}
|
||||
|
||||
$sender = $this->emailSender->withParams($senderParams);
|
||||
|
||||
try {
|
||||
$sender->send($email);
|
||||
} catch (Exception $e) {
|
||||
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function processNotificationNoteEmailReceived(Note $note, User $user): void
|
||||
{
|
||||
$parentId = $note->get('parentId');
|
||||
$parentType = $note->getParentType();
|
||||
|
||||
$allowedEntityTypeList = $this->config->get('streamEmailNotificationsEmailReceivedEntityTypeList');
|
||||
|
||||
if (
|
||||
is_array($allowedEntityTypeList) &&
|
||||
!in_array($parentType, $allowedEntityTypeList)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
|
||||
if (!$emailAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
$noteData = $note->getData();
|
||||
|
||||
if (!isset($noteData->emailId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailSubject = $this->entityManager->getEntityById(Email::ENTITY_TYPE, $noteData->emailId);
|
||||
|
||||
if (!$emailSubject) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailAddresses = $this->entityManager
|
||||
->getRelation($user, Link::EMAIL_ADDRESSES)
|
||||
->find();
|
||||
|
||||
foreach ($emailAddresses as $ea) {
|
||||
if (
|
||||
$this->entityManager->getRelation($emailSubject, 'toEmailAddresses')->isRelated($ea) ||
|
||||
$this->entityManager->getRelation($emailSubject, 'ccEmailAddresses')->isRelated($ea)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
$data['fromName'] = '';
|
||||
|
||||
if (isset($noteData->personEntityName)) {
|
||||
$data['fromName'] = $noteData->personEntityName;
|
||||
} else if (isset($noteData->fromString)) {
|
||||
$data['fromName'] = $noteData->fromString;
|
||||
}
|
||||
|
||||
$data['subject'] = '';
|
||||
|
||||
if (isset($noteData->emailName)) {
|
||||
$data['subject'] = $noteData->emailName;
|
||||
}
|
||||
|
||||
$data['post'] = nl2br($note->get('post'));
|
||||
|
||||
if (!$parentId || !$parentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['url'] = "{$this->getSiteUrl($user)}/#$parentType/view/$parentId";
|
||||
$data['parentName'] = $parent->get(Field::NAME);
|
||||
$data['parentType'] = $parentType;
|
||||
$data['parentId'] = $parentId;
|
||||
|
||||
$data['name'] = $data['parentName'];
|
||||
|
||||
$data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames');
|
||||
$data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']);
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('noteEmailReceived', 'subject', $parentType);
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('noteEmailReceived', 'body', $parentType);
|
||||
|
||||
$subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl);
|
||||
|
||||
$subject = $this->getHtmlizer()->render(
|
||||
$note,
|
||||
$subjectTpl,
|
||||
'note-email-received-email-subject-' . $parentType,
|
||||
$data,
|
||||
true
|
||||
);
|
||||
|
||||
$body = $this->getHtmlizer()->render(
|
||||
$note,
|
||||
$bodyTpl,
|
||||
'note-email-received-email-body-' . $parentType,
|
||||
$data,
|
||||
true
|
||||
);
|
||||
|
||||
/** @var Email $email */
|
||||
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
|
||||
|
||||
$email
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->setIsHtml()
|
||||
->addToAddress($emailAddress)
|
||||
->setParent(LinkParent::create($parentType, $parentId));
|
||||
|
||||
$email->set('isSystem', true);
|
||||
|
||||
$senderParams = SenderParams::create();
|
||||
|
||||
$handler = $this->getHandler('emailReceived', $parentType);
|
||||
|
||||
if ($handler) {
|
||||
$handler->prepareEmail($email, $parent, $user);
|
||||
|
||||
$senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->emailSender
|
||||
->withParams($senderParams)
|
||||
->send($email);
|
||||
} catch (Exception $e) {
|
||||
$this->log->error("Email notification: {$e->getMessage()}", ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getHtmlizer(): Htmlizer
|
||||
{
|
||||
if (!$this->htmlizer) {
|
||||
$this->htmlizer = $this->htmlizerFactory->create(true);
|
||||
}
|
||||
|
||||
return $this->htmlizer;
|
||||
}
|
||||
|
||||
private function getPortalRepository(): PortalRepository
|
||||
{
|
||||
/** @var PortalRepository */
|
||||
return $this->entityManager->getRepository(Portal::ENTITY_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getNoteNotificationTypeList(): array
|
||||
{
|
||||
/** @var string[] $output */
|
||||
$output = $this->config->get('streamEmailNotificationsTypeList', []);
|
||||
|
||||
if (in_array(self::TYPE_STATUS, $output)) {
|
||||
$output[] = Note::TYPE_UPDATE;
|
||||
|
||||
$output = array_values(array_filter($output, fn ($v) => $v !== self::TYPE_STATUS));
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
68
application/Espo/Tools/EmailTemplate/Api/PostPrepare.php
Normal file
68
application/Espo/Tools/EmailTemplate/Api/PostPrepare.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate\Api;
|
||||
|
||||
use Espo\Core\Api\Action;
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
use Espo\Core\Api\ResponseComposer;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Tools\EmailTemplate\Data;
|
||||
use Espo\Tools\EmailTemplate\Service;
|
||||
|
||||
/**
|
||||
* Prepares an email data with an email template applied.
|
||||
*/
|
||||
class PostPrepare implements Action
|
||||
{
|
||||
public function __construct(private Service $service) {}
|
||||
|
||||
public function process(Request $request): Response
|
||||
{
|
||||
$id = $request->getRouteParam('id');
|
||||
|
||||
if ($id === null) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
$data = Data::create()
|
||||
->withRelatedType($body->relatedType ?? null)
|
||||
->withRelatedId($body->relatedId ?? null)
|
||||
->withParentType($body->parentType ?? null)
|
||||
->withParentId($body->parentId ?? null)
|
||||
->withEmailAddress($body->emailAddress ?? null);
|
||||
|
||||
$result = $this->service->process($id, $data);
|
||||
|
||||
return ResponseComposer::json($result->getValueMap());
|
||||
}
|
||||
}
|
||||
169
application/Espo/Tools/EmailTemplate/Data.php
Normal file
169
application/Espo/Tools/EmailTemplate/Data.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\User;
|
||||
|
||||
class Data
|
||||
{
|
||||
/** @var array<string, Entity> */
|
||||
private $entityHash = [];
|
||||
private ?string $emailAddress = null;
|
||||
private ?Entity $parent = null;
|
||||
private ?string $parentId = null;
|
||||
private ?string $parentType = null;
|
||||
private ?string $relatedId = null;
|
||||
private ?string $relatedType = null;
|
||||
private ?User $user = null;
|
||||
|
||||
/**
|
||||
* @return array<string,Entity> $entityHash
|
||||
*/
|
||||
public function getEntityHash(): array
|
||||
{
|
||||
return $this->entityHash;
|
||||
}
|
||||
|
||||
public function getEmailAddress(): ?string
|
||||
{
|
||||
return $this->emailAddress;
|
||||
}
|
||||
|
||||
public function getParent(): ?Entity
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function getParentId(): ?string
|
||||
{
|
||||
return $this->parentId;
|
||||
}
|
||||
|
||||
public function getParentType(): ?string
|
||||
{
|
||||
return $this->parentType;
|
||||
}
|
||||
|
||||
public function getRelatedId(): ?string
|
||||
{
|
||||
return $this->relatedId;
|
||||
}
|
||||
|
||||
public function getRelatedType(): ?string
|
||||
{
|
||||
return $this->relatedType;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* An entity hash.
|
||||
*
|
||||
* @param array<string,Entity> $entityHash
|
||||
*/
|
||||
public function withEntityHash(array $entityHash): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->entityHash = $entityHash;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* An email address.
|
||||
*/
|
||||
public function withEmailAddress(?string $emailAddress): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->emailAddress = $emailAddress;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withParent(?Entity $parent): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->parent = $parent;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withParentId(?string $parentId): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->parentId = $parentId;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withParentType(?string $parentType): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->parentType = $parentType;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withRelatedId(?string $relatedId): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->relatedId = $relatedId;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withRelatedType(?string $relatedType): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->relatedType = $relatedType;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* A user to apply ACL for.
|
||||
*/
|
||||
public function withUser(?User $user): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->user = $user;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
131
application/Espo/Tools/EmailTemplate/EntityMapProvider.php
Normal file
131
application/Espo/Tools/EmailTemplate/EntityMapProvider.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate;
|
||||
|
||||
use Espo\Core\Acl\GlobalRestriction;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
/**
|
||||
* @since 9.2.0
|
||||
* @internal
|
||||
*/
|
||||
class EntityMapProvider
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private AclManager $aclManager,
|
||||
private ServiceContainer $serviceContainer,
|
||||
private Metadata $metadata,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, Entity>
|
||||
*/
|
||||
public function get(Entity $entity, User $user, bool $applyAcl): array
|
||||
{
|
||||
/** @var array<string, string> $map */
|
||||
$map = $this->metadata->get("app.emailTemplate.entityLinkMapping.{$entity->getEntityType()}") ?? [];
|
||||
|
||||
$output = [];
|
||||
|
||||
foreach ($map as $entityType => $link) {
|
||||
$related = $this->getRelated(
|
||||
entity: $entity,
|
||||
link: $link,
|
||||
user: $user,
|
||||
applyAcl: $applyAcl,
|
||||
);
|
||||
|
||||
if ($related) {
|
||||
$output[$entityType] = $related;
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function getRelated(
|
||||
Entity $entity,
|
||||
string $link,
|
||||
User $user,
|
||||
bool $applyAcl,
|
||||
): ?Entity {
|
||||
|
||||
$entityDefs = $this->entityManager->getDefs()->getEntity($entity->getEntityType());
|
||||
|
||||
$forbiddenLinkList = $this->aclManager->getScopeRestrictedLinkList(
|
||||
$entity->getEntityType(),
|
||||
[
|
||||
GlobalRestriction::TYPE_FORBIDDEN,
|
||||
GlobalRestriction::TYPE_INTERNAL,
|
||||
GlobalRestriction::TYPE_ONLY_ADMIN,
|
||||
]
|
||||
);
|
||||
|
||||
if ($applyAcl) {
|
||||
if (
|
||||
$entityDefs->hasField($link) &&
|
||||
!$this->aclManager->checkField($user, $entity->getEntityType(), $link)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (in_array($link, $forbiddenLinkList)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$related = $this->entityManager
|
||||
->getRelation($entity, $link)
|
||||
->findOne();
|
||||
|
||||
if (!$related) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
$applyAcl &&
|
||||
!$this->aclManager->checkEntityRead($user, $related)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->serviceContainer
|
||||
->get($related->getEntityType())
|
||||
->loadAdditionalFields($related);
|
||||
|
||||
return $related;
|
||||
}
|
||||
}
|
||||
178
application/Espo/Tools/EmailTemplate/Formatter.php
Normal file
178
application/Espo/Tools/EmailTemplate/Formatter.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate;
|
||||
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Core\Utils\NumberUtil;
|
||||
use Espo\Core\Utils\Language;
|
||||
|
||||
use Espo\ORM\Type\AttributeType;
|
||||
use Stringable;
|
||||
|
||||
class Formatter
|
||||
{
|
||||
public function __construct(
|
||||
private Metadata $metadata,
|
||||
private Config $config,
|
||||
private DateTimeUtil $dateTime,
|
||||
private NumberUtil $number,
|
||||
private Language $language
|
||||
) {}
|
||||
|
||||
public function formatAttributeValue(Entity $entity, string $attribute, bool $isPlainText = false): ?string
|
||||
{
|
||||
$value = $entity->get($attribute);
|
||||
|
||||
$fieldType = $this->metadata
|
||||
->get(['entityDefs', $entity->getEntityType(), 'fields', $attribute, 'type']);
|
||||
|
||||
$attributeType = $entity->getAttributeType($attribute);
|
||||
|
||||
if ($fieldType === FieldType::ENUM) {
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$label = $this->language->translateOption($value, $attribute, $entity->getEntityType());
|
||||
|
||||
$translationPath = $this->metadata->get(
|
||||
['entityDefs', $entity->getEntityType(), 'fields', $attribute, 'translation']
|
||||
);
|
||||
|
||||
if ($translationPath) {
|
||||
$label = $this->language->get($translationPath . '.' . $value, $label);
|
||||
}
|
||||
|
||||
return $label;
|
||||
}
|
||||
|
||||
if (
|
||||
$fieldType === FieldType::ARRAY ||
|
||||
$fieldType === FieldType::MULTI_ENUM ||
|
||||
$fieldType === FieldType::CHECKLIST
|
||||
) {
|
||||
$valueList = [];
|
||||
|
||||
if (!is_array($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($value as $v) {
|
||||
$valueList[] = $this->language->translateOption($v, $attribute, $entity->getEntityType());
|
||||
}
|
||||
|
||||
return implode(', ', $valueList);
|
||||
}
|
||||
|
||||
if ($attributeType === AttributeType::DATE) {
|
||||
if (!$value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->dateTime->convertSystemDate($value);
|
||||
}
|
||||
|
||||
if ($attributeType === AttributeType::DATETIME) {
|
||||
if (!$value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->dateTime->convertSystemDateTime($value);
|
||||
}
|
||||
|
||||
if ($attributeType === AttributeType::TEXT) {
|
||||
if (!is_string($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($fieldType === FieldType::WYSIWYG) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($isPlainText) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return nl2br($value);
|
||||
}
|
||||
|
||||
if ($attributeType === AttributeType::FLOAT) {
|
||||
if (!is_float($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$decimalPlaces = 2;
|
||||
|
||||
if ($fieldType === FieldType::CURRENCY) {
|
||||
$decimalPlaces = $this->config->get('currencyDecimalPlaces');
|
||||
}
|
||||
|
||||
return $this->number->format($value, $decimalPlaces);
|
||||
}
|
||||
|
||||
if ($attributeType === AttributeType::INT) {
|
||||
if (!is_int($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
$fieldType === FieldType::AUTOINCREMENT ||
|
||||
$fieldType === FieldType::INT &&
|
||||
$this->metadata
|
||||
->get(['entityDefs', $entity->getEntityType(), 'fields', $attribute, 'disableFormatting'])
|
||||
) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
return $this->number->format($value);
|
||||
}
|
||||
|
||||
if (
|
||||
!is_string($value) && is_scalar($value) ||
|
||||
$value instanceof Stringable
|
||||
) {
|
||||
return strval($value);
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
182
application/Espo/Tools/EmailTemplate/InsertField/Service.php
Normal file
182
application/Espo/Tools/EmailTemplate/InsertField/Service.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate\InsertField;
|
||||
|
||||
use Espo\Core\Acl;
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Type\FieldType;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\FieldUtil;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\Modules\Crm\Entities\Lead;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
use Espo\Tools\EmailTemplate\Formatter;
|
||||
use stdClass;
|
||||
|
||||
class Service
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private Acl $acl;
|
||||
private Formatter $formatter;
|
||||
private FieldUtil $fieldUtil;
|
||||
private ServiceContainer $recordServiceContainer;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
Acl $acl,
|
||||
Formatter $formatter,
|
||||
FieldUtil $fieldUtil,
|
||||
ServiceContainer $recordServiceContainer
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->acl = $acl;
|
||||
$this->formatter = $formatter;
|
||||
$this->fieldUtil = $fieldUtil;
|
||||
$this->recordServiceContainer = $recordServiceContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function getData(?string $parentType, ?string $parentId, ?string $to): stdClass
|
||||
{
|
||||
if (!$this->acl->checkScope(Email::ENTITY_TYPE, Table::ACTION_CREATE)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$result = (object) [];
|
||||
|
||||
$dataList = [];
|
||||
|
||||
if ($parentId && $parentType) {
|
||||
$e = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if ($e && $this->acl->check($e)) {
|
||||
$dataList[] = [
|
||||
'type' => 'parent',
|
||||
'entity' => $e,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($to) {
|
||||
$e = $this->getEmailAddressRepository()
|
||||
->getEntityByAddress($to, null,
|
||||
[Contact::ENTITY_TYPE, Lead::ENTITY_TYPE, Account::ENTITY_TYPE]);
|
||||
|
||||
if ($e && $e->getEntityType() !== User::ENTITY_TYPE && $this->acl->check($e)) {
|
||||
$dataList[] = [
|
||||
'type' => 'to',
|
||||
'entity' => $e,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$fm = $this->fieldUtil;
|
||||
|
||||
$formatter = $this->formatter;
|
||||
|
||||
foreach ($dataList as $item) {
|
||||
$type = $item['type'];
|
||||
$e = $item['entity'];
|
||||
|
||||
$entityType = $e->getEntityType();
|
||||
|
||||
$recordService = $this->recordServiceContainer->get($entityType);
|
||||
|
||||
$recordService->loadAdditionalFields($e);
|
||||
$recordService->prepareEntityForOutput($e);
|
||||
|
||||
$ignoreTypeList = [
|
||||
FieldType::IMAGE,
|
||||
FieldType::FILE,
|
||||
FieldType::WYSIWYG,
|
||||
FieldType::LINK_MULTIPLE,
|
||||
FieldType::ATTACHMENT_MULTIPLE,
|
||||
FieldType::BOOL,
|
||||
'map',
|
||||
];
|
||||
|
||||
foreach ($fm->getEntityTypeFieldList($entityType) as $field) {
|
||||
$fieldType = $fm->getEntityTypeFieldParam($entityType, $field, 'type');
|
||||
$fieldAttributeList = $fm->getAttributeList($entityType, $field);
|
||||
|
||||
if (
|
||||
$fm->getEntityTypeFieldParam($entityType, $field, 'disabled') ||
|
||||
$fm->getEntityTypeFieldParam($entityType, $field, 'directAccessDisabled') ||
|
||||
$fm->getEntityTypeFieldParam($entityType, $field, 'templatePlaceholderDisabled') ||
|
||||
in_array($fieldType, $ignoreTypeList)
|
||||
) {
|
||||
foreach ($fieldAttributeList as $a) {
|
||||
$e->clear($a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$attributeList = $fm->getEntityTypeAttributeList($entityType);
|
||||
|
||||
$values = (object) [];
|
||||
|
||||
foreach ($attributeList as $a) {
|
||||
if (!$e->has($a)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $formatter->formatAttributeValue($e, $a);
|
||||
|
||||
if ($value !== null && $value !== '') {
|
||||
$values->$a = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$result->$type = (object) [
|
||||
'entityType' => $e->getEntityType(),
|
||||
'id' => $e->getId(),
|
||||
'values' => $values,
|
||||
'name' => $e->get(Field::NAME),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getEmailAddressRepository(): EmailAddressRepository
|
||||
{
|
||||
/** @var EmailAddressRepository */
|
||||
return $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE);
|
||||
}
|
||||
}
|
||||
76
application/Espo/Tools/EmailTemplate/Params.php
Normal file
76
application/Espo/Tools/EmailTemplate/Params.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
private bool $applyAcl = false;
|
||||
private bool $copyAttachments = false;
|
||||
|
||||
public function applyAcl(): bool
|
||||
{
|
||||
return $this->applyAcl;
|
||||
}
|
||||
|
||||
public function copyAttachments(): bool
|
||||
{
|
||||
return $this->copyAttachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* To apply ACL.
|
||||
*/
|
||||
public function withApplyAcl(bool $applyAcl = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->applyAcl = $applyAcl;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* To copy template attachments records. Not needed if an email not supposed to be stored.
|
||||
*/
|
||||
public function withCopyAttachments(bool $copyAttachments = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->copyAttachments = $copyAttachments;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
}
|
||||
35
application/Espo/Tools/EmailTemplate/Placeholder.php
Normal file
35
application/Espo/Tools/EmailTemplate/Placeholder.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate;
|
||||
|
||||
interface Placeholder
|
||||
{
|
||||
public function get(Data $data): string;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate\Placeholders;
|
||||
|
||||
use DateTime;
|
||||
use DateTimezone;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Tools\EmailTemplate\Data;
|
||||
use Espo\Tools\EmailTemplate\Placeholder;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class CurrentYear implements Placeholder
|
||||
{
|
||||
public function __construct(
|
||||
private Config\ApplicationConfig $applicationConfig,
|
||||
) {}
|
||||
|
||||
public function get(Data $data): string
|
||||
{
|
||||
try {
|
||||
$now = new DateTime('now', new DateTimezone($this->applicationConfig->getTimeZone()));
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
|
||||
return $now->format('Y');
|
||||
}
|
||||
}
|
||||
49
application/Espo/Tools/EmailTemplate/Placeholders/Now.php
Normal file
49
application/Espo/Tools/EmailTemplate/Placeholders/Now.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\EmailTemplate\Placeholders;
|
||||
|
||||
use Espo\Core\Utils\DateTime;
|
||||
use Espo\Tools\EmailTemplate\Data;
|
||||
use Espo\Tools\EmailTemplate\Placeholder;
|
||||
|
||||
/**
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
class Now implements Placeholder
|
||||
{
|
||||
public function __construct(
|
||||
private DateTime $dateTime
|
||||
) {}
|
||||
|
||||
public function get(Data $data): string
|
||||
{
|
||||
return $this->dateTime->getNowString();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user