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,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\Export;
use Espo\ORM\Entity;
interface AdditionalFieldsLoader
{
/**
* @param string[] $fieldList
*/
public function load(Entity $entity, array $fieldList): void;
}

View 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\Export;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use LogicException;
class AdditionalFieldsLoaderFactory
{
public function __construct(
private InjectableFactory $injectableFactory,
private Metadata $metadata
) {}
public function create(string $format): AdditionalFieldsLoader
{
$className = $this->getClassName($format);
if (!$className) {
throw new LogicException();
}
return $this->injectableFactory->create($className);
}
public function isCreatable(string $format): bool
{
return (bool) $this->getClassName($format);
}
/**
* @return ?class-string<AdditionalFieldsLoader>
*/
private function getClassName(string $format): ?string
{
return $this->metadata->get(['app', 'export', 'formatDefs', $format, 'additionalFieldsLoaderClassName']);
}
}

View 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\Export\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\Export\Service;
/**
* Export status.
*/
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,126 @@
<?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\Export\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\Utils\Json;
use Espo\Tools\Export\Params;
use Espo\Tools\Export\Service;
use Espo\Tools\Export\ServiceParams;
use stdClass;
class PostProcess implements Action
{
public function __construct(private Service $service)
{}
public function process(Request $request): Response
{
$params = $this->fetchRawParamsFromRequest($request);
$serviceParams = ServiceParams::create()
->withIsIdle($request->getParsedBody()->idle ?? false);
$result = $this->service->process($params, $serviceParams);
if ($result->hasResult()) {
$subResult = $result->getResult();
assert($subResult !== null);
return ResponseComposer::json([
'id' => $subResult->getAttachmentId()
]);
}
return ResponseComposer::json([
'exportId' => $result->getId()
]);
}
/**
* @throws BadRequest
*/
private function fetchRawParamsFromRequest(Request $request): Params
{
$data = $request->getParsedBody();
$entityType = $data->entityType ?? null;
if (!$entityType) {
throw new BadRequest("No entityType.");
}
$params['entityType'] = $entityType;
$where = $data->where ?? null;
$searchParams = $data->searchParams ?? $data->selectData ?? null;
$ids = $data->ids ?? null;
if (!is_null($where) || !is_null($searchParams)) {
if (!is_null($where)) {
$params['where'] = json_decode(Json::encode($where), true);
}
if (!is_null($searchParams)) {
$params['searchParams'] = json_decode(Json::encode($searchParams), true);
}
} else if (!is_null($ids)) {
$params['ids'] = $ids;
}
if (isset($data->attributeList)) {
$params['attributeList'] = $data->attributeList;
}
if (isset($data->fieldList)) {
$params['fieldList'] = $data->fieldList;
}
if (isset($data->format)) {
$params['format'] = $data->format;
}
$obj = Params::fromRaw($params);
if (isset($data->params) && $data->params instanceof stdClass) {
foreach (get_object_vars($data->params) as $key => $value) {
$obj = $obj->withParam($key, $value);
}
}
return $obj;
}
}

View 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\Export\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\Export\Service;
/**
* Subscribes to a notification on export success.
*/
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,96 @@
<?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\Export;
use Espo\Core\FieldProcessing\ListLoadProcessor;
use Espo\Core\FieldProcessing\Loader\Params as LoaderParams;
use Espo\Core\Record\Service as RecordService;
use Espo\ORM\Collection as OrmCollection;
use Espo\ORM\Entity;
use Espo\Tools\Export\Processor\Params as ProcessorParams;
use IteratorAggregate;
use Traversable;
/**
* A lazy-iterable collection of entities.
*
* @implements IteratorAggregate<int, Entity>
*/
class Collection implements IteratorAggregate
{
/**
* @param OrmCollection<Entity> $collection
* @param RecordService<Entity> $recordService
*/
public function __construct(
private OrmCollection $collection,
private ListLoadProcessor $listLoadProcessor,
private LoaderParams $loaderParams,
private ?AdditionalFieldsLoader $additionalFieldsLoader,
private RecordService $recordService,
private ProcessorParams $processorParams
) {}
public function getIterator(): Traversable
{
return (function () {
foreach ($this->collection as $entity) {
$this->prepareEntity($entity);
yield $entity;
}
})();
}
private function prepareEntity(Entity $entity): void
{
$this->listLoadProcessor->process($entity, $this->loaderParams);
/** For bc. */
if (method_exists($this->recordService, 'loadAdditionalFieldsForExport')) {
$this->recordService->loadAdditionalFieldsForExport($entity);
}
if ($this->additionalFieldsLoader && $this->processorParams->getFieldList()) {
$this->additionalFieldsLoader->load($entity, $this->processorParams->getFieldList());
}
foreach ($entity->getAttributeList() as $attribute) {
$this->prepareEntityValue($entity, $attribute);
}
}
private function prepareEntityValue(Entity $entity, string $attribute): void
{
if (!in_array($attribute, $this->processorParams->getAttributeList())) {
$entity->clear($attribute);
}
}
}

View File

