diff --git a/application/Espo/Core/Utils/FieldUtil.php b/application/Espo/Core/Utils/FieldUtil.php index 958b0c391c..a495dd308a 100644 --- a/application/Espo/Core/Utils/FieldUtil.php +++ b/application/Espo/Core/Utils/FieldUtil.php @@ -124,24 +124,33 @@ class FieldUtil return $attributeList; } - public function getActualAttributeList(string $entityType, string $name): array + /** + * Get storable attributes of a specific field. + */ + public function getActualAttributeList(string $entityType, string $field): array { return array_merge( - $this->getAttributeListByType($entityType, $name, 'actual'), - $this->getAdditionalActualAttributeList($entityType, $name) + $this->getAttributeListByType($entityType, $field, 'actual'), + $this->getAdditionalActualAttributeList($entityType, $field) ); } - public function getNotActualAttributeList(string $entityType, string $name): array + /** + * Get non-storable attributes of a specific field. + */ + public function getNotActualAttributeList(string $entityType, string $field): array { - return $this->getAttributeListByType($entityType, $name, 'notActual'); + return $this->getAttributeListByType($entityType, $field, 'notActual'); } - public function getAttributeList(string $entityType, string $name): array + /** + * Get attributes of a specific field. + */ + public function getAttributeList(string $entityType, string $field): array { return array_merge( - $this->getActualAttributeList($entityType, $name), - $this->getNotActualAttributeList($entityType, $name) + $this->getActualAttributeList($entityType, $field), + $this->getNotActualAttributeList($entityType, $field) ); } diff --git a/application/Espo/Tools/Export/Export.php b/application/Espo/Tools/Export/Export.php index 9cbd94c7f4..8a3ba0d9a5 100644 --- a/application/Espo/Tools/Export/Export.php +++ b/application/Espo/Tools/Export/Export.php @@ -35,11 +35,13 @@ use Espo\Core\{ Select\SelectBuilderFactory, Acl, Acl\Table, + Acl\GlobalRestricton, Record\ServiceContainer, Utils\Metadata, FileStorage\Manager as FileStorageManager, FieldProcessing\ListLoadProcessor, FieldProcessing\LoaderParams, + Utils\FieldUtil, }; use Espo\{ @@ -48,10 +50,18 @@ use Espo\{ ORM\EntityManager, }; +use RuntimeException; + class Export { + /** + * @var Params + */ private $params; + /** + * @var Collection + */ private $collection = null; private $processorFactory; @@ -70,6 +80,8 @@ class Export private $listLoadProcessor; + private $fieldUtil; + public function __construct( ProcessorFactory $processorFactory, SelectBuilderFactory $selectBuilderFactor, @@ -78,7 +90,8 @@ class Export EntityManager $entityManager, Metadata $metadata, FileStorageManager $fileStorageManager, - ListLoadProcessor $listLoadProcessor + ListLoadProcessor $listLoadProcessor, + FieldUtil $fieldUtil ) { $this->processorFactory = $processorFactory; $this->selectBuilderFactory = $selectBuilderFactor; @@ -88,6 +101,7 @@ class Export $this->metadata = $metadata; $this->fileStorageManager = $fileStorageManager; $this->listLoadProcessor = $listLoadProcessor; + $this->fieldUtil = $fieldUtil; } public function setParams(Params $params): self @@ -121,82 +135,13 @@ class Export $processor = $this->processorFactory->create($format); - $exportAllFields = $params->getFieldList() === null; - $collection = $this->getCollection($params); - $attributeListToSkip = $this->acl->getScopeForbiddenAttributeList($entityType, Table::ACTION_READ); + $attributeList = $this->getAttributeList($params); - $attributeListToSkip[] = 'deleted'; + $fieldList = $this->getFieldList($params, $processor); - $attributeList = null; - - if ($params->getAttributeList() !== null) { - $attributeList = []; - - $seed = $this->entityManager->getEntity($entityType); - - foreach ($params->getAttributeList() as $attribute) { - if (in_array($attribute, $attributeListToSkip)) { - continue; - } - - if (!$this->checkAttributeIsAllowedForExport($seed, $attribute)) { - continue; - } - - $attributeList[] = $attribute; - } - } - - $entityDefs = $this->entityManager - ->getDefs() - ->getEntity($entityType); - - if ($exportAllFields) { - $fieldList = $entityDefs->getFieldNameList(); - - array_unshift($fieldList, 'id'); - } - else { - $fieldList = $params->getFieldList(); - } - - foreach ($fieldList as $i => $field) { - if ($field === 'id') { - continue; - } - - if ($entityDefs->getField($field)->getParam('exportDisabled')) { - unset($fieldList[$i]); - } - } - - $fieldList = array_values($fieldList); - - if (method_exists($processor, 'filterFieldList')) { - $fieldList = $processor->filterFieldList($entityType, $fieldList, $exportAllFields); - } - - if (is_null($attributeList)) { - $attributeList = []; - - $seed = $this->entityManager->getEntity($entityType); - - foreach ($entityDefs->getAttributeNameList() as $attribute) { - if (in_array($attribute, $attributeListToSkip)) { - continue; - } - - if (!$this->checkAttributeIsAllowedForExport($seed, $attribute, true)) { - continue; - } - - $attributeList[] = $attribute; - } - } - - if (method_exists($processor, 'addAdditionalAttributes')) { + if ($fieldList !== null && method_exists($processor, 'addAdditionalAttributes')) { $processor->addAdditionalAttributes($entityType, $attributeList, $fieldList); } @@ -215,7 +160,7 @@ class Export $recordService->loadAdditionalFieldsForExport($entity); } - if (method_exists($processor, 'loadAdditionalFields')) { + if (method_exists($processor, 'loadAdditionalFields') && $fieldList !== null) { $processor->loadAdditionalFields($entity, $fieldList); } @@ -234,10 +179,6 @@ class Export rewind($dataResource); - if (is_null($attributeList)) { - $attributeList = []; - } - $mimeType = $this->metadata->get(['app', 'export', 'formatDefs', $format, 'mimeType']); $fileExtension = $this->metadata->get(['app', 'export', 'formatDefs', $format, 'fileExtension']); @@ -366,17 +307,17 @@ class Export protected function checkAttributeIsAllowedForExport( Entity $entity, string $attribute, - bool $isExportAllFields = false + bool $exportAllFields = false ): bool { - if (!$isExportAllFields) { - return true; - } - if ($entity->getAttributeParam($attribute, 'notExportable')) { return false; } + if (!$exportAllFields) { + return true; + } + if ($entity->getAttributeParam($attribute, 'isLinkMultipleIdList')) { return false; } @@ -402,12 +343,16 @@ class Export $searchParams = $params->getSearchParams(); - $query = $this->selectBuilderFactory + $builder = $this->selectBuilderFactory ->create() ->from($entityType) - ->withSearchParams($searchParams) - ->withStrictAccessControl() - ->build(); + ->withSearchParams($searchParams); + + if ($params->applyAccessControl()) { + $builder->withStrictAccessControl(); + } + + $query = $builder->build(); return $this->entityManager ->getRepository($entityType) @@ -415,4 +360,107 @@ class Export ->sth() ->find(); } + + private function getAttributeList(Params $params): array + { + $list = []; + + $entityType = $params->getEntityType(); + + $entityDefs = $this->entityManager + ->getDefs() + ->getEntity($entityType); + + $attributeListToSkip = $params->applyAccessControl() ? + $this->acl->getScopeForbiddenAttributeList($entityType, Table::ACTION_READ) : + $this->acl->getScopeRestrictedAttributeList($entityType, [ + GlobalRestricton::TYPE_FORBIDDEN, + GlobalRestricton::TYPE_INTERNAL, + ]); + + $attributeListToSkip[] = 'deleted'; + + $seed = $this->entityManager->getEntity($entityType); + + $initialAttributeList = $params->getAttributeList(); + + if ($params->getAttributeList() === null && $params->getFieldList() !== null) { + $initialAttributeList = $this->getAttributeListFromFieldList($params); + } + + if ($params->getAttributeList() === null && $params->getFieldList() === null) { + $initialAttributeList = $entityDefs->getAttributeNameList(); + } + + foreach ($initialAttributeList as $attribute) { + if (in_array($attribute, $attributeListToSkip)) { + continue; + } + + if (!$this->checkAttributeIsAllowedForExport($seed, $attribute, $params->allFields())) { + continue; + } + + $list[] = $attribute; + } + + return $list; + } + + 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; + } + + private function getFieldList(Params $params, Processor $processor): ?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->getField($field)->getParam('exportDisabled')) { + unset($fieldList[$i]); + } + } + + if (method_exists($processor, 'filterFieldList')) { + $fieldList = $processor->filterFieldList($params->getEntityType(), $fieldList, $params->allFields()); + } + + return array_values($fieldList); + } } diff --git a/application/Espo/Tools/Export/Params.php b/application/Espo/Tools/Export/Params.php index 68a763fe3b..1e62abba89 100644 --- a/application/Espo/Tools/Export/Params.php +++ b/application/Espo/Tools/Export/Params.php @@ -52,6 +52,8 @@ class Params private $searchParams = null; + private $applyAccessControl = true; + public function __construct(string $entityType) { $this->entityType = $entityType; @@ -188,6 +190,18 @@ class Params return $obj; } + public function withAccessControl(bool $applyAccessControl = true): self + { + $obj = clone $this; + + $obj->applyAccessControl = $applyAccessControl; + + return $obj; + } + + /** + * Get search params. + */ public function getSearchParams(): SearchParams { if (!$this->searchParams) { @@ -197,33 +211,67 @@ class Params return $this->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. + */ public function getAttributeList(): ?array { return $this->attributeList; } + /** + * Get fields to be exported. + */ public function getFieldList(): ?array { return $this->fieldList; } + + /** + * 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; + } } diff --git a/application/Espo/Tools/Export/ProcessorParams.php b/application/Espo/Tools/Export/ProcessorParams.php index af77da998f..6eaa9e2cd1 100644 --- a/application/Espo/Tools/Export/ProcessorParams.php +++ b/application/Espo/Tools/Export/ProcessorParams.php @@ -33,15 +33,15 @@ class ProcessorParams { private $fileName; - private $attributeList; + private $attributeList = null; - private $fieldList; + private $fieldList = null; private $name = null; private $entityType = null; - public function __construct(string $fileName, array $attributeList, array $fieldList) + public function __construct(string $fileName, array $attributeList, ?array $fieldList) { $this->fileName = $fileName; $this->attributeList = $attributeList; @@ -76,7 +76,7 @@ class ProcessorParams return $this->attributeList; } - public function getFieldList(): array + public function getFieldList(): ?array { return $this->fieldList; } diff --git a/application/Espo/Tools/Export/Processors/Xlsx.php b/application/Espo/Tools/Export/Processors/Xlsx.php index ba5ada346b..cb7774533c 100644 --- a/application/Espo/Tools/Export/Processors/Xlsx.php +++ b/application/Espo/Tools/Export/Processors/Xlsx.php @@ -62,6 +62,7 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use DateTime; use DateTimeZone; use Exception; +use RuntimeException; /** * @todo Refactor. @@ -106,6 +107,10 @@ class Xlsx implements Processor $fieldList = $params->getFieldList(); + if ($fieldList === null) { + throw new RuntimeException("Field list is required"); + } + $phpExcel = new Spreadsheet(); $sheet = $phpExcel->setActiveSheetIndex(0); diff --git a/tests/integration/Espo/Export/ExportTest.php b/tests/integration/Espo/Export/ExportTest.php new file mode 100644 index 0000000000..1067e2b55a --- /dev/null +++ b/tests/integration/Espo/Export/ExportTest.php @@ -0,0 +1,249 @@ +entityManager = $this->getContainer()->get('entityManager'); + + $this->factory = $this->getContainer() + ->get('injectableFactory') + ->create(Factory::class); + + $this->fileStorageManager = $this->getContainer()->get('fileStorageManager'); + } + + public function testCsvWithFieldList(): void + { + $user =$this->entityManager->createEntity('User', [ + 'id' => 'user-id', + 'userName' => 'user', + 'lastName' => 'User', + ]); + + $this->entityManager->createEntity('Task', [ + 'id' => '1', + 'name' => 'test-1', + 'assignedUserId' => $user->getId(), + ]); + + $this->entityManager->createEntity('Task', [ + 'id' => '2', + 'name' => 'test-2', + 'assignedUserId' => $user->getId(), + ]); + + $this->entityManager->createEntity('Task', [ + 'id' => '3', + 'name' => 'test-3', + ]); + + $searchParams = SearchParams + ::fromNothing() + ->withWhere(WhereItem::fromRaw([ + 'type' => 'equals', + 'attribute' => 'assignedUserId', + 'value' => $user->getId(), + ])); + + $params = Params + ::fromEntityType('Task') + ->withFieldList([ + 'name', + 'assignedUser', + ]) + ->withSearchParams($searchParams) + ->withFormat('csv'); + + $export = $this->factory->create(); + + $attachmentId = $export + ->setParams($params) + ->run() + ->getAttachmentId(); + + $attachment = $this->entityManager->getEntity('Attachment', $attachmentId); + + $contents = $this->fileStorageManager->getContents($attachment); + + $exepectedContents = + "name,assignedUserId,assignedUserName\n" . + "test-2,user-id,User\n" . + "test-1,user-id,User\n"; + + $this->assertEquals($exepectedContents, $contents); + } + + public function testCsvWithAttributeList(): void + { + $user = $this->entityManager->createEntity('User', [ + 'id' => 'user-id', + 'userName' => 'user', + 'lastName' => 'User', + ]); + + $this->entityManager->createEntity('Task', [ + 'id' => '1', + 'name' => 'test-1', + 'assignedUserId' => $user->getId(), + ]); + + $this->entityManager->createEntity('Task', [ + 'id' => '2', + 'name' => 'test-2', + 'assignedUserId' => $user->getId(), + ]); + + $params = Params + ::fromEntityType('Task') + ->withAttributeList([ + 'id', + 'name', + 'assignedUserId', + ]) + ->withAccessControl(false) + ->withFormat('csv'); + + $export = $this->factory->create(); + + $attachmentId = $export + ->setParams($params) + ->run() + ->getAttachmentId(); + + $attachment = $this->entityManager->getEntity('Attachment', $attachmentId); + + $contents = $this->fileStorageManager->getContents($attachment); + + $exepectedContents = + "id,name,assignedUserId\n" . + "2,test-2,user-id\n" . + "1,test-1,user-id\n"; + + $this->assertEquals($exepectedContents, $contents); + } + + public function testCsvCollection(): void + { + $user = $this->entityManager->createEntity('User', [ + 'id' => 'user-id', + 'userName' => 'user', + 'lastName' => 'User', + ]); + + $this->entityManager->createEntity('Task', [ + 'id' => '1', + 'name' => 'test-1', + 'assignedUserId' => $user->getId(), + ]); + + $this->entityManager->createEntity('Task', [ + 'id' => '2', + 'name' => 'test-2', + 'assignedUserId' => $user->getId(), + ]); + + $this->entityManager->createEntity('Task', [ + 'id' => '3', + 'name' => 'test-3', + ]); + + $collection = $this->entityManager + ->getRDBRepository('Task') + ->where([ + 'assignedUserId' => $user->getId(), + ]) + ->order('id', 'ASC') + ->find(); + + $params = Params + ::fromEntityType('Task') + ->withAttributeList([ + 'name', + 'assignedUserId', + ]) + ->withFieldList([ + 'name', + 'assignedUser', + ]) + ->withFormat('csv'); + + $export = $this->factory->create(); + + $attachmentId = $export + ->setParams($params) + ->setCollection($collection) + ->run() + ->getAttachmentId(); + + $attachment = $this->entityManager->getEntity('Attachment', $attachmentId); + + $contents = $this->fileStorageManager->getContents($attachment); + + $exepectedContents = + "name,assignedUserId\n" . + "test-1,user-id\n" . + "test-2,user-id\n"; + + $this->assertEquals($exepectedContents, $contents); + } +}