diff --git a/application/Espo/Controllers/Export.php b/application/Espo/Controllers/Export.php new file mode 100644 index 0000000000..764bcca4eb --- /dev/null +++ b/application/Espo/Controllers/Export.php @@ -0,0 +1,107 @@ +service = $service; + } + + public function postActionProcess(Request $request): StdClass + { + $params = $this->fetchRawParamsFromRequest($request); + + $result = $this->service->process($params); + + return (object) [ + 'id' => $result->getAttachmentId(), + ]; + } + + 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 ?? 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; + } + + return Params::fromRaw($params); + } +} diff --git a/application/Espo/Controllers/Notification.php b/application/Espo/Controllers/Notification.php index 0b6c0df468..2413061a07 100644 --- a/application/Espo/Controllers/Notification.php +++ b/application/Espo/Controllers/Notification.php @@ -85,9 +85,4 @@ class Notification extends RecordBase return true; } - - public function beforeExport(): void - { - throw new Error(); - } } diff --git a/application/Espo/Core/Controllers/RecordBase.php b/application/Espo/Core/Controllers/RecordBase.php index 18cc6d3444..b9310f443a 100644 --- a/application/Espo/Core/Controllers/RecordBase.php +++ b/application/Espo/Core/Controllers/RecordBase.php @@ -249,53 +249,6 @@ class RecordBase extends Base implements Di\EntityManagerAware return $this->searchParamsFetcher->fetch($request); } - public function postActionExport(Request $request): StdClass - { - $data = $request->getParsedBody(); - - if ($this->config->get('exportDisabled') && !$this->user->isAdmin()) { - throw new Forbidden("Export is disabled."); - } - - $ids = isset($data->ids) ? - $data->ids : null; - - $where = isset($data->where) ? - json_decode(json_encode($data->where), true) : null; - - $byWhere = isset($data->byWhere) ? - $data->byWhere : false; - - $selectData = isset($data->selectData) ? - json_decode(json_encode($data->selectData), true) : null; - - $actionParams = []; - - if ($byWhere) { - $actionParams['selectData'] = $selectData; - $actionParams['where'] = $where; - } - else { - $actionParams['ids'] = $ids; - } - - if (isset($data->attributeList)) { - $actionParams['attributeList'] = $data->attributeList; - } - - if (isset($data->fieldList)) { - $actionParams['fieldList'] = $data->fieldList; - } - - if (isset($data->format)) { - $actionParams['format'] = $data->format; - } - - return (object) [ - 'id' => $this->getRecordService()->export($actionParams), - ]; - } - public function postActionGetDuplicateAttributes(Request $request): StdClass { $id = $request->getParsedBody()->id ?? null; diff --git a/application/Espo/Core/Record/Service.php b/application/Espo/Core/Record/Service.php index 29195845dd..82d80c7703 100644 --- a/application/Espo/Core/Record/Service.php +++ b/application/Espo/Core/Record/Service.php @@ -63,10 +63,6 @@ use Espo\Core\{ FieldProcessing\LoaderParams as FieldLoaderParams, }; -use Espo\Tools\{ - Export\Export as ExportTool, -}; - use Espo\Core\Di; use StdClass; @@ -1657,31 +1653,6 @@ class Service implements Crud, return null; } - /** - * Run an export. - * - * @param Raw export parameters. - * @return An attachment ID. - */ - public function export(array $params): string - { - if ($this->acl->getPermissionLevel('exportPermission') !== AclTable::LEVEL_YES) { - throw new ForbiddenSilent("No 'export' permission."); - } - - if (!$this->acl->check($this->entityType, AclTable::ACTION_READ)) { - throw new ForbiddenSilent("No 'read' access."); - } - - $export = $this->injectableFactory->create(ExportTool::class); - - return $export - ->setRecordService($this) - ->setParams($params) - ->setEntityType($this->entityType) - ->run(); - } - /** * Prepare an entity for output. Clears not allowed attributes. * diff --git a/application/Espo/Resources/metadata/app/export.json b/application/Espo/Resources/metadata/app/export.json index 9253ce6861..ad1129414d 100644 --- a/application/Espo/Resources/metadata/app/export.json +++ b/application/Espo/Resources/metadata/app/export.json @@ -13,8 +13,8 @@ "fileExtension": "xlsx" } }, - "exportFormatClassNameMap": { - "csv": "Espo\\Tools\\Export\\Formats\\Csv", - "xlsx": "Espo\\Tools\\Export\\Formats\\Xlsx" + "processorClassNameMap": { + "csv": "Espo\\Tools\\Export\\Processors\\Csv", + "xlsx": "Espo\\Tools\\Export\\Processors\\Xlsx" } } \ No newline at end of file diff --git a/application/Espo/Resources/metadata/recordDefs/Notification.json b/application/Espo/Resources/metadata/recordDefs/Notification.json new file mode 100644 index 0000000000..534dbbb8b9 --- /dev/null +++ b/application/Espo/Resources/metadata/recordDefs/Notification.json @@ -0,0 +1,3 @@ +{ + "exportDisabled": true +} diff --git a/application/Espo/Resources/routes.json b/application/Espo/Resources/routes.json index ba27f61539..9c54635c05 100644 --- a/application/Espo/Resources/routes.json +++ b/application/Espo/Resources/routes.json @@ -252,6 +252,14 @@ "action": "process" } }, + { + "route": "/Export", + "method": "post", + "params": { + "controller": "Export", + "action": "process" + } + }, { "route": "/Kanban/:entityType", "method": "get", diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index 7837513e09..51919b0781 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -50,6 +50,7 @@ use Espo\Core\Di; use Espo\Tools\{ Export\Export as ExportTool, + Export\Params as ExportParams, }; class Record extends RecordService implements @@ -259,13 +260,13 @@ class Record extends RecordService implements throw new ForbiddenSilent("No 'read' access."); } + $params['entityType'] = $this->entityType; + $export = $this->injectableFactory->create(ExportTool::class); return $export - ->setRecordService($this) - ->setParams($params) + ->setParams(ExportParams::fromRaw($params)) ->setCollection($collection) - ->setEntityType($this->entityType) ->run(); } diff --git a/application/Espo/Tools/Export/Export.php b/application/Espo/Tools/Export/Export.php index 38bcb8ec30..40354169aa 100644 --- a/application/Espo/Tools/Export/Export.php +++ b/application/Espo/Tools/Export/Export.php @@ -30,56 +30,56 @@ namespace Espo\Tools\Export; use Espo\Core\{ - Exceptions\BadRequest, Exceptions\Error, Utils\Json, - Di, - Select\SearchParams, + Select\SelectBuilderFactory, + Acl, + Acl\Table, + Record\ServiceContainer, + Utils\Metadata, }; use Espo\{ ORM\Entity, ORM\Collection, - Services\Record, + ORM\EntityManager, }; -class Export implements - - Di\MetadataAware, - Di\EntityManagerAware, - Di\SelectBuilderFactoryAware, - Di\AclAware, - Di\InjectableFactoryAware +class Export { - use Di\MetadataSetter; - use Di\EntityManagerSetter; - use Di\SelectBuilderFactorySetter; - use Di\AclSetter; - use Di\InjectableFactorySetter; + private $params; - protected $entityType; + private $collection = null; - protected $recordService; + private $processorFactory; - protected $collection = null; + private $selectBuilderFactory; - protected $params = []; + private $serviceContainer; - public function setRecordService(Record $recordService): self - { - $this->recordService = $recordService; + private $acl; - return $this; + private $entityManager; + + private $metadata; + + public function __construct( + ProcessorFactory $processorFactory, + SelectBuilderFactory $selectBuilderFactor, + ServiceContainer $serviceContainer, + Acl $acl, + EntityManager $entityManager, + Metadata $metadata + ) { + $this->processorFactory = $processorFactory; + $this->selectBuilderFactory = $selectBuilderFactor; + $this->serviceContainer = $serviceContainer; + $this->acl = $acl; + $this->entityManager = $entityManager; + $this->metadata = $metadata; } - public function setEntityType(string $entityType): self - { - $this->entityType = $entityType; - - return $this; - } - - public function setParams(array $params): self + public function setParams(Params $params): self { $this->params = $params; @@ -95,56 +95,37 @@ class Export implements /** * Run export. - * - * @return An ID of a generated attachment. */ - public function run(): string + public function run(): Result { + if (!$this->params) { + throw new Error("No params set."); + } + $params = $this->params; - if (!$this->entityType) { - throw new Error("Entity type is not specified."); - } + $entityType = $params->getEntityType(); - if (array_key_exists('format', $params)) { - $format = $params['format']; - } - else { - $format = 'csv'; - } + $format = $params->getFormat() ?? 'csv'; - if (!in_array($format, $this->metadata->get(['app', 'export', 'formatList']))) { - throw new Error('Not supported export format.'); - } + $processor = $this->processorFactory->create($format); - $className = $this->metadata->get(['app', 'export', 'exportFormatClassNameMap', $format]); + $exportAllFields = $params->getFieldList() === null; - if (empty($className)) { - throw new Error(); - } + $collection = $this->getCollection($params); - $exportObj = $this->injectableFactory->create($className); + $attributeListToSkip = $this->acl->getScopeForbiddenAttributeList($entityType, Table::ACTION_READ); - $exportAllFields = !array_key_exists('fieldList', $params); - - $collection = $this->getCollection(); - - $attributeListToSkip = [ - 'deleted', - ]; - - foreach ($this->acl->getScopeForbiddenAttributeList($this->entityType, 'read') as $attribute) { - $attributeListToSkip[] = $attribute; - } + $attributeListToSkip[] = 'deleted'; $attributeList = null; - if (array_key_exists('attributeList', $params)) { + if ($params->getAttributeList() !== null) { $attributeList = []; - $seed = $this->entityManager->getEntity($this->entityType); + $seed = $this->entityManager->getEntity($entityType); - foreach ($params['attributeList'] as $attribute) { + foreach ($params->getAttributeList() as $attribute) { if (in_array($attribute, $attributeListToSkip)) { continue; } @@ -157,34 +138,41 @@ class Export implements } } + $entityDefs = $this->entityManager + ->getDefs() + ->getEntity($entityType); + if ($exportAllFields) { - $fieldDefs = $this->metadata->get(['entityDefs', $this->entityType, 'fields'], []); - $fieldList = array_keys($fieldDefs); + $fieldList = $entityDefs->getFieldNameList(); array_unshift($fieldList, 'id'); } else { - $fieldList = $params['fieldList']; + $fieldList = $params->getFieldList(); } foreach ($fieldList as $i => $field) { - if ($this->metadata->get(['entityDefs', $this->entityType, 'fields', $field, 'exportDisabled'])) { + if ($field === 'id') { + continue; + } + + if ($entityDefs->getField($field)->getParam('exportDisabled')) { unset($fieldList[$i]); } } $fieldList = array_values($fieldList); - if (method_exists($exportObj, 'filterFieldList')) { - $fieldList = $exportObj->filterFieldList($this->entityType, $fieldList, $exportAllFields); + if (method_exists($processor, 'filterFieldList')) { + $fieldList = $processor->filterFieldList($entityType, $fieldList, $exportAllFields); } if (is_null($attributeList)) { $attributeList = []; - $seed = $this->entityManager->getEntity($this->entityType); + $seed = $this->entityManager->getEntity($entityType); - foreach ($seed->getAttributeList() as $attribute) { + foreach ($entityDefs->getAttributeNameList() as $attribute) { if (in_array($attribute, $attributeListToSkip)) { continue; } @@ -197,19 +185,19 @@ class Export implements } } - if (method_exists($exportObj, 'addAdditionalAttributes')) { - $exportObj->addAdditionalAttributes($this->entityType, $attributeList, $fieldList); + if (method_exists($processor, 'addAdditionalAttributes')) { + $processor->addAdditionalAttributes($entityType, $attributeList, $fieldList); } $fp = fopen('php://temp', 'w'); - foreach ($collection as $entity) { - if ($this->recordService) { - $this->recordService->loadAdditionalFieldsForExport($entity); - } + $recordService = $this->serviceContainer->get($entityType); - if (method_exists($exportObj, 'loadAdditionalFields')) { - $exportObj->loadAdditionalFields($entity, $fieldList); + foreach ($collection as $entity) { + $recordService->loadAdditionalFieldsForExport($entity); + + if (method_exists($processor, 'loadAdditionalFields')) { + $processor->loadAdditionalFields($entity, $fieldList); } $row = []; @@ -234,30 +222,27 @@ class Export implements $mimeType = $this->metadata->get(['app', 'export', 'formatDefs', $format, 'mimeType']); $fileExtension = $this->metadata->get(['app', 'export', 'formatDefs', $format, 'fileExtension']); - $fileName = null; + $fileName = $params->getFileName(); - if (!empty($params['fileName'])) { - $fileName = trim($params['fileName']); + if ($fileName !== null) { + $fileName = trim($fileName); } - if (!empty($fileName)) { + if ($fileName) { $fileName = $fileName . '.' . $fileExtension; - } else { - $fileName = "Export_{$this->entityType}." . $fileExtension; + } + else { + $fileName = "Export_{$entityType}." . $fileExtension; } - $exportParams = [ - 'attributeList' => $attributeList, - 'fileName ' => $fileName - ]; + $processorParams = + (new ProcessorParams($fileName, $attributeList, $fieldList)) + ->withName($params->getName()) + ->withEntityType($params->getEntityType()); - $exportParams['fieldList'] = $fieldList; + $processorData = new ProcessorData($fp); - if (array_key_exists('exportName', $params)) { - $exportParams['exportName'] = $params['exportName']; - } - - $contents = $exportObj->process($this->entityType, $exportParams, null, $fp); + $stream = $processor->process($processorParams, $processorData); fclose($fp); @@ -266,11 +251,11 @@ class Export implements $attachment->set('name', $fileName); $attachment->set('role', 'Export File'); $attachment->set('type', $mimeType); - $attachment->set('contents', $contents); + $attachment->set('contents', $stream->getContents()); $this->entityManager->saveEntity($attachment); - return $attachment->getId(); + return new Result($attachment->getId()); } protected function getAttributeFromEntity(Entity $entity, string $attribute) @@ -321,7 +306,7 @@ class Export implements { $defs = $this->entityManager->getDefs(); - $entityDefs = $defs->getEntity($this->entityType); + $entityDefs = $defs->getEntity($entity->getEntityType()); $relation = $entity->getAttributeParam($attribute, 'relation'); $foreign = $entity->getAttributeParam($attribute, 'foreign'); @@ -386,55 +371,26 @@ class Export implements return true; } - private function getCollection(): Collection + private function getCollection(Params $params): Collection { if ($this->collection) { return $this->collection; } - $params = $this->params; + $entityType = $params->getEntityType(); - $selectBuilder = $this->selectBuilderFactory + $searchParams = $params->getSearchParams(); + + $query = $this->selectBuilderFactory ->create() - ->from($this->entityType) - ->withStrictAccessControl(); - - if (array_key_exists('ids', $params)) { - $ids = $params['ids']; - - $queryBuilder = $selectBuilder - ->withDefaultOrder() - ->buildQueryBuilder() - ->where([ - 'id' => $ids, - ]); - } - else if (array_key_exists('where', $params)) { - $where = $params['where']; - - $searchParams = []; - - $searchParams['where'] = $where; - - $selectData = $params['selectData'] ?? []; - - foreach ($selectData as $k => $v) { - $searchParams[$k] = $v; - } - - unset($searchParams['select']); - - $queryBuilder = $selectBuilder - ->withSearchParams(SearchParams::fromRaw($searchParams)) - ->buildQueryBuilder(); - } - else { - throw new BadRequest("Bad export parameters."); - } + ->from($entityType) + ->withSearchParams($searchParams) + ->withStrictAccessControl() + ->build(); return $this->entityManager - ->getRepository($this->entityType) - ->clone($queryBuilder->build()) + ->getRepository($entityType) + ->clone($query) ->sth() ->find(); } diff --git a/application/Espo/Tools/Export/Factory.php b/application/Espo/Tools/Export/Factory.php new file mode 100644 index 0000000000..d25508760d --- /dev/null +++ b/application/Espo/Tools/Export/Factory.php @@ -0,0 +1,49 @@ +injectableFactory = $injectableFactory; + } + + public function create(): Export + { + return $this->injectableFactory->create(Export::class); + } +} diff --git a/application/Espo/Tools/Export/Params.php b/application/Espo/Tools/Export/Params.php new file mode 100644 index 0000000000..a9a05f45a5 --- /dev/null +++ b/application/Espo/Tools/Export/Params.php @@ -0,0 +1,228 @@ +entityType = $entityType; + } + + 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 + ::fromNothing() + ->withWhere([ + [ + 'type' => 'equals', + 'attribute' => 'id', + 'value' => $ids, + ] + ]); + } + + if ($searchParams) { + $actualSearchParams = $searchParams; + + unset($actualSearchParams['select']); + + $obj->searchParams = SearchParams::fromRaw($actualSearchParams); + } + + return $obj; + } + + public static function fromEntityType(string $entityType): self + { + return new self($entityType); + } + + public function withFormat(?string $format): self + { + $obj = clone $this; + + $obj->format = $format; + + return $obj; + } + + 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 withFieldList(?array $fieldList): self + { + $obj = clone $this; + + $obj->fieldList = $fieldList; + + return $obj; + } + + public function withAttributeList(?array $attributeList): self + { + $obj = clone $this; + + $obj->attributeList = $attributeList; + + return $obj; + } + + public function getSearchParams(): SearchParams + { + if (!$this->searchParams) { + return SearchParams::fromNothing(); + } + + return $this->searchParams; + } + + public function getEntityType(): string + { + return $this->entityType; + } + + public function getFileName(): ?string + { + return $this->fileName; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getFormat(): ?string + { + return $this->format; + } + + public function getAttributeList(): ?array + { + return $this->attributeList; + } + + public function getFieldList(): ?array + { + return $this->fieldList; + } +} diff --git a/application/Espo/Tools/Export/Processor.php b/application/Espo/Tools/Export/Processor.php new file mode 100644 index 0000000000..8add389a3f --- /dev/null +++ b/application/Espo/Tools/Export/Processor.php @@ -0,0 +1,37 @@ +resource = $resource; + } + + public function readRow(): ?array + { + $line = fgets($this->resource); + + if ($line === false) { + return null; + } + + return unserialize(base64_decode($line)); + } +} diff --git a/application/Espo/Tools/Export/ProcessorFactory.php b/application/Espo/Tools/Export/ProcessorFactory.php new file mode 100644 index 0000000000..821808ab03 --- /dev/null +++ b/application/Espo/Tools/Export/ProcessorFactory.php @@ -0,0 +1,65 @@ +injectableFactory = $injectableFactory; + $this->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}'."); + } + + $className = $this->metadata->get(['app', 'export', 'processorClassNameMap', $format]); + + if (!$className) { + throw new LogicException("No implementation for format '{$format}'."); + } + + return $this->injectableFactory->create($className); + } +} diff --git a/application/Espo/Tools/Export/ProcessorParams.php b/application/Espo/Tools/Export/ProcessorParams.php new file mode 100644 index 0000000000..af77da998f --- /dev/null +++ b/application/Espo/Tools/Export/ProcessorParams.php @@ -0,0 +1,93 @@ +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; + } + + public function getFileName(): string + { + return $this->fileName; + } + + public function getAttributeList(): array + { + return $this->attributeList; + } + + public function getFieldList(): array + { + return $this->fieldList; + } + + public function getName(): ?string + { + return $this->name; + } + + public function getEntityType(): string + { + return $this->entityType; + } +} diff --git a/application/Espo/Tools/Export/Formats/Csv.php b/application/Espo/Tools/Export/Processors/Csv.php similarity index 59% rename from application/Espo/Tools/Export/Formats/Csv.php rename to application/Espo/Tools/Export/Processors/Csv.php index 78a6a71937..2aa116e7bd 100644 --- a/application/Espo/Tools/Export/Formats/Csv.php +++ b/application/Espo/Tools/Export/Processors/Csv.php @@ -27,24 +27,35 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\Export\Formats; +namespace Espo\Tools\Export\Processors; -use Espo\Core\Exceptions\Error; - -use Espo\Core\ORM\Entity; +use Espo\ORM\Entity; use Espo\Core\{ Utils\Config, Utils\Metadata, + Utils\Json, }; use Espo\Entities\Preferences; -class Csv +use Espo\Tools\Export\{ + ProcessorParams, + ProcessorData, + Processor, +}; + +use Psr\Http\Message\StreamInterface; + +use GuzzleHttp\Psr7\Stream; + +class Csv implements Processor { - protected $config; - protected $preferences; - protected $metadata; + private $config; + + private $preferences; + + private $metadata; public function __construct(Config $config, Preferences $preferences, Metadata $metadata) { @@ -53,13 +64,56 @@ class Csv $this->metadata = $metadata; } - public function loadAdditionalFields(Entity $entity, $fieldList) + public function process(ProcessorParams $params, ProcessorData $data): StreamInterface + { + $attributeList = $params->getAttributeList(); + + $delimiterRaw = + $this->preferences->get('exportDelimiter') ?? + $this->config->get('exportDelimiter') ?? + ','; + + $delimiter = str_replace('\t', "\t", $delimiterRaw); + + $fp = fopen('php://temp', 'w'); + + fputcsv($fp, $attributeList, $delimiter); + + while (($row = $data->readRow()) !== null) { + $preparedRow = $this->prepareRow($row); + + fputcsv($fp, $preparedRow, $delimiter); + } + + rewind($fp); + + return new Stream($fp); + } + + protected function prepareRow(array $row): array + { + $preparedRow = []; + + foreach ($row as $item) { + if (is_array($item) || is_object($item)) { + $item = Json::encode($item); + } + + $preparedRow[] = $item; + } + + return $preparedRow; + } + + public function loadAdditionalFields(Entity $entity, array $fieldList): void { foreach ($fieldList as $field) { - $fieldType = $this->metadata->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']); + $fieldType = $this->metadata + ->get(['entityDefs', $entity->getEntityType(), 'fields', $field, 'type']); if ( - $fieldType === 'linkMultiple' || $fieldType === 'attachmentMultiple' + $fieldType === 'linkMultiple' || + $fieldType === 'attachmentMultiple' ) { if (!$entity->has($field . 'Ids')) { $entity->loadLinkMultipleField($field); @@ -67,54 +121,4 @@ class Csv } } } - - public function process(string $entityType, array $params, ?array $dataList, $dataFp = null) - { - if (!is_array($params['attributeList'])) { - throw new Error(); - } - - $attributeList = $params['attributeList']; - - $delimiter = $this->preferences->get('exportDelimiter'); - if (empty($delimiter)) { - $delimiter = $this->config->get('exportDelimiter', ','); - } - - $delimiter = str_replace('\t', "\t", $delimiter); - - $fp = fopen('php://temp', 'w'); - fputcsv($fp, $attributeList, $delimiter); - - if ($dataFp) { - while (($line = fgets($dataFp)) !== false) { - $row = unserialize(base64_decode($line)); - $preparedRow = $this->prepareRow($row); - fputcsv($fp, $preparedRow, $delimiter); - } - } else { - foreach ($dataList as $row) { - $preparedRow = $this->prepareRow($row); - fputcsv($fp, $preparedRow, $delimiter); - } - } - - rewind($fp); - $csv = stream_get_contents($fp); - fclose($fp); - - return $csv; - } - - protected function prepareRow($row) - { - $preparedRow = []; - foreach ($row as $item) { - if (is_array($item) || is_object($item)) { - $item = \Espo\Core\Utils\Json::encode($item); - } - $preparedRow[] = $item; - } - return $preparedRow; - } } \ No newline at end of file diff --git a/application/Espo/Tools/Export/Formats/Xlsx.php b/application/Espo/Tools/Export/Processors/Xlsx.php similarity index 81% rename from application/Espo/Tools/Export/Formats/Xlsx.php rename to application/Espo/Tools/Export/Processors/Xlsx.php index 06e9350115..ba5ada346b 100644 --- a/application/Espo/Tools/Export/Formats/Xlsx.php +++ b/application/Espo/Tools/Export/Processors/Xlsx.php @@ -27,10 +27,9 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Tools\Export\Formats; +namespace Espo\Tools\Export\Processors; use Espo\ORM\Entity; -use Espo\Core\Exceptions\Error; use Espo\Core\{ Utils\Config, @@ -38,12 +37,21 @@ use Espo\Core\{ Utils\Language, Utils\DateTime as DateTimeUtil, FileStorage\Manager as FileStorageManager, - Utils\File\Manager as FileManager, ORM\EntityManager, Fields\Address, Fields\Address\AddressFormatterFactory, }; +use Espo\Tools\Export\{ + ProcessorParams, + ProcessorData, + Processor, +}; + +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; @@ -55,7 +63,10 @@ use DateTime; use DateTimeZone; use Exception; -class Xlsx +/** + * @todo Refactor. + */ +class Xlsx implements Processor { protected $config; @@ -69,8 +80,6 @@ class Xlsx protected $fileStorageManager; - protected $fileManager; - protected $addressFormatterFactory; public function __construct( @@ -80,7 +89,6 @@ class Xlsx DateTimeUtil $dateTime, EntityManager $entityManager, FileStorageManager $fileStorageManager, - FileManager $fileManager, AddressFormatterFactory $addressFormatterFactory ) { $this->config = $config; @@ -89,136 +97,27 @@ class Xlsx $this->dateTime = $dateTime; $this->entityManager = $entityManager; $this->fileStorageManager = $fileStorageManager; - $this->fileManager = $fileManager; $this->addressFormatterFactory = $addressFormatterFactory; } - public function loadAdditionalFields(Entity $entity, $fieldList) + public function process(ProcessorParams $params, ProcessorData $data): StreamInterface { - foreach ($entity->getRelationList() as $link) { - if (in_array($link, $fieldList)) { - if ($entity->getRelationType($link) === 'belongsToParent') { - if (!$entity->get($link . 'Name')) { - $entity->loadParentNameField($link); - } - } else if ( - ( - ( - $entity->getRelationType($link) === 'belongsTo' - && - $entity->getRelationParam($link, 'noJoin') - ) - || - $entity->getRelationType($link) === 'hasOne' - ) - && - $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']); + $entityType = $params->getEntityType(); - if ($fieldType === 'linkMultiple' || $fieldType === 'attachmentMultiple') { - if (!$entity->has($field . 'Ids')) { - $entity->loadLinkMultipleField($field); - } - } - } - } - - public function filterFieldList($entityType, $fieldList, $exportAllFields) - { - if ($exportAllFields) { - foreach ($fieldList as $i => $field) { - $type = $this->metadata->get(['entityDefs', $entityType, 'fields', $field, 'type']); - if (in_array($type, ['linkMultiple', 'attachmentMultiple'])) { - unset($fieldList[$i]); - } - } - } - - return array_values($fieldList); - } - - public function addAdditionalAttributes($entityType, &$attributeList, $fieldList) - { - $linkList = []; - - if (!in_array('id', $attributeList)) { - $attributeList[] = 'id'; - } - - $linkDefs = $this->metadata->get(['entityDefs', $entityType, 'links']); - - if (is_array($linkDefs)) { - foreach ($linkDefs as $link => $defs) { - if (empty($defs['type'])) { - continue; - } - - if ($defs['type'] === 'belongsToParent') { - $linkList[] = $link; - } - else if ($defs['type'] === 'belongsTo' && !empty($defs['noJoin'])) { - 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 === 'currencyConverted') { - if (!in_array($field, $attributeList)) { - $attributeList[] = $field; - } - } - } - } - - public function process(string $entityType, array $params, ?array $dataList = null, $dataFp = null) - { - if (!is_array($params['fieldList'])) { - throw new Error(); - } + $fieldList = $params->getFieldList(); $phpExcel = new Spreadsheet(); $sheet = $phpExcel->setActiveSheetIndex(0); - if (isset($params['exportName'])) { - $exportName = $params['exportName']; - } - else { - $exportName = $this->language->translate($entityType, 'scopeNamesPlural'); - } + $sheetName = $this->getSheetNameFromParams($params); - $sheetName = mb_substr($exportName, 0, 30, 'utf-8'); - $badCharList = ['*', ':', '/', '\\', '?', '[', ']']; - - foreach ($badCharList as $badChar) { - $sheetName = str_replace($badCharList, ' ', $sheetName); - } - - $sheetName = str_replace('\'', '', $sheetName); + $exportName = + $params->getName() ?? + $this->language->translate($entityType, 'scopeNamesPlural'); $sheet->setTitle($sheetName); - $fieldList = $params['fieldList']; - $titleStyle = [ 'font' => [ 'bold' => true, @@ -247,6 +146,7 @@ class Xlsx $azRange = range('A', 'Z'); $azRangeCopied = $azRange; + foreach ($azRangeCopied as $i => $char1) { foreach ($azRangeCopied as $j => $char2) { $azRange[] = $char1 . $char2; @@ -276,9 +176,13 @@ class Xlsx if (strpos($name, '_') !== false) { list($linkName, $foreignField) = explode('_', $name); + $foreignScope = $this->metadata->get(['entityDefs', $entityType, 'links', $linkName, 'entity']); + if ($foreignScope) { - $label = $this->language->translate($linkName, 'links', $entityType) . '.' . $this->language->translate($foreignField, 'fields', $foreignScope); + $label = + $this->language->translate($linkName, 'links', $entityType) . '.' . + $this->language->translate($foreignField, 'fields', $foreignScope); } } else { @@ -316,28 +220,14 @@ class Xlsx $rowNumber++; $lineIndex = -1; - if ($dataList) { - $lineCount = count($dataList); - } while (true) { $lineIndex++; - if ($dataFp) { - $line = fgets($dataFp); + $row = $data->readRow(); - if ($line === false) { - break; - } - - $row = unserialize(base64_decode($line)); - } - else { - if ($lineIndex >= $lineCount) { - break; - } - - $row = $dataList[$lineIndex]; + if ($row === null) { + break; } $i = 0; @@ -348,7 +238,7 @@ class Xlsx $defs = $this->metadata->get(['entityDefs', $entityType, 'fields', $name]); if (!$defs) { - $defs = array(); + $defs = []; $defs['type'] = 'base'; } @@ -359,43 +249,51 @@ class Xlsx if (strpos($name, '_') !== false) { list($linkName, $foreignField) = explode('_', $name); - $foreignScope = $this->metadata->get(['entityDefs', $entityType, 'links', $linkName, 'entity']); + $foreignScope = $this->metadata + ->get(['entityDefs', $entityType, 'links', $linkName, 'entity']); if ($foreignScope) { - $type = $this->metadata->get(['entityDefs', $foreignScope, 'fields', $foreignField, 'type'], $type); + $type = $this->metadata + ->get(['entityDefs', $foreignScope, 'fields', $foreignField, 'type'], $type); } } if ($type === 'foreign') { - $linkName = $this->metadata->get(['entityDefs', $entityType, 'fields', $name, 'link']); - $foreignField = $this->metadata->get(['entityDefs', $entityType, 'fields', $name, 'field']); - $foreignScope = $this->metadata->get(['entityDefs', $entityType, 'links', $linkName, 'entity']); + $linkName = $this->metadata + ->get(['entityDefs', $entityType, 'fields', $name, 'link']); + + $foreignField = $this->metadata + ->get(['entityDefs', $entityType, 'fields', $name, 'field']); + + $foreignScope = $this->metadata + ->get(['entityDefs', $entityType, 'links', $linkName, 'entity']); if ($foreignScope) { - $type = $this->metadata->get(['entityDefs', $foreignScope, 'fields', $foreignField, 'type'], $type); + $type = $this->metadata + ->get(['entityDefs', $foreignScope, 'fields', $foreignField, 'type'], $type); } } $typesCache[$name] = $type; $link = null; - if ($type == 'link') { + if ($type === 'link') { if (array_key_exists($name.'Name', $row)) { $sheet->setCellValue("$col$rowNumber", $row[$name.'Name']); } } - else if ($type == 'linkParent') { + else if ($type === 'linkParent') { if (array_key_exists($name.'Name', $row)) { $sheet->setCellValue("$col$rowNumber", $row[$name.'Name']); } } - else if ($type == 'int') { + else if ($type === 'int') { $sheet->setCellValue("$col$rowNumber", $row[$name] ?: 0); } - else if ($type == 'float') { + else if ($type === 'float') { $sheet->setCellValue("$col$rowNumber", $row[$name] ?: 0); } - else if ($type == 'currency') { + else if ($type === 'currency') { if (array_key_exists($name.'Currency', $row) && array_key_exists($name, $row)) { $sheet->setCellValue("$col$rowNumber", $row[$name] ? $row[$name] : ''); @@ -408,7 +306,7 @@ class Xlsx ); } } - else if ($type == 'currencyConverted') { + else if ($type === 'currencyConverted') { if (array_key_exists($name, $row)) { $currency = $this->config->get('defaultCurrency'); @@ -421,7 +319,7 @@ class Xlsx $sheet->setCellValue("$col$rowNumber", $row[$name] ? $row[$name] : ''); } } - else if ($type == 'personName') { + else if ($type === 'personName') { if (!empty($row['name'])) { $sheet->setCellValue("$col$rowNumber", $row['name']); } else { @@ -435,18 +333,21 @@ class Xlsx } $personName .= $row['lastName']; } - $sheet->setCellValue("$col$rowNumber", $personName); + $sheet->setCellValue($col . $rowNumber, $personName); } } - else if ($type == 'date') { + else if ($type === 'date') { if (isset($row[$name])) { - $sheet->setCellValue("$col$rowNumber", SharedDate::PHPToExcel(strtotime($row[$name]))); + $sheet->setCellValue( + $col . $rowNumber, + SharedDate::PHPToExcel(strtotime($row[$name])) + ); } } - else if ($type == 'datetime' || $type == 'datetimeOptional') { + else if ($type === 'datetime' || $type === 'datetimeOptional') { $value = null; - if ($type == 'datetimeOptional') { + if ($type === 'datetimeOptional') { if (isset($row[$name . 'Date']) && $row[$name . 'Date']) { $value = $row[$name . 'Date']; } @@ -463,6 +364,7 @@ class Xlsx $timeZone = $this->config->get('timeZone'); $dt = new DateTime($value); + $dt->setTimezone(new DateTimeZone($timeZone)); $value = $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT); @@ -476,7 +378,7 @@ class Xlsx $sheet->setCellValue("$col$rowNumber", SharedDate::PHPToExcel(strtotime($value))); } } - else if ($type == 'image') { + else if ($type === 'image') { if (isset($row[$name . 'Id']) && $row[$name . 'Id']) { $attachment = $this->entityManager->getEntity('Attachment', $row[$name . 'Id']); @@ -495,12 +397,12 @@ class Xlsx } } - else if ($type == 'file') { + else if ($type === 'file') { if (array_key_exists($name.'Name', $row)) { $sheet->setCellValue("$col$rowNumber", $row[$name.'Name']); } } - else if ($type == 'enum') { + else if ($type === 'enum') { if (array_key_exists($name, $row)) { if ($linkName) { $value = $this->language->translateOption($row[$name], $foreignField, $foreignScope); @@ -512,7 +414,7 @@ class Xlsx $sheet->setCellValue("$col$rowNumber", $value); } } - else if ($type == 'linkMultiple' || $type == 'attachmentMultiple') { + else if ($type === 'linkMultiple' || $type === 'attachmentMultiple') { if (array_key_exists($name . 'Ids', $row) && array_key_exists($name . 'Names', $row)) { $nameList = []; @@ -529,7 +431,7 @@ class Xlsx $sheet->setCellValue("$col$rowNumber", implode(', ', $nameList)); } } - else if ($type == 'address') { + else if ($type === 'address') { $address = Address::createBuilder() ->setStreet($row[$name . 'Street'] ?? null) ->setCity($row[$name . 'City'] ?? null) @@ -579,7 +481,7 @@ class Xlsx $sheet->setCellValue("$col$rowNumber", $value); } } - else if ($type == 'multiEnum' || $type == 'array') { + else if ($type === 'multiEnum' || $type === 'array') { if (!empty($row[$name])) { $array = json_decode($row[$name]); @@ -621,17 +523,17 @@ class Xlsx list($foreignLink, $foreignField) = explode('_', $name); } - if ($name == 'name') { + if ($name === 'name') { if (array_key_exists('id', $row)) { $link = $this->config->getSiteUrl() . "/#".$entityType . "/view/" . $row['id']; } } - else if ($type == 'url') { + else if ($type === 'url') { if (array_key_exists($name, $row) && filter_var($row[$name], FILTER_VALIDATE_URL)) { $link = $row[$name]; } } - else if ($type == 'link') { + else if ($type === 'link') { if (array_key_exists($name.'Id', $row)) { $foreignEntity = null; @@ -651,27 +553,29 @@ class Xlsx } if ($foreignEntity) { - $link = $this->config->getSiteUrl() . "/#" . $foreignEntity. "/view/". $row[$name.'Id']; + $link = + $this->config->getSiteUrl() . + "/#" . $foreignEntity. "/view/". $row[$name.'Id']; } } } - else if ($type == 'file') { + else if ($type === 'file') { if (array_key_exists($name.'Id', $row)) { $link = $this->config->getSiteUrl() . "/?entryPoint=download&id=" . $row[$name.'Id']; } } - else if ($type == 'linkParent') { + else if ($type === 'linkParent') { if (array_key_exists($name.'Id', $row) && array_key_exists($name.'Type', $row)) { $link = $this->config->getSiteUrl() . "/#".$row[$name.'Type']."/view/". $row[$name.'Id']; } } - else if ($type == 'phone') { + else if ($type === 'phone') { if (array_key_exists($name, $row)) { $link = "tel:".$row[$name]; } } - else if ($type == 'email' && array_key_exists($name, $row)) { + else if ($type === 'email' && array_key_exists($name, $row)) { if (array_key_exists($name, $row)) { $link = "mailto:".$row[$name]; } @@ -753,28 +657,25 @@ class Xlsx ]; foreach ($linkColList as $linkColumn) { - $sheet->getStyle($linkColumn.$startingRowNumber.':'.$linkColumn.$rowNumber)->applyFromArray($linkStyle); + $sheet + ->getStyle($linkColumn.$startingRowNumber . ':' . $linkColumn.$rowNumber) + ->applyFromArray($linkStyle); } $objWriter = IOFactory::createWriter($phpExcel, 'Xlsx'); - if (!$this->fileManager->isDir('data/cache/')) { - $this->fileManager->mkdir('data/cache/'); - } + $resource = fopen('php://temp', 'r+'); - $tempFileName = 'data/cache/' . 'export_' . substr(md5(rand()), 0, 7); + $objWriter->save($resource); - $objWriter->save($tempFileName); + $stream = new Stream($resource); - $fp = fopen($tempFileName, 'r'); - $xlsx = stream_get_contents($fp); + $stream->seek(0); - $this->fileManager->unlink($tempFileName); - - return $xlsx; + return $stream; } - protected function getCurrencyFormatCode(string $currency) : string + protected function getCurrencyFormatCode(string $currency): string { $currencySymbol = $this->metadata->get(['app', 'currency', 'symbolMap', $currency], ''); @@ -786,4 +687,120 @@ class Xlsx return '[$'.$currencySymbol.'-409]#,##0.00;-[$'.$currencySymbol.'-409]#,##0.00'; } + + public function loadAdditionalFields(Entity $entity, array $fieldList): void + { + foreach ($entity->getRelationList() as $link) { + if (!in_array($link, $fieldList)) { + continue; + } + + if ($entity->getRelationType($link) === 'belongsToParent') { + if (!$entity->get($link . 'Name')) { + $entity->loadParentNameField($link); + } + } + else if ( + ( + ( + $entity->getRelationType($link) === 'belongsTo' && + $entity->getRelationParam($link, 'noJoin') + ) || + $entity->getRelationType($link) === 'hasOne' + ) && + $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 === 'linkMultiple' || $fieldType === 'attachmentMultiple') { + if (!$entity->has($field . 'Ids')) { + $entity->loadLinkMultipleField($field); + } + } + } + } + + public 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, ['linkMultiple', 'attachmentMultiple'])) { + unset($fieldList[$i]); + } + } + } + + return array_values($fieldList); + } + + public function addAdditionalAttributes(string $entityType, array &$attributeList, array $fieldList): void + { + $linkList = []; + + if (!in_array('id', $attributeList)) { + $attributeList[] = 'id'; + } + + $linkDefs = $this->metadata->get(['entityDefs', $entityType, 'links']); + + if (is_array($linkDefs)) { + foreach ($linkDefs as $link => $defs) { + if (empty($defs['type'])) { + continue; + } + + if ($defs['type'] === 'belongsToParent') { + $linkList[] = $link; + } + else if ($defs['type'] === 'belongsTo' && !empty($defs['noJoin'])) { + 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 === 'currencyConverted') { + if (!in_array($field, $attributeList)) { + $attributeList[] = $field; + } + } + } + } + + protected function getSheetNameFromParams(ProcessorParams $params): string + { + $exportName = + $params->getName() ?? + $this->language->translate($params->getEntityType(), 'scopeNamesPlural'); + + $badCharList = ['*', ':', '/', '\\', '?', '[', ']']; + + $sheetName = mb_substr($exportName, 0, 30, 'utf-8'); + + $sheetName = str_replace($badCharList, ' ', $sheetName); + + $sheetName = str_replace('\'', '', $sheetName); + + return $sheetName; + } } diff --git a/application/Espo/Tools/Export/Result.php b/application/Espo/Tools/Export/Result.php new file mode 100644 index 0000000000..d3e13995d7 --- /dev/null +++ b/application/Espo/Tools/Export/Result.php @@ -0,0 +1,45 @@ +attachmentId = $attachmentId; + } + + public function getAttachmentId(): string + { + return $this->attachmentId; + } +} diff --git a/application/Espo/Tools/Export/Service.php b/application/Espo/Tools/Export/Service.php new file mode 100644 index 0000000000..2f89e87114 --- /dev/null +++ b/application/Espo/Tools/Export/Service.php @@ -0,0 +1,94 @@ +factory = $factory; + $this->config = $config; + $this->acl = $acl; + $this->user = $user; + $this->metadata = $metadata; + } + + public function process(Params $params): Result + { + if ($this->config->get('exportDisabled') && !$this->user->isAdmin()) { + throw new ForbiddenSilent("Export disabled for non-admin users."); + } + + $entityType = $params->getEntityType(); + + if ($this->acl->getPermissionLevel('exportPermission') !== 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}'."); + } + + $export = $this->factory->create(); + + return $export + ->setParams($params) + ->run(); + } +} diff --git a/client/src/views/record/list.js b/client/src/views/record/list.js index d755dbb378..bc55a0c807 100644 --- a/client/src/views/record/list.js +++ b/client/src/views/record/list.js @@ -539,18 +539,20 @@ define('views/record/list', 'view', function (Dep) { export: function (data, url, fieldList) { if (!data) { - data = {}; + data = { + entityType: this.entityType, + }; if (this.allResultIsChecked) { data.where = this.collection.getWhere(); - data.selectData = this.collection.data || {}; - data.byWhere = true; - } else { + data.searchData = this.collection.data || {}; + } + else { data.ids = this.checkedList; } } - var url = url || this.entityType + '/action/export'; + var url = url || 'Export'; var o = { scope: this.entityType @@ -584,13 +586,16 @@ define('views/record/list', 'view', function (Dep) { Espo.Ui.notify(this.translate('pleaseWait', 'messages')); - Espo.Ajax.postRequest(url, data, {timeout: 0}).then(function (data) { - Espo.Ui.notify(false); + Espo.Ajax.postRequest(url, data, {timeout: 0}) + .then( + function (data) { + Espo.Ui.notify(false); - if ('id' in data) { - window.location = this.getBasePath() + '?entryPoint=download&id=' + data.id; - } - }.bind(this)); + if ('id' in data) { + window.location = this.getBasePath() + '?entryPoint=download&id=' + data.id; + } + }.bind(this) + ); }, this); }, this); },