@@ -0,0 +1,459 @@
<?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\Export;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\Core\Record\Select\ApplierClassNameListProvider;
use Espo\ORM\Defs\Params\AttributeParam as OrmAttributeParam;
use Espo\ORM\Name\Attribute;
use Espo\Tools\Export\Collection as ExportCollection;
use Espo\Tools\Export\Processor\Params as ProcessorParams;
use Espo\ORM\Entity;
use Espo\ORM\BaseEntity;
use Espo\Entities\User;
use Espo\Entities\Attachment;
use Espo\Core\Acl;
use Espo\Core\Acl\GlobalRestriction;
use Espo\Core\FieldProcessing\ListLoadProcessor;
use Espo\Core\FieldProcessing\Loader\Params as LoaderParams;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Core\Record\ServiceContainer;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Core\Utils\FieldUtil;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Collection;
use Espo\ORM\EntityManager;
use RuntimeException;
use LogicException;
class Export
{
private const DEFAULT_FORMAT = 'csv';
/** @var ?Params */
private ?Params $params = null;
/** @var ?Collection<Entity> */
private ?Collection $collection = null;
public function __construct(
private ProcessorFactory $processorFactory,
private ProcessorParamsHandlerFactory $processorParamsHandlerFactory,
private AdditionalFieldsLoaderFactory $additionalFieldsLoaderFactory,
private SelectBuilderFactory $selectBuilderFactory,
private ServiceContainer $serviceContainer,
private Acl $acl,
private EntityManager $entityManager,
private Metadata $metadata,
private FileStorageManager $fileStorageManager,
private ListLoadProcessor $listLoadProcessor,
private FieldUtil $fieldUtil,
private User $user,
private ApplierClassNameListProvider $applierClassNameListProvider
) {}
public function setParams(Params $params): self
{
$this->params = $params;
return $this;
}
/**
* @param Collection<Entity> $collection
*/
public function setCollection(Collection $collection): self
{
$this->collection = $collection;
return $this;
}
/**
* Run export.
*/
public function run(): Result
{
if (!$this->params) {
throw new LogicException("No params set.");
}
$params = $this->params;
$entityType = $params->getEntityType();
$format = $params->getFormat() ?? self::DEFAULT_FORMAT;
$collection = $this->getCollection($params);
$processor = $this->processorFactory->create($format);
$processorParams = $this->createProcessorParams($params)
->withAttributeList($this->getAttributeList($params))
->withFieldList($this->getFieldList($params));
if ($this->processorParamsHandlerFactory->isCreatable($format)) {
$processorParams = $this->processorParamsHandlerFactory
->create($format)
->handle($params, $processorParams);
}
$loaderParams = LoaderParams::create()
->withSelect($processorParams->getAttributeList());
$recordService = $this->serviceContainer->get($entityType);
$loader = $this->additionalFieldsLoaderFactory->isCreatable($format) ?
$this->additionalFieldsLoaderFactory->create($format) : null;
$exportCollection = new ExportCollection(
collection: $collection,
listLoadProcessor: $this->listLoadProcessor,
loaderParams: $loaderParams,
additionalFieldsLoader: $loader,
recordService: $recordService,
processorParams: $processorParams
) ;
$stream = $processor->process($processorParams, $exportCollection);
$mimeType = $this->metadata->get(['app', 'export', 'formatDefs', $format, 'mimeType']);
/** @var Attachment $attachment */
$attachment = $this->entityManager->getRepositoryByClass(Attachment::class)->getNew();
$attachment
->setName($processorParams->getFileName())
->setRole(Attachment::ROLE_EXPORT_FILE)
->setType($mimeType)
->setSize($stream->getSize());
$this->entityManager->saveEntity($attachment, [
SaveOption::CREATED_BY_ID => $this->user->getId(),
]);
$this->fileStorageManager->putStream($attachment, $stream);
return new Result($attachment->getId());
}
private function createProcessorParams(Params $params): ProcessorParams
{
$fileName = $params->getFileName();
$format = $params->getFormat() ?? self::DEFAULT_FORMAT;
$entityType = $params->getEntityType();
$attributeList = $params->getAttributeList() ?? [];
$fieldList = $params->getFieldList();
$fileExtension = $this->metadata->get(['app', 'export', 'formatDefs', $format, 'fileExtension']);
if ($fileName !== null) {
$fileName = trim($fileName);
}
$fileName = $fileName ?
$fileName . '.' . $fileExtension :
"Export_$entityType.$fileExtension";
$processorParams = (new ProcessorParams($fileName, $attributeList, $fieldList))
->withName($params->getName())
->withEntityType($params->getEntityType());
foreach ($params->getParamList() as $n) {
$processorParams = $processorParams->withParam($n, $params->getParam($n));
}
return $processorParams;
}
private function getForeignAttributeType(Entity $entity, string $attribute): ?string
{
$defs = $this->entityManager->getDefs();
$entityDefs = $defs->getEntity($entity->getEntityType());
[$relation, $foreign] = str_contains($attribute, '_') ?
explode('_', $attribute) :
[
$this->getAttributeParam($entity, $attribute, OrmAttributeParam::RELATION),
$this->getAttributeParam($entity, $attribute, 'foreign')
];
if (!$relation) {
return null;
}
if (!$foreign) {
return null;
}
if (!is_string($foreign)) {
return Entity::VARCHAR;
}
if (!$entityDefs->hasRelation($relation)) {
return null;
}
if (!$entityDefs->getRelation($relation)->hasForeignEntityType()) {
return null;
}
$entityType = $entityDefs->getRelation($relation)->getForeignEntityType();
if (!$defs->hasEntity($entityType)) {
return null;
}
$foreignEntityDefs = $defs->getEntity($entityType);
if (!$foreignEntityDefs->hasAttribute($foreign)) {
return null;
}
return $foreignEntityDefs->getAttribute($foreign)->getType();
}
private function checkAttributeIsAllowedForExport(
Entity $entity,
string $attribute,
bool $exportAllFields = false
): bool {
$type = $entity->getAttributeType($attribute);
if ($type === Entity::FOREIGN || str_contains($attribute, '_')) {
$type = $this->getForeignAttributeType($entity, $attribute) ?? $type;
}
if ($type === Entity::PASSWORD) {
return false;
}
if ($this->getAttributeParam($entity, $attribute, AttributeParam::NOT_EXPORTABLE)) {
return false;
}
if (!$exportAllFields) {
return true;
}
if ($this->getAttributeParam($entity, $attribute, AttributeParam::IS_LINK_MULTIPLE_ID_LIST)) {
return false;
}
if ($this->getAttributeParam($entity, $attribute, AttributeParam::IS_LINK_MULTIPLE_NAME_MAP)) {
return false;
}
// Revise.
if ($this->getAttributeParam($entity, $attribute, 'isLinkStub')) {
return false;
}
return true;
}
/**
* @return Collection<Entity>
*/
private function getCollection(Params $params): Collection
{
if ($this->collection) {
return $this->collection;
}
$entityType = $params->getEntityType();
$searchParams = $params->getSearchParams();
$builder = $this->selectBuilderFactory
->create()
->forUser($this->user)
->from($entityType)
->withAdditionalApplierClassNameList(
$this->applierClassNameListProvider->get($entityType)
)
->withSearchParams($searchParams);
if ($params->applyAccessControl()) {
$builder->withStrictAccessControl();
}
$query = $builder->build();
/** @var Collection<Entity> */
return $this->entityManager
->getRDBRepository($entityType)
->clone($query)
->sth()
->find();
}
/**
* @return string[]
*/
private function getAttributeList(Params $params): array
{
$list = [];
$entityType = $params->getEntityType();
$entityDefs = $this->entityManager
->getDefs()
->getEntity($entityType);
$attributeListToSkip = $params->applyAccessControl() ?
$this->acl->getScopeForbiddenAttributeList($entityType) :
$this->acl->getScopeRestrictedAttributeList($entityType, [
GlobalRestriction::TYPE_FORBIDDEN,
GlobalRestriction::TYPE_INTERNAL,
]);
$attributeListToSkip[] = Attribute::DELETED;
$initialAttributeList = $params->getAttributeList();
if (
$params->getAttributeList() === null &&
$params->getFieldList() !== null
) {
$initialAttributeList = $this->getAttributeListFromFieldList($params);
}
if (
$params->getAttributeList() === null &&
$params->getFieldList() === null
) {
$initialAttributeList = $entityDefs->getAttributeNameList();
}
assert($initialAttributeList !== null);
$seed = $this->entityManager->getNewEntity($entityType);
foreach ($initialAttributeList as $attribute) {
if (in_array($attribute, $attributeListToSkip)) {
continue;
}
if (!$this->checkAttributeIsAllowedForExport($seed, $attribute, $params->allFields())) {
continue;
}
$list[] = $attribute;
}
return $list;
}
/**
* @return string[]
* @throws RuntimeException
*/
private function getAttributeListFromFieldList(Params $params): array
{
$entityType = $params->getEntityType();
$fieldList = $params->getFieldList();
if ($fieldList === null) {
throw new RuntimeException();
}
$attributeList = [];
foreach ($fieldList as $field) {
$attributeList = array_merge(
$attributeList,
$this->fieldUtil->getAttributeList($entityType, $field)
);
}
return $attributeList;
}
/**
* @return ?string[]
*/
private function getFieldList(Params $params): ?array
{
$entityDefs = $this->entityManager
->getDefs()
->getEntity($params->getEntityType());
$fieldList = $params->getFieldList();
if ($params->allFields()) {
$fieldList = $entityDefs->getFieldNameList();
array_unshift($fieldList, 'id');
}
if ($fieldList === null) {
return null;
}
foreach ($fieldList as $i => $field) {
if ($field === 'id') {
continue;
}
if (!$entityDefs->hasField($field)) {
continue;
}
if ($entityDefs->getField($field)->getParam('exportDisabled')) {
unset($fieldList[$i]);
}
}
$fieldList = array_filter($fieldList, function ($item) use ($params) {
return $this->acl->checkField($params->getEntityType(), $item);
});
return array_values($fieldList);
}
private function getAttributeParam(Entity $entity, string $attribute, string $param): mixed
{
if ($entity instanceof BaseEntity) {
return $entity->getAttributeParam($attribute, $param);
}
$entityDefs = $this->entityManager
->getDefs()
->getEntity($entity->getEntityType());
if (!$entityDefs->hasAttribute($attribute)) {
return null;
}
return $entityDefs->getAttribute($attribute)->getParam($param);
}
}

View File

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

View File

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

View 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\Export\Format;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\Tools\Export\Format\Xlsx\CellValuePreparators\General;
class CellValuePreparatorFactory
{
public function __construct(
private InjectableFactory $injectableFactory,
private Metadata $metadata
) {}
public function create(string $format, string $fieldType): CellValuePreparator
{
/** @var class-string<CellValuePreparator> $className */
$className = $this->metadata
->get(['app', 'export', 'formatDefs', $format, 'cellValuePreparatorClassNameMap', $fieldType]) ??
General::class;
return $this->injectableFactory->create($className);
}
}

View 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\Export\Format\Csv;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Entity;
use Espo\Tools\Export\AdditionalFieldsLoader as AdditionalFieldsLoaderInterface;
/**
* @noinspection PhpUnused
*/
class AdditionalFieldsLoader implements AdditionalFieldsLoaderInterface
{
public function __construct(private Metadata $metadata) {}
public function load(Entity $entity, array $fieldList): void
{
if (!$entity instanceof CoreEntity) {
return;
}
foreach ($fieldList as $field) {
$fieldType = $this->metadata
->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']);
if (
$fieldType === FieldType::LINK_MULTIPLE ||
$fieldType === FieldType::ATTACHMENT_MULTIPLE
) {
if (!$entity->has($field . 'Ids') && $entity->hasLinkMultipleField($field)) {
$entity->loadLinkMultipleField($field);
}
}
}
}
}

View File

