Initial commit

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

View File

@@ -0,0 +1,174 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction\Actions;
use Espo\Core\Acl\Table;
use Espo\Core\Acl;
use Espo\Core\Currency\ConfigDataProvider as CurrencyConfigDataProvider;
use Espo\Core\Currency\Rates as CurrencyRates;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\MassAction\Data;
use Espo\Core\MassAction\MassAction;
use Espo\Core\MassAction\Params;
use Espo\Core\MassAction\QueryBuilder;
use Espo\Core\MassAction\Result;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\ORM\EntityManager;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\Core\Utils\Log;
use Espo\Core\Utils\Metadata;
use Espo\Entities\User;
use Espo\Tools\Currency\Conversion\EntityConverterFactory;
use RuntimeException;
/**
* @noinspection PhpUnused
*/
class MassConvertCurrency implements MassAction
{
public function __construct(
private QueryBuilder $queryBuilder,
private Acl $acl,
private EntityManager $entityManager,
private Metadata $metadata,
private CurrencyConfigDataProvider $configDataProvider,
private EntityConverterFactory $converterFactory,
private User $user,
private Log $log,
) {}
public function process(Params $params, Data $data): Result
{
$entityType = $params->getEntityType();
if (!$this->acl->checkScope($entityType, Table::ACTION_EDIT)) {
throw new Forbidden("No edit access for '{$entityType}'.");
}
if ($this->acl->getPermissionLevel(Acl\Permission::MASS_UPDATE) !== Table::LEVEL_YES) {
throw new Forbidden("No mass-update permission.");
}
$this->checkFieldAccess($entityType);
$dataRaw = $data->getRaw();
if (empty($dataRaw->targetCurrency)) {
throw new BadRequest("No target currency.");
}
if (isset($dataRaw->rates) && !is_object($dataRaw->rates)) {
throw new BadRequest();
}
$baseCurrency = $this->configDataProvider->getBaseCurrency();
$targetCurrency = $dataRaw->targetCurrency;
$rates = $this->getRatesFromData($data) ??
$this->configDataProvider->getCurrencyRates();
if ($targetCurrency !== $baseCurrency && !$rates->hasRate($targetCurrency)) {
throw new BadRequest("Target currency rate is not specified.");
}
$query = $this->queryBuilder->build($params);
$collection = $this->entityManager
->getRDBRepository($entityType)
->clone($query)
->sth()
->find();
$ids = [];
$count = 0;
$converter = $this->converterFactory->create($entityType);
foreach ($collection as $entity) {
if (!$this->acl->checkEntity($entity, Table::ACTION_EDIT)) {
continue;
}
if (!$entity instanceof CoreEntity) {
throw new RuntimeException("Only Core-Entity allowed.");
}
try {
$converter->convert($entity, $targetCurrency, $rates);
} catch (Forbidden $e) {
$this->log->info("Could not convert currency for {id}.", [
'id' => $entity->getId(),
'exception' => $e,
]);
}
$this->entityManager->saveEntity($entity, [SaveOption::MODIFIED_BY_ID => $this->user->getId()]);
$ids[] = $entity->getId();
$count++;
}
return new Result($count, $ids);
}
private function getRatesFromData(Data $data): ?CurrencyRates
{
if ($data->get('rates') === null) {
return null;
}
$baseCurrency = $this->configDataProvider->getBaseCurrency();
$ratesArray = get_object_vars($data->get('rates'));
$ratesArray[$baseCurrency] = 1.0;
return CurrencyRates::fromAssoc($ratesArray, $baseCurrency);
}
/**
* @throws Forbidden
*/
private function checkFieldAccess(string $entityType): void
{
/** @var string[] $requiredFieldList */
$requiredFieldList = $this->metadata->get(['scopes', $entityType, 'currencyConversionAccessRequiredFieldList']);
if ($requiredFieldList === null) {
return;
}
foreach ($requiredFieldList as $field) {
if (!$this->acl->checkField($entityType, $field, Table::ACTION_EDIT)) {
throw new Forbidden("No edit access to field `$field`.");
}
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction\Actions;
use Espo\Core\ORM\Repository\Option\RemoveOption;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\Core\Record\ActionHistory\Action;
use Espo\Core\Utils\Log;
use Espo\Entities\User;
use Espo\Core\Acl;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\MassAction\Data;
use Espo\Core\MassAction\MassAction;
use Espo\Core\MassAction\Params;
use Espo\Core\MassAction\QueryBuilder;
use Espo\Core\MassAction\Result;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Record\ServiceFactory;
use Exception;
class MassDelete implements MassAction
{
public function __construct(
private QueryBuilder $queryBuilder,
private Acl $acl,
private ServiceFactory $serviceFactory,
private EntityManager $entityManager,
private User $user,
private Log $log,
) {}
public function process(Params $params, Data $data): Result
{
$entityType = $params->getEntityType();
if (!$this->acl->check($entityType, Acl\Table::ACTION_DELETE)) {
throw new Forbidden("No delete access for '$entityType'.");
}
if (
!$params->hasIds() &&
$this->acl->getPermissionLevel(Acl\Permission::MASS_UPDATE) !== Acl\Table::LEVEL_YES
) {
throw new Forbidden("No mass-update permission.");
}
$service = $this->serviceFactory->createForUser($entityType, $this->user);
$repository = $this->entityManager->getRDBRepository($entityType);
$query = $this->queryBuilder->build($params);
$collection = $repository
->clone($query)
->sth()
->find();
$ids = [];
$count = 0;
foreach ($collection as $entity) {
if (!$this->acl->checkEntityDelete($entity)) {
continue;
}
try {
$repository->remove($entity, [
RemoveOption::MASS_REMOVE => true,
RemoveOption::MODIFIED_BY_ID => $this->user->getId(),
SaveOption::MASS_UPDATE => true, // Legacy.
]);
} catch (Exception $e) {
$this->log->info("Mass delete exception. Record: {id}.", [
'exception' => $e,
'id' => $entity->getId(),
]);
continue;
}
$id = $entity->getId();
$ids[] = $id;
$count++;
$service->processActionHistoryRecord(Action::DELETE, $entity);
}
return new Result($count, $ids);
}
}

View File

@@ -0,0 +1,116 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction\Actions;
use Espo\Tools\Stream\Service as StreamService;
use Espo\Core\Acl;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\MassAction\Data;
use Espo\Core\MassAction\MassAction;
use Espo\Core\MassAction\Params;
use Espo\Core\MassAction\QueryBuilder;
use Espo\Core\MassAction\Result;
use Espo\Core\ORM\EntityManager;
use Espo\Entities\User;
class MassFollow implements MassAction
{
private QueryBuilder $queryBuilder;
private Acl $acl;
private StreamService $streamService;
private EntityManager $entityManager;
private User $user;
public function __construct(
QueryBuilder $queryBuilder,
Acl $acl,
StreamService $streamService,
EntityManager $entityManager,
User $user
) {
$this->queryBuilder = $queryBuilder;
$this->acl = $acl;
$this->streamService = $streamService;
$this->entityManager = $entityManager;
$this->user = $user;
}
public function process(Params $params, Data $data): Result
{
$entityType = $params->getEntityType();
$passedUserId = $data->get('userId');
if ($passedUserId && !$this->user->isAdmin()) {
throw new Forbidden();
}
$userId = $passedUserId ?? $this->user->getId();
if (!$this->acl->check($entityType, Acl\Table::ACTION_STREAM)) {
throw new Forbidden("No stream access for '{$entityType}'.");
}
$query = $this->queryBuilder->build($params);
$collection = $this->entityManager
->getRDBRepository($entityType)
->clone($query)
->sth()
->find();
$ids = [];
$count = 0;
foreach ($collection as $entity) {
if (
!$this->acl->checkEntityStream($entity) ||
!$this->acl->checkEntityRead($entity)
) {
continue;
}
$followResult = $this->streamService->followEntity($entity, $userId);
if (!$followResult) {
continue;
}
/** @var string $id */
$id = $entity->getId();
$ids[] = $id;
$count++;
}
return new Result($count, $ids);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction\Actions;
use Espo\Core\ApplicationUser;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\MassAction\Data;
use Espo\Core\MassAction\MassAction;
use Espo\Core\MassAction\Params;
use Espo\Core\MassAction\QueryBuilder;
use Espo\Core\MassAction\Result;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\SystemUser;
use Espo\Entities\User;
class MassRecalculateFormula implements MassAction
{
public function __construct(
private QueryBuilder $queryBuilder,
private EntityManager $entityManager,
private User $user,
private SystemUser $systemUser
) {}
public function process(Params $params, Data $data): Result
{
if (!$this->user->isAdmin()) {
throw new Forbidden();
}
$entityType = $params->getEntityType();
$query = $this->queryBuilder->build($params);
$collection = $this->entityManager
->getRDBRepository($entityType)
->clone($query)
->sth()
->find();
$ids = [];
$count = 0;
foreach ($collection as $entity) {
$this->entityManager->saveEntity($entity, [
'modifiedById' => $this->systemUser->getId(),
]);
/** @var string $id */
$id = $entity->getId();
$ids[] = $id;
$count++;
}
return new Result($count, $ids);
}
}

View 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\Core\MassAction\Actions;
use Espo\Tools\Stream\Service as StreamService;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\MassAction\Data;
use Espo\Core\MassAction\MassAction;
use Espo\Core\MassAction\Params;
use Espo\Core\MassAction\QueryBuilder;
use Espo\Core\MassAction\Result;
use Espo\Core\ORM\EntityManager;
use Espo\Entities\User;
class MassUnfollow implements MassAction
{
public function __construct(
private QueryBuilder $queryBuilder,
private StreamService $streamService,
private EntityManager $entityManager,
private User $user
) {}
public function process(Params $params, Data $data): Result
{
$entityType = $params->getEntityType();
$passedUserId = $data->get('userId');
if ($passedUserId && !$this->user->isAdmin()) {
throw new Forbidden();
}
$userId = $passedUserId ?? $this->user->getId();
$query = $this->queryBuilder->build($params);
$collection = $this->entityManager
->getRDBRepository($entityType)
->clone($query)
->sth()
->find();
$ids = [];
$count = 0;
foreach ($collection as $entity) {
$this->streamService->unfollowEntity($entity, $userId);
/** @var string $id */
$id = $entity->getId();
$ids[] = $id;
$count++;
}
return new Result($count, $ids);
}
}

View File

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

View File

@@ -0,0 +1,56 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction\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\MassAction\Service;
class GetStatus implements Action
{
public function __construct(private Service $service)
{}
public function process(Request $request): Response
{
$id = $request->getRouteParam('id');
if (!$id) {
throw new BadRequest();
}
$result = $this->service->getStatusData($id);
return ResponseComposer::json($result);
}
}

View File

@@ -0,0 +1,151 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction\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\Core\MassAction\Params;
use Espo\Core\MassAction\Service;
use Espo\Core\MassAction\ServiceParams;
use Espo\Core\MassAction\ServiceResult;
use Espo\Core\Utils\Json;
use RuntimeException;
use stdClass;
/**
* Processes mass actions.
*/
class PostProcess implements Action
{
public function __construct(private Service $service)
{}
public function process(Request $request): Response
{
$body = $request->getParsedBody();
$entityType = $body->entityType ?? null;
$action = $body->action ?? null;
$params = $body->params ?? null;
$data = $body->data ?? (object) [];
$isIdle = $body->idle ?? false;
if (!$entityType || !$action || !$params) {
throw new BadRequest();
}
$rawParams = $this->prepareMassActionParams($params);
try {
$massActionParams = Params::fromRaw($rawParams, $entityType);
} catch (RuntimeException $e) {
throw new BadRequest($e->getMessage());
}
$serviceParams = ServiceParams::create($massActionParams)
->withIsIdle($isIdle);
$result = $this->service->process(
$entityType,
$action,
$serviceParams,
$data
);
$result = $this->convertResult($result);
return ResponseComposer::json($result);
}
/**
* @return array<string, mixed>
* @throws BadRequest
*/
private function prepareMassActionParams(stdClass $data): array
{
$where = $data->where ?? null;
$searchParams = $data->searchParams ?? $data->selectData ?? null;
$ids = $data->ids ?? null;
if (!is_null($where) || !is_null($searchParams)) {
$params = [];
if (!is_null($where)) {
$params['where'] = json_decode(Json::encode($where), true);
}
if (!is_null($searchParams)) {
$params['searchParams'] = json_decode(Json::encode($searchParams), true);
}
return $params;
}
if (is_null($ids)) {
throw new BadRequest("Bad search params for mass action.");
}
return ['ids' => $ids];
}
/**
* @throws Error
*/
private function convertResult(ServiceResult $serviceResult): stdClass
{
if (!$serviceResult->hasResult()) {
return (object) [
'id' => $serviceResult->getId(),
];
}
$result = $serviceResult->getResult();
if (!$result) {
throw new Error();
}
$data = (object) [];
if ($result->hasCount()) {
$data->count = $result->getCount();
}
if ($result->hasIds()) {
$data->ids = $result->getIds();
}
return $data;
}
}

View File

@@ -0,0 +1,56 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction\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\MassAction\Service;
class PostSubscribe implements Action
{
public function __construct(private Service $service)
{}
public function process(Request $request): Response
{
$id = $request->getRouteParam('id');
if (!$id) {
throw new BadRequest();
}
$this->service->subscribeToNotificationOnSuccess($id);
return ResponseComposer::json(true);
}
}

View File

@@ -0,0 +1,93 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction;
use Espo\Core\Utils\ObjectUtil;
use stdClass;
class Data
{
private stdClass $data;
private function __construct()
{
$this->data = (object) [];
}
public function getRaw(): stdClass
{
return ObjectUtil::clone($this->data);
}
/**
* Get an item value.
*
* @return mixed
*/
public function get(string $name)
{
return $this->getRaw()->$name ?? null;
}
/**
* Has an item.
*/
public function has(string $name): bool
{
return property_exists($this->data, $name);
}
public static function fromRaw(stdClass $data): self
{
$obj = new self();
$obj->data = $data;
return $obj;
}
/**
* Clone with an item value.
*
* @param mixed $value
*/
public function with(string $name, $value): self
{
$obj = clone $this;
$obj->data->$name = $value;
return $obj;
}
public function __clone()
{
$this->data = ObjectUtil::clone($this->data);
}
}

View File

@@ -0,0 +1,140 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction\Jobs;
use Espo\Core\Exceptions\Error;
use Espo\Core\Job\Job;
use Espo\Core\Job\Job\Data as JobData;
use Espo\Core\MassAction\MassActionFactory;
use Espo\Core\MassAction\Result;
use Espo\Core\Utils\Language;
use Espo\ORM\EntityManager;
use Espo\Entities\MassAction as MassActionEntity;
use Espo\Entities\Notification;
use Espo\Entities\User;
use Throwable;
class Process implements Job
{
public function __construct(
private EntityManager $entityManager,
private MassActionFactory $factory,
private Language $language
) {}
/**
* @throws Error
*/
public function run(JobData $data): void
{
$id = $data->getTargetId();
if ($id === null) {
throw new Error("ID not passed to the mass action job.");
}
/** @var MassActionEntity|null $entity */
$entity = $this->entityManager->getEntityById(MassActionEntity::ENTITY_TYPE, $id);
if ($entity === null) {
throw new Error("MassAction '$id' not found.");
}
/** @var User|null $user */
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $entity->getCreatedBy()->getId());
if (!$user) {
throw new Error("MassAction '$id', user not found.");
}
$params = $entity->getParams();
try {
$massAction = $this->factory->createForUser($entity->getAction(), $params->getEntityType(), $user);
$this->setRunning($entity);
$result = $massAction->process(
$params,
$entity->getData()
);
} catch (Throwable $e) {
$this->setFailed($entity);
throw new Error("Mass action job error: " . $e->getMessage());
}
$this->setSuccess($entity, $result);
$this->entityManager->refreshEntity($entity);
if ($entity->notifyOnFinish()) {
$this->notifyFinish($entity);
}
}
private function notifyFinish(MassActionEntity $entity): void
{
$notification = $this->entityManager->getRDBRepositoryByClass(Notification::class)->getNew();
$message = $this->language->translateLabel('massActionProcessed', 'messages');
$notification
->setType(Notification::TYPE_MESSAGE)
->setMessage($message)
->setUserId($entity->getCreatedBy()->getId());
$this->entityManager->saveEntity($notification);
}
private function setFailed(MassActionEntity $entity): void
{
$entity->setStatus(MassActionEntity::STATUS_FAILED);
$this->entityManager->saveEntity($entity);
}
private function setRunning(MassActionEntity $entity): void
{
$entity->setStatus(MassActionEntity::STATUS_RUNNING);
$this->entityManager->saveEntity($entity);
}
private function setSuccess(MassActionEntity $entity, Result $result): void
{
$entity
->setStatus(MassActionEntity::STATUS_SUCCESS)
->setProcessedCount($result->getCount());
$this->entityManager->saveEntity($entity);
}
}

View File

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

View File

@@ -0,0 +1,174 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction;
use Espo\Entities\User;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Utils\Metadata;
use Espo\Core\InjectableFactory;
use Espo\Core\AclManager;
use Espo\Core\Acl;
use Espo\Core\Binding\BindingContainerBuilder;
class MassActionFactory
{
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory,
private AclManager $aclManager
) {}
/**
* @throws Forbidden
* @throws NotFound
*/
public function create(string $action, string $entityType): MassAction
{
$className = $this->getClassName($action, $entityType);
if (!$className) {
throw new NotFound("Mass action '{$action}' not found.");
}
if ($this->isDisabled($action, $entityType)) {
throw new Forbidden("Mass action '{$action}' is disabled for '{$entityType}'.");
}
return $this->injectableFactory->create($className);
}
public function createForUser(string $action, string $entityType, User $user): MassAction
{
$className = $this->getClassName($action, $entityType);
if (!$className) {
throw new NotFound("Mass action '{$action}' not found.");
}
$bindingContainer = BindingContainerBuilder::create()
->bindInstance(User::class, $user)
->bindInstance(Acl::class, $this->aclManager->createUserAcl($user))
->build();
return $this->injectableFactory->createWithBinding($className, $bindingContainer);
}
/**
* @param array<string, mixed> $with
* @throws NotFound
* @todo Remove.
* @deprecated As of v7.1. Use create.
*/
public function createWith(string $action, string $entityType, array $with): MassAction
{
$className = $this->getClassName($action, $entityType);
if (!$className) {
throw new NotFound("Mass action '{$action}' not found.");
}
return $this->injectableFactory->createWith($className, $with);
}
/**
* @return ?class-string<MassAction>
*/
private function getClassName(string $action, string $entityType): ?string
{
/** @var ?class-string<MassAction> $className */
$className = $this->getEntityTypeClassName($action, $entityType);
if ($className) {
return $className;
}
/** @var ?class-string<MassAction> */
return $this->metadata->get(
['app', 'massActions', $action, 'implementationClassName']
);
}
/**
* @return ?class-string<MassAction>
*/
private function getEntityTypeClassName(string $action, string $entityType): ?string
{
/** @var ?class-string<MassAction> */
return $this->metadata->get(
['recordDefs', $entityType, 'massActions', $action, 'implementationClassName']
);
}
private function isDisabled(string $action, string $entityType): bool
{
$actionsDisabled = $this->metadata
->get(['recordDefs', $entityType, 'actionsDisabled']) ?? false;
if ($actionsDisabled) {
return true;
}
$massActionsDisabled = $this->metadata
->get(['recordDefs', $entityType, 'massActionsDisabled']) ?? false;
if ($massActionsDisabled) {
return true;
}
if ($this->needsToBeAllowed($entityType)) {
if (!$this->isAllowed($action, $entityType)) {
return true;
}
}
return $this->metadata
->get(['recordDefs', $entityType, 'massActions', $action, 'disabled']) ?? false;
}
private function needsToBeAllowed(string $entityType): bool
{
$isObject = $this->metadata->get(['scopes', $entityType, 'object']) ?? false;
if (!$isObject) {
return true;
}
return $this->metadata
->get(['recordDefs', $entityType, 'notAllowedActionsDisabled']) ?? false;
}
private function isAllowed(string $action, string $entityType): bool
{
return $this->metadata
->get(['recordDefs', $entityType, 'massActions', $action, 'allowed']) ?? false;
}
}

View File

@@ -0,0 +1,213 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction;
use Espo\Core\Select\SearchParams;
use RuntimeException;
/**
* Immutable.
*/
class Params
{
private string $entityType;
/** @var ?string[] */
private ?array $ids = null;
private ?SearchParams $searchParams = null;
private function __construct() {}
public function getEntityType(): string
{
return $this->entityType;
}
/**
* @return string[]
* @throws RuntimeException
*/
public function getIds(): array
{
if (!$this->ids) {
throw new RuntimeException("No IDs.");
}
return $this->ids;
}
public function getSearchParams(): SearchParams
{
if (!$this->searchParams) {
throw new RuntimeException("No search params.");
}
return $this->searchParams;
}
public function hasIds(): bool
{
return !is_null($this->ids);
}
/**
* @param string[] $ids
*/
public static function createWithIds(string $entityType, array $ids): self
{
return self::fromRaw([
'entityType' => $entityType,
'ids' => $ids,
]);
}
public static function createWithSearchParams(string $entityType, SearchParams $searchParams): self
{
$obj = new self();
$obj->entityType = $entityType;
$obj->searchParams = $searchParams;
return $obj;
}
/**
* Create from raw params.
*
* @param array{
* entityType?: string,
* where?: array<int, array<string, mixed>>,
* ids?: ?string[],
* searchParams?: ?array<string, mixed>,
* } $params
* @throws RuntimeException
*/
public static function fromRaw(array $params, ?string $entityType = null): self
{
/** @var array<string, mixed> $params */
$obj = new self();
$passedEntityType = $entityType ?? $params['entityType'] ?? null;
if (!$passedEntityType) {
throw new RuntimeException("No 'entityType'.");
}
$obj->entityType = $passedEntityType;
$where = $params['where'] ?? null;
$ids = $params['ids'] ?? null;
$searchParams = $params['searchParams'] ?? $params['selectData'] ?? null;
if ($where !== null && !is_array($where)) {
throw new RuntimeException("Bad 'where'.");
}
if ($searchParams !== null && !is_array($searchParams)) {
throw new RuntimeException("Bad 'searchParams'.");
}
if ($where !== null && $searchParams !== null) {
$searchParams['where'] = $where;
}
if ($where !== null && $searchParams === null) {
$searchParams = [
'where' => $where,
];
}
if ($searchParams !== null) {
if ($ids !== null) {
throw new RuntimeException("Can't combine 'ids' and search params.");
}
} else if ($ids !== null) {
if (!is_array($ids)) {
throw new RuntimeException("Bad 'ids'.");
}
$obj->ids = $ids;
} else {
throw new RuntimeException("Bad mass action params.");
}
if ($searchParams !== null) {
$actualSearchParams = $searchParams;
unset($actualSearchParams['select']);
$obj->searchParams = SearchParams::fromRaw($actualSearchParams);
}
return $obj;
}
/**
* @return void
*/
public function __clone()
{
if ($this->searchParams) {
$this->searchParams = clone $this->searchParams;
}
}
/**
* @return array{
* entityType: string,
* ids: ?string[],
* searchParams: string,
* }
*/
public function __serialize(): array
{
return [
'entityType' => $this->entityType,
'ids' => $this->ids,
'searchParams' => serialize($this->searchParams),
];
}
/**
* @param array{
* entityType: string,
* ids: ?string[],
* searchParams: string,
* } $data
*/
public function __unserialize(array $data): void
{
$this->entityType = $data['entityType'];
$this->ids = $data['ids'];
$this->searchParams = unserialize($data['searchParams']);
}
}

View File

@@ -0,0 +1,68 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\ORM\Query\Select;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Entities\User;
class QueryBuilder
{
public function __construct(private SelectBuilderFactory $selectBuilderFactory, private User $user)
{}
/**
* @throws BadRequest
* @throws Forbidden
*/
public function build(Params $params): Select
{
$builder = $this->selectBuilderFactory
->create()
->from($params->getEntityType())
->forUser($this->user)
->withStrictAccessControl();
if ($params->hasIds()) {
return $builder
->buildQueryBuilder()
->where([
'id' => $params->getIds(),
])
->build();
}
return $builder
->withSearchParams($params->getSearchParams())
->build();
}
}

View File

@@ -0,0 +1,104 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction;
use RuntimeException;
/**
* Immutable.
*/
class Result
{
private ?int $count = null;
/** @var ?string[] */
private $ids = null;
/**
* @param ?string[] $ids
*/
public function __construct(?int $count, ?array $ids = null)
{
$this->count = $count;
$this->ids = $ids;
}
public function hasIds(): bool
{
return $this->ids !== null;
}
public function hasCount(): bool
{
return $this->count !== null;
}
/**
* @return string[]
*/
public function getIds(): array
{
if (!$this->hasIds()) {
throw new RuntimeException("No IDs.");
}
/** @var string[] */
return $this->ids;
}
public function getCount(): int
{
if (!$this->hasCount()) {
throw new RuntimeException("No count.");
}
/** @var int */
return $this->count;
}
public function withNoIds(): self
{
return new self($this->count);
}
/**
* @deprecated
* @param array{
* count?: ?int,
* ids?: ?string[],
* } $data
*/
public static function fromArray(array $data): self
{
return new self(
$data['count'] ?? null,
$data['ids'] ?? null
);
}
}

View File

@@ -0,0 +1,167 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\MassAction;
use Espo\Core\Exceptions\BadRequest;
use Espo\ORM\EntityManager;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Acl;
use Espo\Core\MassAction\Jobs\Process;
use Espo\Core\Job\JobSchedulerFactory;
use Espo\Core\Job\Job\Data as JobData;
use Espo\Entities\User;
use Espo\Entities\MassAction as MassActionEntity;
use stdClass;
class Service
{
public function __construct(
private MassActionFactory $factory,
private Acl $acl,
private JobSchedulerFactory $jobSchedulerFactory,
private EntityManager $entityManager,
private User $user
) {}
/**
* Perform a mass action.
*
* @throws Forbidden
* @throws NotFound
* @throws BadRequest
*/
public function process(
string $entityType,
string $action,
ServiceParams $serviceParams,
stdClass $data
): ServiceResult {
if (!$this->acl->checkScope($entityType)) {
throw new ForbiddenSilent();
}
$params = $serviceParams->getParams();
if ($serviceParams->isIdle()) {
if ($this->user->isPortal()) {
throw new Forbidden("Idle mass actions are not allowed for portal users.");
}
return $this->schedule($entityType, $action, $params, $data);
}
$massAction = $this->factory->create($action, $entityType);
$result = $massAction->process(
$params,
Data::fromRaw($data)
);
if ($params->hasIds()) {
return ServiceResult::createWithResult($result);
}
return ServiceResult::createWithResult(
$result->withNoIds()
);
}
/**
* @throws Forbidden
* @throws NotFound
*/
public function getStatusData(string $id): stdClass
{
/** @var ?MassActionEntity $entity */
$entity = $this->entityManager->getEntityById(MassActionEntity::ENTITY_TYPE, $id);
if (!$entity) {
throw new NotFound();
}
if ($entity->getCreatedBy()->getId() !== $this->user->getId()) {
throw new Forbidden();
}
return (object) [
'status' => $entity->getStatus(),
'processedCount' => $entity->getProcessedCount(),
];
}
/**
* @throws Forbidden
* @throws NotFound
*/
public function subscribeToNotificationOnSuccess(string $id): void
{
/** @var ?MassActionEntity $entity */
$entity = $this->entityManager->getEntityById(MassActionEntity::ENTITY_TYPE, $id);
if (!$entity) {
throw new NotFound();
}
if ($entity->getCreatedBy()->getId() !== $this->user->getId()) {
throw new Forbidden();
}
$entity->setNotifyOnFinish();
$this->entityManager->saveEntity($entity);
}
private function schedule(string $entityType, string $action, Params $params, stdClass $data): ServiceResult
{
$entity = $this->entityManager->createEntity(MassActionEntity::ENTITY_TYPE, [
'entityType' => $entityType,
'action' => $action,
// Additional encoding to handle null-character issue in PostgreSQL.
'params' => base64_encode(serialize($params)),
'data' => $data,
]);
$this->jobSchedulerFactory
->create()
->setClassName(Process::class)
->setData(
JobData::create()
->withTargetId($entity->getId())
->withTargetType($entity->getEntityType())
)
->schedule();
return ServiceResult::createWithId($entity->getId());
}
}

View File

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

View 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\Core\MassAction;
/**
* Immutable.
*/
class ServiceResult
{
private ?Result $result = null;
private ?string $id = null;
private function __construct() {}
public function hasResult(): bool
{
return $this->result !== null;
}
public function getId(): ?string
{
return $this->id;
}
public function getResult(): ?Result
{
return $this->result;
}
public static function createWithId(string $id): self
{
$obj = new self;
$obj->id = $id;
return $obj;
}
public static function createWithResult(Result $result): self
{
$obj = new self;
$obj->result = $result;
return $obj;
}
}