@@ -0,0 +1,123 @@
<?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\Export\Format\Csv;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Json;
use Espo\Entities\Preferences;
use Espo\ORM\Entity;
use Espo\Tools\Export\Collection;
use Espo\Tools\Export\Processor as ProcessorInterface;
use Espo\Tools\Export\Processor\Params;
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\Stream;
use RuntimeException;
use const JSON_UNESCAPED_UNICODE;
class Processor implements ProcessorInterface
{
public function __construct(
private Config $config,
private Preferences $preferences
) {}
public function process(Params $params, Collection $collection): StreamInterface
{
$attributeList = $params->getAttributeList();
$delimiterRaw =
$this->preferences->get('exportDelimiter') ??
$this->config->get('exportDelimiter') ??
',';
$delimiter = str_replace('\t', "\t", $delimiterRaw);
$fp = fopen('php://temp', 'w');
if ($fp === false) {
throw new RuntimeException("Could not open temp.");
}
fputcsv($fp, $attributeList, $delimiter);
foreach ($collection as $entity) {
$preparedRow = $this->prepareRow($entity, $attributeList);
fputcsv($fp, $preparedRow, $delimiter, '"' , "\0");
}
rewind($fp);
return new Stream($fp);
}
/**
* @param string[] $attributeList
* @return string[]
*/
private function prepareRow(Entity $entity, array $attributeList): array
{
$preparedRow = [];
foreach ($attributeList as $attribute) {
$value = $entity->get($attribute);
if (is_array($value) || is_object($value)) {
$value = Json::encode($value, JSON_UNESCAPED_UNICODE);
}
$value = (string) $value;
$preparedRow[] = $this->sanitizeCellValue($value);
}
return $preparedRow;
}
private function sanitizeCellValue(string $value): string
{
if ($value === '') {
return $value;
}
if (is_numeric($value)) {
return $value;
}
if (in_array($value[0], ['+', '-', '@', '='])) {
return "'" . $value;
}
return $value;
}
}

View 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\Export\Format\Xlsx;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Entity;
use Espo\Tools\Export\AdditionalFieldsLoader as AdditionalFieldsLoaderInterface;
/**
* @noinspection PhpUnused
*/
class AdditionalFieldsLoader implements AdditionalFieldsLoaderInterface
{
public function __construct(private Metadata $metadata)
{}
public function load(Entity $entity, array $fieldList): void
{
if (!$entity instanceof CoreEntity) {
return;
}
foreach ($entity->getRelationList() as $link) {
if (!in_array($link, $fieldList)) {
continue;
}
if ($entity->getRelationType($link) === Entity::BELONGS_TO_PARENT) {
if (!$entity->get($link . 'Name')) {
$entity->loadParentNameField($link);
}
} else if (
(
(
$entity->getRelationType($link) === Entity::BELONGS_TO &&
$entity->getRelationParam($link, RelationParam::NO_JOIN)
) ||
$entity->getRelationType($link) === Entity::HAS_ONE
) &&
$entity->hasAttribute($link . 'Name')
) {
if (!$entity->get($link . 'Name') || !$entity->get($link . 'Id')) {
$entity->loadLinkField($link);
}
}
}
foreach ($fieldList as $field) {
$fieldType = $this->metadata
->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']);
if ($fieldType === FieldType::LINK_MULTIPLE || $fieldType === FieldType::ATTACHMENT_MULTIPLE) {
if (!$entity->has($field . 'Ids') && $entity->hasLinkMultipleField($field)) {
$entity->loadLinkMultipleField($field);
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Tools\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Field\Address\AddressFactory;
use Espo\Core\Field\Address\AddressFormatterFactory;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class Address implements CellValuePreparator
{
public function __construct(
private AddressFormatterFactory $formatterFactory
) {}
public function prepare(Entity $entity, string $name): ?string
{
$address = (new AddressFactory())->createFromEntity($entity, $name);
$formatter = $this->formatterFactory->createDefault();
return $formatter->format($address) ?: null;
}
}

View 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\Export\Format\Xlsx\CellValuePreparators;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class Boolean implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): bool
{
return (bool) $entity->get($name);
}
}

View 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\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Field\Currency as CurrencyValue;
use Espo\Core\Field\Currency\CurrencyFactory;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class Currency implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): ?CurrencyValue
{
$factory = new CurrencyFactory();
if (!$factory->isCreatableFromEntity($entity, $name)) {
return null;
}
return $factory->createFromEntity($entity, $name);
}
}

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\Tools\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Field\Currency as CurrencyValue;
use Espo\Core\Utils\Config;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class CurrencyConverted implements CellValuePreparator
{
private string $code;
public function __construct(Config $config)
{
$this->code = $config->get('defaultCurrency');
}
public function prepare(Entity $entity, string $name): ?CurrencyValue
{
$value = $entity->get($name);
if ($value === null) {
return null;
}
return CurrencyValue::create($value, $this->code);
}
}

View 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\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Field\Date as DateValue;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class Date implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): ?DateValue
{
$value = $entity->get($name);
if (!$value) {
return null;
}
return DateValue::fromString($value);
}
}

View File

@@ -0,0 +1,61 @@
<?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\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Field\DateTime as DateTimeValue;
use Espo\Core\Utils\Config;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
use DateTimeZone;
class DateTime implements CellValuePreparator
{
private string $timezone;
public function __construct(Config\ApplicationConfig $applicationConfig)
{
$this->timezone = $applicationConfig->getTimeZone();
}
public function prepare(Entity $entity, string $name): ?DateTimeValue
{
$value = $entity->get($name);
if (!$value) {
return null;
}
return DateTimeValue::fromString($value)
->withTimezone(
new DateTimeZone($this->timezone)
);
}
}

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\Tools\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Field\DateTime as DateTimeValue;
use Espo\Core\Field\Date as DateValue;
use Espo\Core\Utils\Config;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
use DateTimeZone;
class DateTimeOptional implements CellValuePreparator
{
private string $timezone;
public function __construct(Config\ApplicationConfig $applicationConfig)
{
$this->timezone = $applicationConfig->getTimeZone();
}
public function prepare(Entity $entity, string $name): DateTimeValue|DateValue|null
{
$dateValue = $entity->get($name . 'Date');
if ($dateValue !== null) {
return DateValue::fromString($dateValue);
}
$value = $entity->get($name);
if (!$value) {
return null;
}
return DateTimeValue::fromString($value)
->withTimezone(
new DateTimeZone($this->timezone)
);
}
}

View File

@@ -0,0 +1,81 @@
<?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\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Utils\Language;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class Duration implements CellValuePreparator
{
public function __construct(private Language $language)
{}
public function prepare(Entity $entity, string $name): ?string
{
$value = $entity->get($name);
if (!$value) {
return null;
}
$seconds = intval($value);
$days = intval(floor($seconds / 86400));
$seconds = $seconds - $days * 86400;
$hours = intval(floor($seconds / 3600));
$seconds = $seconds - $hours * 3600;
$minutes = intval(floor($seconds / 60));
$value = '';
if ($days) {
$value .= $days . $this->language->translateLabel('d', 'durationUnits');
if ($minutes || $hours) {
$value .= ' ';
}
}
if ($hours) {
$value .= $hours . $this->language->translateLabel('h', 'durationUnits');
if ($minutes) {
$value .= ' ';
}
}
if ($minutes) {
$value .= $minutes . $this->language->translateLabel('m', 'durationUnits');
}
return $value;
}
}

View File

@@ -0,0 +1,80 @@
<?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\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Utils\Language;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
use Espo\Tools\Export\Format\Xlsx\FieldHelper;
class Enumeration implements CellValuePreparator
{
public function __construct(
private Defs $ormDefs,
private Language $language,
private FieldHelper $fieldHelper
) {}
public function prepare(Entity $entity, string $name): ?string
{
if (!$entity->has($name)) {
return null;
}
$value = $entity->get($name);
$fieldData = $this->fieldHelper->getData($entity->getEntityType(), $name);
if (!$fieldData) {
return $value;
}
$entityType = $fieldData->getEntityType();
$field = $fieldData->getField();
$translation = $this->ormDefs
->getEntity($entityType)
->getField($field)
->getParam('translation');
if (!$translation) {
return $this->language->translateOption($value, $field, $entityType);
}
$map = $this->language->get($translation);
if (!is_array($map)) {
return $value;
}
return $map[$value] ?? $value;
}
}

View 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\Export\Format\Xlsx\CellValuePreparators;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class Floating implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): float
{
return $entity->get($name) ?? 0.0;
}
}

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\Tools\Export\Format\Xlsx\CellValuePreparators;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class General implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): string|bool|int|float|null
{
$value = $entity->get($name);
if ($value === null) {
return null;
}
if (
!is_string($value) &&
!is_int($value) &&
!is_float($value) &&
!is_bool($value)
) {
return null;
}
return $value;
}
}

View 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\Export\Format\Xlsx\CellValuePreparators;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class Integer implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): int
{
/** @var int */
return $entity->get($name) ?? 0;
}
}

View 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\Export\Format\Xlsx\CellValuePreparators;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class Link implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): ?string
{
/** @var ?string */
return $entity->get($name . 'Name');
}
}

View 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\Export\Format\Xlsx\CellValuePreparators;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
use stdClass;
class LinkMultiple implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): ?string
{
if (
!$entity->has($name . 'Ids') ||
!$entity->has($name . 'Names')
) {
return null;
}
/** @var string[] $ids */
$ids = $entity->get($name . 'Ids');
/** @var ?stdClass $names */
$names = $entity->get($name . 'Names');
$nameList = array_map(function ($id) use ($names) {
return $names->$id ?? $id;
}, $ids);
return implode(',', $nameList);
}
}

View File

@@ -0,0 +1,108 @@
<?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\Export\Format\Xlsx\CellValuePreparators;
use Espo\Core\Utils\Language;
use Espo\ORM\Defs;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
use Espo\Tools\Export\Format\Xlsx\FieldHelper;
class MultiEnum implements CellValuePreparator
{
public function __construct(
private Defs $ormDefs,
private Language $language,
private FieldHelper $fieldHelper
) {}
public function prepare(Entity $entity, string $name): ?string
{
if (!$entity->has($name)) {
return null;
}
$list = $entity->get($name);
if (!is_array($list)) {
return null;
}
/** @var string[] $list */
$fieldData = $this->fieldHelper->getData($entity->getEntityType(), $name);
if (!$fieldData) {
return $this->joinList($list);
}
$entityType = $fieldData->getEntityType();
$field = $fieldData->getField();
$translation = $this->ormDefs
->getEntity($entityType)
->getField($field)
->getParam('translation');
if (!$translation) {
return $this->joinList(
array_map(
function ($item) use ($field, $entityType) {
return $this->language->translateOption($item, $field, $entityType);
},
$list
)
);
}
$map = $this->language->get($translation);
if (!is_array($map)) {
return $this->joinList($list);
}
return $this->joinList(
array_map(
function ($item) use ($map) {
return $map[$item] ?? $item;
},
$list
)
);
}
/**
* @param string[] $list
*/
private function joinList(array $list): string
{
return implode(', ', $list);
}
}

View File

@@ -0,0 +1,60 @@
<?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\Export\Format\Xlsx\CellValuePreparators;
use Espo\ORM\Entity;
use Espo\Tools\Export\Format\CellValuePreparator;
class PersonName implements CellValuePreparator
{
public function prepare(Entity $entity, string $name): ?string
{
$value = $entity->get($name);
if ($value) {
return $value;
}
$arr = [];
$firstName = $entity->get('first' . ucfirst($name));
$lastName = $entity->get('last' . ucfirst($name));
if ($firstName) {
$arr[] = $firstName;
}
if ($lastName) {
$arr[] = $lastName;
}
return implode(' ', $arr) ?: null;
}
}

View File

@@ -0,0 +1,60 @@
<?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\Export\Format\Xlsx;
class FieldData
{
public function __construct(
private string $entityType,
private string $field,
private string $type,
private ?string $link
) {}
public function getEntityType(): string
{
return $this->entityType;
}
public function getField(): string
{
return $this->field;
}
public function getType(): string
{
return $this->type;
}
public function getLink(): ?string
{
return $this->link;
}
}

View File

@@ -0,0 +1,115 @@
<?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\Export\Format\Xlsx;
use Espo\Core\ORM\Type\FieldType;
use Espo\ORM\Defs;
class FieldHelper
{
public function __construct(
private Defs $ormDefs
) {}
public function isForeignReference(string $name): bool
{
return str_contains($name, '_');
}
private function isForeign(string $entityType, string $name): bool
{
if ($this->isForeignReference($name)) {
return true;
}
$entityDefs = $this->ormDefs->getEntity($entityType);
return
$entityDefs->hasField($name) &&
$entityDefs->getField($name)->getType() === FieldType::FOREIGN;
}
public function getData(string $entityType, string $name): ?FieldData
{
$entityDefs = $this->ormDefs->getEntity($entityType);
if (!$this->isForeign($entityType, $name)) {
if (!$entityDefs->hasField($name)) {
return null;
}
$type = $entityDefs
->getField($name)
->getType();
return new FieldData($entityType, $name, $type, null);
}
$link = null;
$field = null;
if (
$entityDefs->hasField($name) &&
$entityDefs->getField($name)->getType() === FieldType::FOREIGN
) {
$fieldDefs = $entityDefs->getField($name);
$link = $fieldDefs->getParam('link');
$field = $fieldDefs->getParam('field');
} else if (str_contains($name, '_')) {
[$link, $field] = explode('_', $name);
}
if (!$link || !$field) {
return null;
}
$entityDefs = $this->ormDefs->getEntity($entityType);
if (!$entityDefs->hasRelation($link)) {
return null;
}
$relationDefs = $entityDefs->getRelation($link);
if (!$relationDefs->hasForeignEntityType()) {
return null;
}
$foreignEntityType = $relationDefs->getForeignEntityType();
$type = $this->ormDefs
->getEntity($foreignEntityType)
->getField($field)
->getType();
return new FieldData($foreignEntityType, $field, $type, $link);
}
}

View 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\Export\Format\Xlsx;
use Espo\Core\Field\Currency;
use Espo\Core\Field\Date;
use Espo\Core\Field\DateTime;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Core\Utils\Language;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Entity;
use Espo\Tools\Export\Collection;
use Espo\Tools\Export\Format\CellValuePreparator;
use Espo\Tools\Export\Format\CellValuePreparatorFactory;
use Espo\Tools\Export\Processor as ProcessorInterface;
use Espo\Tools\Export\Processor\Params;
use GuzzleHttp\Psr7\Stream;
use LogicException;
use OpenSpout\Common\Entity\Cell;
use OpenSpout\Common\Entity\Style\Style;
use OpenSpout\Common\Exception\InvalidArgumentException;
use OpenSpout\Common\Exception\IOException;
use OpenSpout\Writer\Exception\WriterNotOpenedException;
use OpenSpout\Writer\XLSX\Writer;
use OpenSpout\Writer\XLSX\Entity\SheetView;
use OpenSpout\Writer\XLSX\Options;
use OpenSpout\Common\Entity\Row;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
class OpenSpoutProcessor implements ProcessorInterface
{
private const FORMAT = 'xlsx';
/** @var array<string, CellValuePreparator> */
private array $preparatorsCache = [];
/** @var array<string, string> */
private array $typesCache = [];
public function __construct(
private FieldHelper $fieldHelper,
private CellValuePreparatorFactory $cellValuePreparatorFactory,
private Language $language,
private DateTimeUtil $dateTime,
private Config $config,
private Metadata $metadata
) {}
/**
* @throws IOException
* @throws WriterNotOpenedException
* @throws InvalidArgumentException
*/
public function process(Params $params, Collection $collection): StreamInterface
{
if (!$params->getFieldList()) {
throw new LogicException("No field list.");
}
$filePath = tempnam(sys_get_temp_dir(), 'espo-export');
if (!$filePath) {
throw new RuntimeException("Could not create a temp file.");
}
$options = new Options();
$options->setColumnWidthForRange(20, 1, count($params->getFieldList()));
$writer = new Writer($options);
$writer->openToFile($filePath);
$sheetView = new SheetView();
$sheetView->setFreezeRow(2);
$writer->getCurrentSheet()->setSheetView($sheetView);
$headerCells = [];
foreach ($params->getFieldList() as $name) {
$label = $this->translateLabel($params->getEntityType(), $name);
$headerCells[] = Cell::fromValue($label, (new Style())->setFontBold());
}
$writer->addRow(new Row($headerCells));
foreach ($collection as $entity) {
$this->processRow($params, $entity, $writer);
}
$writer->close();
$resource = fopen($filePath, 'r+');
if ($resource === false) {
throw new RuntimeException("Could not open temp.");
}
$stream = new Stream($resource);
$stream->seek(0);
return $stream;
}
private function translateLabel(string $entityType, string $name): string
{
$label = $name;
$fieldData = $this->fieldHelper->getData($entityType, $name);
$isForeignReference = $this->fieldHelper->isForeignReference($name);
if ($isForeignReference && $fieldData && $fieldData->getLink()) {
$label =
$this->language->translateLabel($fieldData->getLink(), 'links', $entityType) . '.' .
$this->language->translateLabel($fieldData->getField(), 'fields', $fieldData->getEntityType());
}
if (!$isForeignReference) {
$label = $this->language->translateLabel($name, 'fields', $entityType);
}
return $label;
}
private function processRow(Params $params, Entity $entity, Writer $writer): void
{
$cells = [];
foreach ($params->getFieldList() ?? [] as $name) {
$cells[] = $this->prepareCell($params, $entity, $name);
}
$writer->addRow(new Row($cells));
}
private function prepareCell(Params $params, Entity $entity, mixed $name): Cell
{
$type = $this->getFieldType($params->getEntityType(), $name);
$value = $this->getPreparator($type)
->prepare($entity, $name);
if (is_string($value)) {
$value = $this->sanitizeCellValue($value);
return Cell\StringCell::fromValue($value);
}
if (is_int($value)) {
return Cell\NumericCell::fromValue($value);
}
if (is_float($value)) {
return Cell\NumericCell::fromValue($value);
}
if (is_bool($value)) {
return Cell\BooleanCell::fromValue($value);
}
if ($value instanceof Date) {
$dateFormat = self::convertDateFormat($this->dateTime->getDateFormat());
$style = new Style();
$style->setFormat($dateFormat);
return Cell\DateTimeCell::fromValue($value->toDateTime(), $style);
}
if ($value instanceof DateTime) {
$dateTimeFormat = self::convertDateFormat($this->dateTime->getDateTimeFormat());
$style = new Style();
$style->setFormat($dateTimeFormat);
return Cell\DateTimeCell::fromValue($value->toDateTime(), $style);
}
if ($value instanceof Currency) {
$format = $this->getCurrencyFormat($value->getCode());
$style = new Style();
$style->setFormat($format);
return Cell\NumericCell::fromValue($value->getAmount(), $style);
}
return Cell::fromValue('');
}
private function getFieldType(string $entityType, string $name): string
{
$key = $entityType . '-' . $name;
$type = $this->typesCache[$key] ?? null;
if (!$type) {
$fieldData = $this->fieldHelper->getData($entityType, $name);
$type = $fieldData ? $fieldData->getType() : 'base';
$this->typesCache[$key] = $type;
}
return $type;
}
private function getPreparator(string $type): CellValuePreparator
{
if (!array_key_exists($type, $this->preparatorsCache)) {
$this->preparatorsCache[$type] = $this->cellValuePreparatorFactory->create(self::FORMAT, $type);
}
return $this->preparatorsCache[$type];
}
private static function convertDateFormat(string $format): string
{
$map = [
'MM' => 'mm',
'DD' => 'dd',
'YYYY' => 'yyyy',
'HH' => 'hh',
'mm' => 'mm',
'hh' => 'hh',
'A' => 'AM/PM',
'a' => 'AM/PM',
'ss' => 'ss',
];
return str_replace(
array_keys($map),
array_values($map),
$format
);
}
private function getCurrencyFormat(string $code): string
{
$currencySymbol = $this->metadata->get(['app', 'currency', 'symbolMap', $code], '');
$currencyFormat = $this->config->get('currencyFormat') ?? 2;
if ($currencyFormat === 3) {
return '#,##0.00_-"' . $currencySymbol . '"';
}
return '[$' . $currencySymbol . '-409]#,##0.00;-[$' . $currencySymbol . '-409]#,##0.00';
}
private function sanitizeCellValue(string $value): string
{
if ($value === '') {
return $value;
}
if (is_numeric($value)) {
return $value;
}
if (in_array($value[0], ['+', '-', '@', '='])) {
return "'" . $value;
}
return $value;
}
}

View File

@@ -0,0 +1,134 @@
<?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\Export\Format\Xlsx;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Entity;
use Espo\Tools\Export\Params;
use Espo\Tools\Export\Processor;
use Espo\Tools\Export\ProcessorParamsHandler;
class ParamsHandler implements ProcessorParamsHandler
{
public function __construct(
private Metadata $metadata
) {}
public function handle(Params $params, Processor\Params $processorParams): Processor\Params
{
$fieldList = $processorParams->getFieldList();
if ($fieldList === null) {
return $processorParams;
}
$fieldList = $this->filterFieldList($params->getEntityType(), $fieldList, $params->allFields());
$attributeList = $processorParams->getAttributeList();
$this->addAdditionalAttributes($params->getEntityType(), $attributeList, $fieldList);
return $processorParams
->withAttributeList($attributeList)
->withFieldList($fieldList);
}
/**
* @param string[] $fieldList
* @return string[]
*/
private function filterFieldList(string $entityType, array $fieldList, bool $exportAllFields): array
{
if ($exportAllFields) {
foreach ($fieldList as $i => $field) {
$type = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']);
if (in_array($type, [FieldType::LINK_MULTIPLE, FieldType::ATTACHMENT_MULTIPLE])) {
unset($fieldList[$i]);
}
}
}
return array_values($fieldList);
}
/**
* @param string[] $attributeList
* @param string[] $fieldList
*/
private function addAdditionalAttributes(string $entityType, array &$attributeList, array $fieldList): void
{
$linkList = [];
if (!in_array('id', $attributeList)) {
$attributeList[] = 'id';
}
$linkDefs = $this->metadata->get(['entityDefs', $entityType, 'links']) ?? [];
foreach ($linkDefs as $link => $defs) {
$linkType = $defs['type'] ?? null;
if (!$linkType) {
continue;
}
if ($linkType === Entity::BELONGS_TO_PARENT) {
$linkList[] = $link;
continue;
}
if ($linkType === Entity::BELONGS_TO && !empty($defs[RelationParam::NO_JOIN])) {
if ($this->metadata->get(['entityDefs', $entityType, 'fields', $link])) {
$linkList[] = $link;
}
}
}
foreach ($linkList as $item) {
if (in_array($item, $fieldList) && !in_array($item . 'Name', $attributeList)) {
$attributeList[] = $item . 'Name';
}
}
foreach ($fieldList as $field) {
$type = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']);
if ($type === FieldType::CURRENCY_CONVERTED) {
if (!in_array($field, $attributeList)) {
$attributeList[] = $field;
}
}
}
}
}

View File

@@ -0,0 +1,657 @@
<?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\Export\Format\Xlsx;
use Espo\Core\Field\Currency;
use Espo\Core\Field\Date;
use Espo\Core\Field\DateTime as DateTimeValue;
use Espo\Core\ORM\Type\FieldType;
use Espo\Core\Utils\Config\ApplicationConfig;
use Espo\Entities\Attachment;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\Core\Utils\Language;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Entity;
use Espo\Tools\Export\Collection;
use Espo\Tools\Export\Format\CellValuePreparator;
use Espo\Tools\Export\Format\CellValuePreparatorFactory;
use Espo\Tools\Export\Processor as ProcessorInterface;
use Espo\Tools\Export\Processor\Params;
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7\Stream;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
use DateTime;
use DateTimeZone;
use RuntimeException;
class PhpSpreadsheetProcessor implements ProcessorInterface
{
private const FORMAT = 'xlsx';
private const PARAM_RECORD_LINKS = 'recordLinks';
private const PARAM_TITLE = 'title';
/** @var array<string, CellValuePreparator> */
private array $preparatorsCache = [];
/** @var array<string, mixed> */
private array $titleStyle = [
'font' => [
'bold' => true,
'size' => 12,
]
];
/** @var array<string, mixed> */
private array $dateStyle = [
'font' => [
'size' => 12,
]
];
/** @var array<string, mixed> */
private array $headerStyle = [
'font' => [
'bold' => true,
'size' => 12,
]
];
/** @var array<string, mixed> */
private array $linkStyle = [
'font' => [
'color' => ['rgb' => '345b7c'],
'underline' => 'single',
]
];
public function __construct(
private Config $config,
private Metadata $metadata,
private Language $language,
private DateTimeUtil $dateTime,
private EntityManager $entityManager,
private FileStorageManager $fileStorageManager,
private FieldHelper $fieldHelper,
private CellValuePreparatorFactory $cellValuePreparatorFactory,
private ApplicationConfig $applicationConfig,
) {}
/**
* @throws SpreadsheetException
* @throws WriterException
*/
public function process(Params $params, Collection $collection): StreamInterface
{
$entityType = $params->getEntityType();
$fieldList = $params->getFieldList();
if ($fieldList === null) {
throw new RuntimeException("Field list is required.");
}
$sheetName = $this->getSheetNameFromParams($params);
$exportName = $params->getName() ??
$this->language->translate($entityType, 'scopeNamesPlural');
$phpExcel = new Spreadsheet();
$headerRowNumber = $params->getParam(self::PARAM_TITLE) ? 3 : 1;
$sheet = $phpExcel->setActiveSheetIndex(0)
->setTitle($sheetName)
->freezePane('A' . ($headerRowNumber + 1));
$now = new DateTime();
$now->setTimezone(new DateTimeZone($this->config->get('timeZone', 'UTC')));
if ($params->getParam(self::PARAM_TITLE)) {
$sheet
->setCellValue('A1', $this->sanitizeCellValue($exportName))
->setCellValue('A2',
SharedDate::PHPToExcel(strtotime($now->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)))
);
$sheet->getStyle('A1')->applyFromArray($this->titleStyle);
$sheet->getStyle('A2')->applyFromArray($this->dateStyle);
$sheet->getStyle('A2')
->getNumberFormat()
->setFormatCode($this->dateTime->getDateTimeFormat());
}
$azRange = $this->getColumnsRange($fieldList);
$rowNumber = $headerRowNumber;
$linkColList = [];
$lastIndex = 0;
foreach ($fieldList as $i => $name) {
$col = $azRange[$i];
$type = 'base';
$label = $this->translateLabel($entityType, $name);
$fieldData = $this->fieldHelper->getData($entityType, $name);
if ($fieldData) {
$type = $fieldData->getType();
}
$sheet->setCellValue($col . $rowNumber, $this->sanitizeCellValue($label));
$sheet->getColumnDimension($col)->setAutoSize(true);
$linkTypeList = $params->getParam(self::PARAM_RECORD_LINKS) ?
[FieldType::URL, FieldType::PHONE, FieldType::EMAIL, FieldType::LINK, FieldType::LINK_PARENT] :
['url'];
if (
in_array($type, $linkTypeList) ||
$params->getParam(self::PARAM_RECORD_LINKS) && $name === 'name'
) {
$linkColList[] = $col;
}
$lastIndex = $i;
}
$col = $azRange[$lastIndex];
$sheet->getStyle("A$rowNumber:$col$rowNumber")->applyFromArray($this->headerStyle);
$sheet->setAutoFilter("A$rowNumber:$col$rowNumber");
$typesCache = [];
$rowNumber++;
foreach ($collection as $entity) {
$this->processRow(
$entity,
$sheet,
$rowNumber,
$fieldList,
$azRange,
$typesCache
);
$rowNumber++;
}
$sheet->getStyle("A$headerRowNumber:A$rowNumber")
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_TEXT);
$startingRowNumber = 2;
if ($params->getParam(self::PARAM_TITLE)) {
$startingRowNumber += 2;
}
foreach ($fieldList as $i => $name) {
$col = $azRange[$i];
if (!array_key_exists($name, $typesCache)) {
break;
}
$type = $typesCache[$name];
$coordinate = "$col$startingRowNumber:$col$rowNumber";
switch ($type) {
case FieldType::CURRENCY:
case FieldType::CURRENCY_CONVERTED:
break;
case FieldType::INT:
$sheet->getStyle($coordinate)
->getNumberFormat()
->setFormatCode('0');
break;
case FieldType::FLOAT:
$sheet->getStyle($coordinate)
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1);
break;
case FieldType::DATE:
$sheet->getStyle($coordinate)
->getNumberFormat()
->setFormatCode($this->dateTime->getDateFormat());
break;
case FieldType::DATETIME_OPTIONAL:
case FieldType::DATETIME:
$sheet->getStyle($coordinate)
->getNumberFormat()
->setFormatCode($this->dateTime->getDateTimeFormat());
break;
default:
$sheet->getStyle($coordinate)
->getNumberFormat()
->setFormatCode('@');
break;
}
}
foreach ($linkColList as $linkColumn) {
$sheet
->getStyle($linkColumn . $startingRowNumber . ':' . $linkColumn . $rowNumber)
->applyFromArray($this->linkStyle);
}
$objWriter = IOFactory::createWriter($phpExcel, 'Xlsx');
$resource = fopen('php://temp', 'r+');
if ($resource === false) {
throw new RuntimeException("Could not open temp.");
}
$objWriter->save($resource);
$stream = new Stream($resource);
$stream->seek(0);
return $stream;
}
private function translateLabel(string $entityType, string $name): string
{
$label = $name;
$fieldData = $this->fieldHelper->getData($entityType, $name);
$isForeignReference = $this->fieldHelper->isForeignReference($name);
if ($isForeignReference && $fieldData && $fieldData->getLink()) {
$label =
$this->language->translateLabel($fieldData->getLink(), 'links', $entityType) . '.' .
$this->language->translateLabel($fieldData->getField(), 'fields', $fieldData->getEntityType());
}
if (!$isForeignReference) {
$label = $this->language->translateLabel($name, 'fields', $entityType);
}
return $label;
}
/**
* @param string[] $fieldList
* @return string[]
*/
private function getColumnsRange(array $fieldList): array
{
$azRange = range('A', 'Z');
$azRangeCopied = $azRange;
foreach ($azRangeCopied as $i => $char1) {
foreach ($azRangeCopied as $j => $char2) {
$azRange[] = $char1 . $char2;
if ($i * count($azRangeCopied) + $j === count($fieldList)) {
break 2;
}
}
}
return $azRange;
}
/**
* @param string[] $fieldList
* @param string[] $azRange
* @param array<string, string> $typesCache
* @throws SpreadsheetException
*/
private function processRow(
Entity $entity,
Worksheet $sheet,
int $rowNumber,
array $fieldList,
array $azRange,
array &$typesCache
): void {
foreach ($fieldList as $i => $name) {
$col = $azRange[$i];
$coordinate = $col . $rowNumber;
$this->processCell(
$entity,
$sheet,
$rowNumber,
$coordinate,
$name,
$typesCache
);
}
}
/**
* @param array<string, string> $typesCache
* @throws SpreadsheetException
*/
private function processCell(
Entity $entity,
Worksheet $sheet,
int $rowNumber,
string $coordinate,
string $name,
array &$typesCache
): void {
$entityType = $entity->getEntityType();
$type = $typesCache[$name] ?? null;
if (!$type) {
$fieldData = $this->fieldHelper->getData($entityType, $name);
$type = $fieldData ? $fieldData->getType() : 'base';
$typesCache[$name] = $type;
}
$preparator = $this->getPreparator($type);
$value = $preparator->prepare($entity, $name);
if ($type === FieldType::IMAGE) {
$this->applyImage(
$entity,
$coordinate,
$sheet,
$rowNumber,
$name
);
$value = null;
}
$value = $this->sanitizeCellValue($value);
if (is_string($value)) {
$sheet->setCellValueExplicit($coordinate, $value, DataType::TYPE_STRING);
} else if (is_int($value) || is_float($value)) {
$sheet->setCellValueExplicit($coordinate, $value, DataType::TYPE_NUMERIC);
}
if (is_bool($value)) {
$sheet->setCellValueExplicit($coordinate, $value, DataType::TYPE_BOOL);
} else if ($value instanceof Date) {
$sheet->setCellValue(
$coordinate,
SharedDate::PHPToExcel(
strtotime($value->toString())
)
);
} else if ($value instanceof DateTimeValue) {
$sheet->setCellValue(
$coordinate,
SharedDate::PHPToExcel(
strtotime($value->toDateTime()->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT))
)
);
} else if ($value instanceof Currency) {
$sheet->setCellValue($coordinate, $value->getAmount());
$sheet->getStyle($coordinate)
->getNumberFormat()
->setFormatCode($this->getCurrencyFormatCode($value->getCode()));
}
$this->applyLinks(
$type,
$entity,
$sheet,
$coordinate,
$name
);
}
/**
* @throws SpreadsheetException
*/
private function applyImage(
Entity $entity,
string $coordinate,
Worksheet $sheet,
int $rowNumber,
string $name
): void {
$attachmentId = $entity->get($name . 'Id');
if (!$attachmentId) {
return;
}
/** @var ?Attachment $attachment */
$attachment = $this->entityManager->getEntityById(Attachment::ENTITY_TYPE, $attachmentId);
if (!$attachment) {
return;
}
$objDrawing = new Drawing();
$filePath = $this->fileStorageManager->getLocalFilePath($attachment);
if (!$filePath || !file_exists($filePath)) {
return;
}
$objDrawing->setPath($filePath);
$objDrawing->setHeight(100);
$objDrawing->setCoordinates($coordinate);
$objDrawing->setWorksheet($sheet);
$sheet->getRowDimension($rowNumber)->setRowHeight(100);
}
/**
* @throws SpreadsheetException
*/
private function applyLinks(
string $type,
Entity $entity,
Worksheet $sheet,
string $coordinate,
string $name
): void {
$entityType = $entity->getEntityType();
$link = null;
$foreignLink = null;
$foreignField = null;
if (strpos($name, '_')) {
[$foreignLink, $foreignField] = explode('_', $name);
}
$siteUrl = $this->applicationConfig->getSiteUrl();
if ($name === 'name') {
if ($entity->hasId()) {
$link = "$siteUrl/#$entityType/view/{$entity->getId()}";
}
} else if ($type === FieldType::URL) {
$value = $entity->get($name);
if ($value) {
$link = $this->sanitizeUrl($value);
}
} else if ($type === FieldType::LINK) {
$idValue = $entity->get($name . 'Id');
if ($idValue && $foreignField) {
if (!$foreignLink) {
$foreignEntity =
$this->metadata->get(['entityDefs', $entityType, 'links', $name, RelationParam::ENTITY]);
} else {
$foreignEntity1 = $this->metadata
->get(['entityDefs', $entityType, 'links', $foreignLink, 'entity']);
$foreignEntity = $this->metadata
->get(['entityDefs', $foreignEntity1, 'links', $foreignField, 'entity']);
}
if ($foreignEntity) {
$link = "$siteUrl/#$foreignEntity/view/$idValue";
}
}
} else if ($type === FieldType::FILE) {
$idValue = $entity->get($name . 'Id');
if ($idValue) {
$link = "$siteUrl/?entryPoint=download&id=$idValue";
}
} else if ($type === FieldType::LINK_PARENT) {
$idValue = $entity->get($name . 'Id');
$typeValue = $entity->get($name . 'Type');
if ($idValue && $typeValue) {
$link = "$siteUrl/#$typeValue/view/$idValue";
}
} else if ($type === FieldType::PHONE) {
$value = $entity->get($name);
if ($value) {
$link = "tel:$value";
}
} else if ($type === FieldType::EMAIL) {
$value = $entity->get($name);
if ($value) {
$link = "mailto:$value";
}
}
if (!$link) {
return;
}
$cell = $sheet->getCell($coordinate);
$hyperLink = $cell->getHyperlink();
$hyperLink->setUrl($link);
$hyperLink->setTooltip($link);
}
private function getPreparator(string $type): CellValuePreparator
{
if (!array_key_exists($type, $this->preparatorsCache)) {
$this->preparatorsCache[$type] = $this->cellValuePreparatorFactory->create(self::FORMAT, $type);
}
return $this->preparatorsCache[$type];
}
private function getCurrencyFormatCode(string $currency): string
{
$currencySymbol = $this->metadata->get(['app', 'currency', 'symbolMap', $currency], '');
$currencyFormat = $this->config->get('currencyFormat') ?? 2;
if ($currencyFormat == 3) {
return '#,##0.00_-"' . $currencySymbol . '"';
}
return '[$'.$currencySymbol.'-409]#,##0.00;-[$'.$currencySymbol.'-409]#,##0.00';
}
private function getSheetNameFromParams(Params $params): string
{
$exportName =
$params->getName() ??
$this->language->translateLabel($params->getEntityType(), 'scopeNamesPlural');
$badCharList = ['*', ':', '/', '\\', '?', '[', ']'];
$sheetName = mb_substr($exportName, 0, 30, 'utf-8');
$sheetName = str_replace($badCharList, ' ', $sheetName);
return str_replace('\'', '', $sheetName);
}
private function sanitizeCellValue(mixed $value): mixed
{
if (!is_string($value)) {
return $value;
}
if ($value === '') {
return $value;
}
if (is_numeric($value)) {
return $value;
}
if (in_array($value[0], ['+', '-', '@', '='])) {
return "'" . $value;
}
return $value;
}
private function sanitizeUrl(string $value): ?string
{
$link = $value;
if (!preg_match("/[a-z]+:\/\//", $link)) {
$link = 'https://' . $link;
}
if (filter_var($link, FILTER_VALIDATE_URL)) {
return $link;
}
return null;
}
}

View File

@@ -0,0 +1,83 @@
<?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\Export\Format\Xlsx;
use Espo\Core\Exceptions\Error;
use Espo\Tools\Export\Collection;
use Espo\Tools\Export\Processor as ProcessorInterface;
use Espo\Tools\Export\Processor\Params;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
use Psr\Http\Message\StreamInterface;
class Processor implements ProcessorInterface
{
private const PARAM_LITE = 'lite';
public function __construct(
private PhpSpreadsheetProcessor $phpSpreadsheetProcessor,
private OpenSpoutProcessor $openSpoutProcessor,
) {}
/**
* @throws Error
*/
public function process(Params $params, Collection $collection): StreamInterface
{
return $params->getParam(self::PARAM_LITE) ?
$this->processOpenSpout($params, $collection) :
$this->processPhpSpreadsheet($params, $collection);
}
/**
* @throws Error
*/
private function processPhpSpreadsheet(Params $params, Collection $collection): StreamInterface
{
try {
return $this->phpSpreadsheetProcessor->process($params, $collection);
} catch (SpreadsheetException|WriterException $e) {
throw new Error($e->getMessage());
}
}
/**
* @throws Error
*/
private function processOpenSpout(Params $params, Collection $collection): StreamInterface
{
try {
return $this->openSpoutProcessor->process($params, $collection);
} catch (\Throwable $e) {
throw new Error($e->getMessage());
}
}
}

View 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\Export\Jobs;
use Espo\Core\Exceptions\Error;
use Espo\Core\Job\Job;
use Espo\Core\Job\Job\Data as JobData;
use Espo\Tools\Export\Factory;
use Espo\Tools\Export\Result;
use Espo\Core\Utils\Language;
use Espo\ORM\EntityManager;
use Espo\Entities\Export;
use Espo\Entities\Notification;
use Espo\Entities\User;
use Throwable;
class Process implements Job
{
public function __construct(
private EntityManager $entityManager,
private Factory $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 Export|null $entity */
$entity = $this->entityManager->getEntityById(Export::ENTITY_TYPE, $id);
if ($entity === null) {
throw new Error("Export '$id' not found.");
}
/** @var User|null $user */
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $entity->getCreatedBy()->getId());
if (!$user) {
throw new Error("Export entity '$id', user not found.");
}
try {
$export = $this->factory->createForUser($user);
$this->setRunning($entity);
$result = $export
->setParams($entity->getParams())
->run();
} catch (Throwable $e) {
$this->setFailed($entity);
throw new Error("Export job error: " . $e->getMessage());
}
$this->setSuccess($entity, $result);
$this->entityManager->refreshEntity($entity);
if ($entity->notifyOnFinish()) {
$this->notifyFinish($entity);
}
}
private function notifyFinish(Export $entity): void
{
$notification = $this->entityManager->getRDBRepositoryByClass(Notification::class)->getNew();
$url = '?entryPoint=download&id=' . $entity->getAttachmentId();
$message = str_replace(
'{url}',
$url,
$this->language->translateLabel('exportProcessed', 'messages', 'Export')
);
$notification
->setType(Notification::TYPE_MESSAGE)
->setMessage($message)
->setUserId($entity->getCreatedBy()->getId());
$this->entityManager->saveEntity($notification);
}
private function setFailed(Export $entity): void
{
$entity->setStatus(Export::STATUS_FAILED);
$this->entityManager->saveEntity($entity);
}
private function setRunning(Export $entity): void
{
$entity->setStatus(Export::STATUS_RUNNING);
$this->entityManager->saveEntity($entity);
}
private function setSuccess(Export $entity, Result $result): void
{
$entity
->setStatus(Export::STATUS_SUCCESS)
->setAttachmentId($result->getAttachmentId());
$this->entityManager->saveEntity($entity);
}
}

View File

@@ -0,0 +1,324 @@
<?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\Export;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\Where\Item as WhereItem;
use RuntimeException;
/**
* Immutable.
*/
class Params
{
private string $entityType;
/** @var ?string[] */
private $attributeList = null;
/** @var ?string[] */
private $fieldList = null;
private ?string $fileName = null;
private ?string $format = null;
private ?string $name = null;
/** @var array<string, mixed> */
private array $params = [];
private ?SearchParams $searchParams = null;
private bool $applyAccessControl = true;
public function __construct(string $entityType)
{
$this->entityType = $entityType;
}
/**
* @param array<string, mixed> $params
* @throws RuntimeException
*/
public static function fromRaw(array $params): self
{
$entityType = $params['entityType'] ?? null;
if (!$entityType) {
throw new RuntimeException("No entityType.");
}
$obj = new self($entityType);
$obj->name = $params['name'] ?? $params['exportName'] ?? null;
$obj->fileName = $params['fileName'] ?? null;
$obj->format = $params['format'] ?? null;
$obj->attributeList = $params['attributeList'] ?? null;
$obj->fieldList = $params['fieldList'] ?? null;
$where = $params['where'] ?? null;
$ids = $params['ids'] ?? null;
$searchParams = $params['searchParams'] ?? null;
if ($where && !is_array($where)) {
throw new RuntimeException("Bad 'where'.");
}
if ($searchParams && !is_array($searchParams)) {
throw new RuntimeException("Bad 'searchParams'.");
}
if ($where && $searchParams) {
$searchParams['where'] = $where;
}
if ($where && !$searchParams) {
$searchParams = [
'where' => $where,
];
}
if ($searchParams) {
if ($ids) {
throw new RuntimeException("Can't combine 'ids' and search params.");
}
} else if ($ids) {
if (!is_array($ids)) {
throw new RuntimeException("Bad 'ids'.");
}
$obj->searchParams = SearchParams
::create()
->withWhere(
WhereItem::fromRaw([
'type' => 'equals',
'attribute' => 'id',
'value' => $ids,
])
);
}
if ($searchParams) {
$actualSearchParams = $searchParams;
unset($actualSearchParams['select']);
$obj->searchParams = SearchParams::fromRaw($actualSearchParams);
}
return $obj;
}
public static function create(string $entityType): self
{
return new self($entityType);
}
public function withFormat(?string $format): self
{
$obj = clone $this;
$obj->format = $format;
return $obj;
}
/** @noinspection PhpUnused */
public function withFileName(?string $fileName): self
{
$obj = clone $this;
$obj->fileName = $fileName;
return $obj;
}
public function withName(?string $name): self
{
$obj = clone $this;
$obj->name = $name;
return $obj;
}
public function withSearchParams(?SearchParams $searchParams): self
{
$obj = clone $this;
$obj->searchParams = $searchParams;
return $obj;
}
public function withParam(string $name, mixed $value): self
{
$obj = clone $this;
$obj->params[$name] = $value;
return $obj;
}
/**
* @param ?string[] $fieldList
*/
public function withFieldList(?array $fieldList): self
{
$obj = clone $this;
$obj->fieldList = $fieldList;
return $obj;
}
/**
* @param ?string[] $attributeList
*/
public function withAttributeList(?array $attributeList): self
{
$obj = clone $this;
$obj->attributeList = $attributeList;
return $obj;
}
public function withAccessControl(bool $applyAccessControl = true): self
{
$obj = clone $this;
$obj->applyAccessControl = $applyAccessControl;
return $obj;
}
/**
* Get search params.
*/
public function getSearchParams(): SearchParams
{
$searchParams = $this->searchParams ?? SearchParams::create();
if ($searchParams->getSelect() !== null) {
return $searchParams;
}
if ($this->getAttributeList()) {
$searchParams = $searchParams->withSelect($this->getAttributeList());
} else {
$searchParams = $searchParams->withSelect(['*']);
}
return $searchParams;
}
/**
* Get a target entity type.
*/
public function getEntityType(): string
{
return $this->entityType;
}
/**
* Get a filename for a result export file.
*/
public function getFileName(): ?string
{
return $this->fileName;
}
/**
* Get a name.
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Get a format.
*/
public function getFormat(): ?string
{
return $this->format;
}
/**
* Get attributes to be exported.
*
* @return ?string[]
*/
public function getAttributeList(): ?array
{
return $this->attributeList;
}
/**
* Get fields to be exported.
*
* @return ?string[]
*/
public function getFieldList(): ?array
{
return $this->fieldList;
}
/**
* Get a parameter list.
*
* @return string[]
*/
public function getParamList(): array
{
return array_keys($this->params);
}
/**
* Get a parameter value.
*/
public function getParam(string $name): mixed
{
return $this->params[$name] ?? null;
}
/**
* Has a parameter.
*/
public function hasParam(string $name): bool
{
return array_key_exists($name, $this->params);
}
/**
* Whether all fields should be exported.
*/
public function allFields(): bool
{
return $this->fieldList === null && $this->attributeList === null;
}
/**
* Whether to apply access control.
*/
public function applyAccessControl(): bool
{
return $this->applyAccessControl;
}
}

View File

@@ -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\Export;
use Psr\Http\Message\StreamInterface;
use Espo\Tools\Export\Processor\Params;
interface Processor
{
public function process(Params $params, Collection $collection): StreamInterface;
}

View 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\Export\Processor;
use RuntimeException;
/**
* Immutable.
*/
class Params
{
private string $fileName;
/** @var string[] */
private array $attributeList;
/** @var ?string[] */
private ?array $fieldList = null;
private ?string $name = null;
private ?string $entityType = null;
/** @var array<string, mixed> */
private array $params = [];
/**
* @param string[] $attributeList
* @param ?string[] $fieldList
*/
public function __construct(string $fileName, array $attributeList, ?array $fieldList)
{
$this->fileName = $fileName;
$this->attributeList = $attributeList;
$this->fieldList = $fieldList;
}
public function withEntityType(string $entityType): self
{
$obj = clone $this;
$obj->entityType = $entityType;
return $obj;
}
public function withName(?string $name): self
{
$obj = clone $this;
$obj->name = $name;
return $obj;
}
/**
* @param ?string[] $fieldList
*/
public function withFieldList(?array $fieldList): self
{
$obj = clone $this;
$obj->fieldList = $fieldList;
return $obj;
}
/**
* @param string[] $attributeList
*/
public function withAttributeList(array $attributeList): self
{
$obj = clone $this;
$obj->attributeList = $attributeList;
return $obj;
}
public function withParam(string $name, mixed $value): self
{
$obj = clone $this;
$obj->params[$name] = $value;
return $obj;
}
/**
* An export file name.
*/
public function getFileName(): string
{
return $this->fileName;
}
/**
* Attributes to export.
*
* @return string[]
*/
public function getAttributeList(): array
{
return $this->attributeList;
}
/**
* Fields to export.
*
* @return ?string[]
*/
public function getFieldList(): ?array
{
return $this->fieldList;
}
/**
* An export name.
*/
public function getName(): ?string
{
return $this->name;
}
/**
* An entity type.
*/
public function getEntityType(): string
{
if ($this->entityType === null) {
throw new RuntimeException("No entity-type.");
}
return $this->entityType;
}
/**
* Get a parameter value.
*/
public function getParam(string $name): mixed
{
return $this->params[$name] ?? null;
}
}

View 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\Export;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use LogicException;
class ProcessorFactory
{
public function __construct(
private InjectableFactory $injectableFactory,
private Metadata $metadata
) {}
public function create(string $format): Processor
{
if (!in_array($format, $this->metadata->get(['app', 'export', 'formatList']))) {
throw new LogicException("Not supported export format '{$format}'.");
}
/** @var ?class-string<Processor> $className */
$className = $this->metadata->get(['app', 'export', 'formatDefs', $format, 'processorClassName']);
if (!$className) {
throw new LogicException("No implementation for format '{$format}'.");
}
return $this->injectableFactory->create($className);
}
}

View 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\Export;
interface ProcessorParamsHandler
{
public function handle(Params $params, Processor\Params $processorParams): Processor\Params;
}

View 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\Export;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use LogicException;
class ProcessorParamsHandlerFactory
{
public function __construct(
private InjectableFactory $injectableFactory,
private Metadata $metadata
) {}
public function create(string $format): ProcessorParamsHandler
{
$className = $this->getClassName($format);
if (!$className) {
throw new LogicException();
}
return $this->injectableFactory->create($className);
}
public function isCreatable(string $format): bool
{
return (bool) $this->getClassName($format);
}
/**
* @return ?class-string<ProcessorParamsHandler>
*/
private function getClassName(string $format): ?string
{
return $this->metadata->get(['app', 'export', 'formatDefs', $format, 'processorParamsHandler']);
}
}

View 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\Export;
/**
* Immutable.
*/
class Result
{
private string $attachmentId;
public function __construct(string $attachmentId)
{
$this->attachmentId = $attachmentId;
}
public function getAttachmentId(): string
{
return $this->attachmentId;
}
}

View File

@@ -0,0 +1,165 @@
<?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\Export;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Exceptions\NotFoundSilent;
use Espo\Core\Acl;
use Espo\Core\Acl\Table;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Metadata;
use Espo\Core\Job\JobSchedulerFactory;
use Espo\Core\Job\Job\Data as JobData;
use Espo\Tools\Export\Jobs\Process;
use Espo\ORM\EntityManager;
use Espo\Entities\Export as ExportEntity;
use Espo\Entities\User;
use stdClass;
class Service
{
public function __construct(
private Factory $factory,
private Config $config,
private Acl $acl,
private User $user,
private Metadata $metadata,
private EntityManager $entityManager,
private JobSchedulerFactory $jobSchedulerFactory
) {}
/**
* @throws Forbidden
*/
public function process(Params $params, ServiceParams $serviceParams): ServiceResult
{
if ($this->config->get('exportDisabled') && !$this->user->isAdmin()) {
throw new ForbiddenSilent("Export disabled for non-admin users.");
}
$entityType = $params->getEntityType();
if ($this->acl->getPermissionLevel(Acl\Permission::EXPORT) !== Table::LEVEL_YES) {
throw new ForbiddenSilent("No 'export' permission.");
}
if (!$this->acl->check($entityType, Table::ACTION_READ)) {
throw new ForbiddenSilent("No 'read' access.");
}
if ($this->metadata->get(['recordDefs', $entityType, 'exportDisabled'])) {
throw new ForbiddenSilent("Export disabled for '$entityType'.");
}
if ($serviceParams->isIdle()) {
if ($this->user->isPortal()) {
throw new ForbiddenSilent("Idle export is not allowed for portal users.");
}
return $this->schedule($params);
}
$export = $this->factory->create();
$result = $export
->setParams($params)
->run();
return ServiceResult::createWithResult($result);
}
/**
* @throws Forbidden
* @throws NotFound
*/
public function getStatusData(string $id): stdClass
{
/** @var ?ExportEntity $entity */
$entity = $this->entityManager->getEntityById(ExportEntity::ENTITY_TYPE, $id);
if (!$entity) {
throw new NotFoundSilent();
}
if ($entity->getCreatedBy()->getId() !== $this->user->getId()) {
throw new ForbiddenSilent();
}
return (object) [
'status' => $entity->getStatus(),
'attachmentId' => $entity->getAttachmentId(),
];
}
/**
* @throws Forbidden
* @throws NotFound
*/
public function subscribeToNotificationOnSuccess(string $id): void
{
/** @var ?ExportEntity $entity */
$entity = $this->entityManager->getEntityById(ExportEntity::ENTITY_TYPE, $id);
if (!$entity) {
throw new NotFoundSilent();
}
if ($entity->getCreatedBy()->getId() !== $this->user->getId()) {
throw new ForbiddenSilent();
}
$entity->setNotifyOnFinish();
$this->entityManager->saveEntity($entity);
}
private function schedule(Params $params): ServiceResult
{
$entity = $this->entityManager->createEntity(ExportEntity::ENTITY_TYPE, [
// Additional encoding to handle null-character issue in PostgreSQL.
'params' => base64_encode(serialize($params)),
]);
$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,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\Export;
class ServiceParams
{
private bool $isIdle = false;
public static function create(): self
{
return new self();
}
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\Tools\Export;
/**
* 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;
}
}