diff --git a/composer.json b/composer.json index c47ca351c..60900a30b 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "league/csv": "^9.22", "nesbot/carbon": "^3.8.4", "pimcore/static-resolver-bundle": "^3.3.0", - "pimcore/generic-data-index-bundle": "^2.3.0", + "pimcore/generic-data-index-bundle": "^2.4.0", "pimcore/pimcore": "^12.3", "zircote/swagger-php": "^4.8 || ^5.0", "ext-zip": "*", diff --git a/config/gdpr.yaml b/config/gdpr.yaml index be17c5704..9e8aae2f4 100644 --- a/config/gdpr.yaml +++ b/config/gdpr.yaml @@ -25,3 +25,16 @@ services: tags: ["pimcore.studio_backend.gdpr_data_provider"] arguments: $logsDir: "%kernel.logs_dir%" + + Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataObjectProvider: + tags: ["pimcore.studio_backend.gdpr_data_provider"] + + Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\AssetsProvider: + tags: ["pimcore.studio_backend.gdpr_data_provider"] + + # --- Legacy Code from the admin-ui-classic-bundle --- + Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\Legacy\ObjectExporterInterface: + class: Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\Legacy\ObjectExporter + + Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\Legacy\AssetExporterInterface: + class: Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\Legacy\AssetExporter diff --git a/doc/10_Extending_Studio/11_Gdpr.md b/doc/10_Extending_Studio/11_Gdpr.md index 9d047993a..6c4059902 100644 --- a/doc/10_Extending_Studio/11_Gdpr.md +++ b/doc/10_Extending_Studio/11_Gdpr.md @@ -48,9 +48,40 @@ Instead of handling deletion logic inside the provider, you simply **point** to 2. This returns the unique **Operation ID** that handles deleting specific type of item. 3. When the user confirms, the frontend calls that API endpoint using the item's ID. +## Configuration + +The GDPR Data Extractor can be configured. The following options are available: + +```yaml +pimcore_studio_backend: + gdpr_data_extractor: + dataObjects: + classes: + # Configure which classes should be considered + # Array key is the class name + Person: + allowDelete: true # Allow delete of objects directly in preview grid (default: false) + Customer: + allowDelete: false + assets: + types: + # Configure which asset types should be considered + - image + - document + - video +``` + +### Configuration Options + +| Option | Type | Default | Description | +|-------------------------------------------------------------------|---------|---------|------------------------------------------------------------------------------------------------------------| +| `gdpr_data_extractor.dataObjects.classes` | array | `[]` | Configure which Data Object classes should be considered for GDPR search. The array key is the class name. | +| `gdpr_data_extractor.dataObjects.classes..allowDelete` | boolean | `false` | Allow deletion of objects directly in the preview grid. | +| `gdpr_data_extractor.assets.types` | array | `[]` | Configure which asset types should be considered for GDPR search (e.g., `image`, `document`, `video`). | + ## Example Data Provider -Example below shows some of the important functions with their implementations +The example below shows some of the important functions with their implementations ```php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1339890a6..efca79ee2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,2 +1,6 @@ parameters: - ignoreErrors: \ No newline at end of file + ignoreErrors: + - + message: "#^Constructor of class Pimcore\\\\Bundle\\\\StudioBackendBundle\\\\Gdpr\\\\Provider\\\\PimcoreUserProvider has an unused parameter \\$gdprConfig\\.$#" + count: 1 + path: src/Gdpr/Provider/PimcoreUserProvider.php \ No newline at end of file diff --git a/src/DataIndex/Query/AssetQuery.php b/src/DataIndex/Query/AssetQuery.php index 730005502..1fe556727 100644 --- a/src/DataIndex/Query/AssetQuery.php +++ b/src/DataIndex/Query/AssetQuery.php @@ -32,6 +32,7 @@ use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Tree\TagFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\ElementKeySearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\FullTextSearch; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\MultiMatchSearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\WildcardSearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\QueryLanguage\PqlFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Sort\OrderByField; @@ -220,6 +221,17 @@ public function filterFullText(string $value): QueryInterface return $this; } + public function filterMultiMatch( + string $searchTerm, + array $fields = [], + string $type = 'best_fields', + string $operator = 'or' + ): QueryInterface { + $this->search->addModifier(new MultiMatchSearch($searchTerm, $fields, $type, $operator)); + + return $this; + } + public function filterNumber( string $fieldName, int|float $searchTerm, diff --git a/src/DataIndex/Query/DataObjectQuery.php b/src/DataIndex/Query/DataObjectQuery.php index f56e1ad97..7765b6c7a 100644 --- a/src/DataIndex/Query/DataObjectQuery.php +++ b/src/DataIndex/Query/DataObjectQuery.php @@ -35,6 +35,7 @@ use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Tree\TagFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\ElementKeySearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\FullTextSearch; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\MultiMatchSearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\WildcardSearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\QueryLanguage\PqlFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Sort\OrderByField; @@ -215,6 +216,17 @@ public function filterFullText(string $value): QueryInterface return $this; } + public function filterMultiMatch( + string $searchTerm, + array $fields = [], + string $type = 'best_fields', + string $operator = 'or' + ): QueryInterface { + $this->search->addModifier(new MultiMatchSearch($searchTerm, $fields, $type, $operator)); + + return $this; + } + public function orderByField(string $fieldName, SortDirection $direction): self { $this->search->addModifier(new OrderByField($fieldName, $direction)); diff --git a/src/DataIndex/Query/DocumentQuery.php b/src/DataIndex/Query/DocumentQuery.php index ecc3e37f4..18686a4a8 100644 --- a/src/DataIndex/Query/DocumentQuery.php +++ b/src/DataIndex/Query/DocumentQuery.php @@ -32,6 +32,7 @@ use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Filter\Tree\TagFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\ElementKeySearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\FullTextSearch; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\MultiMatchSearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\FullTextSearch\WildcardSearch; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\QueryLanguage\PqlFilter; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\Modifier\Sort\OrderByField; @@ -190,6 +191,17 @@ public function filterFullText(string $value): QueryInterface return $this; } + public function filterMultiMatch( + string $searchTerm, + array $fields = [], + string $type = 'best_fields', + string $operator = 'or' + ): QueryInterface { + $this->search->addModifier(new MultiMatchSearch($searchTerm, $fields, $type, $operator)); + + return $this; + } + public function filterDatetime( string $field, Carbon|int|null $startDate = null, diff --git a/src/DataIndex/Query/QueryInterface.php b/src/DataIndex/Query/QueryInterface.php index f93b9b390..02202c329 100644 --- a/src/DataIndex/Query/QueryInterface.php +++ b/src/DataIndex/Query/QueryInterface.php @@ -53,6 +53,13 @@ public function filterInteger(string $field, int $value): self; public function filterFullText(string $value): self; + public function filterMultiMatch( + string $searchTerm, + array $fields = [], + string $type = 'best_fields', + string $operator = 'or' + ): self; + public function orderByField(string $fieldName, SortDirection $direction): self; public function wildcardSearch( diff --git a/src/DependencyInjection/CompilerPass/DataProviderPass.php b/src/DependencyInjection/CompilerPass/DataProviderPass.php index 830491aaf..4f935c1df 100644 --- a/src/DependencyInjection/CompilerPass/DataProviderPass.php +++ b/src/DependencyInjection/CompilerPass/DataProviderPass.php @@ -27,6 +27,8 @@ { use MustImplementInterfaceTrait; + private const string GDPR_CONFIG_PARAMETER = 'pimcore_studio_backend.gdpr_data_extractor'; + /** * @throws MustImplementInterfaceException */ @@ -39,8 +41,13 @@ public function process(ContainerBuilder $container): void ] ); - foreach ($taggedServices as $environmentType) { - $this->checkInterface($environmentType, DataProviderInterface::class); + $gdprConfig = $container->getParameter(self::GDPR_CONFIG_PARAMETER); + + foreach ($taggedServices as $dataProviderId) { + $this->checkInterface($dataProviderId, DataProviderInterface::class); + + $definition = $container->getDefinition($dataProviderId); + $definition->setArgument('$gdprConfig', $gdprConfig); } } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 706222035..c16107e7e 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -78,6 +78,7 @@ public function getConfigTreeBuilder(): TreeBuilder $this->addPerspectivesConfigurationNode($rootNode); $this->addElementTreeWidgetConfigurationNode($rootNode); $this->addDefaultFromEmail($rootNode); + $this->addGdprDataExtractorNode($rootNode); $rootNode->append($this->addTwigSandboxNode()); ConfigurationHelper::addConfigLocationWithWriteTargetNodes( @@ -657,4 +658,45 @@ private function addDefaultFromEmail(ArrayNodeDefinition $node): void ->end() ->end(); } + + private function addGdprDataExtractorNode(ArrayNodeDefinition $node): void + { + $node->children() + ->arrayNode('gdpr_data_extractor') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('dataObjects') + ->addDefaultsIfNotSet() + ->info('Settings for DataObjects DataProvider') + ->children() + ->arrayNode('classes') + ->info('Configure which classes should be considered, array key is class name') + ->useAttributeAsKey('name') + ->defaultValue([]) + ->arrayPrototype() + ->children() + ->booleanNode('allowDelete') + ->info('Allow delete of objects directly in preview grid.') + ->defaultFalse() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('assets') + ->addDefaultsIfNotSet() + ->info('Settings for Assets DataProvider') + ->children() + ->arrayNode('types') + ->info('Configure which asset types should be considered') + ->scalarPrototype()->end() + ->defaultValue([]) + ->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + } } diff --git a/src/DependencyInjection/PimcoreStudioBackendExtension.php b/src/DependencyInjection/PimcoreStudioBackendExtension.php index 0eb7d41d8..ec7518cc6 100644 --- a/src/DependencyInjection/PimcoreStudioBackendExtension.php +++ b/src/DependencyInjection/PimcoreStudioBackendExtension.php @@ -180,6 +180,11 @@ public function load(array $configs, ContainerBuilder $container): void '$clientSideUrl' => $config['mercure_settings']['hub_url_client'], ]); + $container->setParameter( + 'pimcore_studio_backend.gdpr_data_extractor', + $config['gdpr_data_extractor'] + ); + $this->populateTwigSandboxExtension($config, $container); } diff --git a/src/Gdpr/Attribute/Request/GdprRequestBody.php b/src/Gdpr/Attribute/Request/GdprRequestBody.php index 52585055f..6bb724e66 100644 --- a/src/Gdpr/Attribute/Request/GdprRequestBody.php +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -18,6 +18,7 @@ use OpenApi\Attributes\JsonContent; use OpenApi\Attributes\Property; use OpenApi\Attributes\RequestBody; +use Pimcore\Bundle\StudioBackendBundle\Filter\Attribute\Property\FilterProperty; /** * @internal @@ -49,6 +50,8 @@ public function __construct() description: 'The object containing the search values.', type: 'object' ), + + new FilterProperty(), ], type: 'object', ), diff --git a/src/Gdpr/Attribute/Request/SearchTerms.php b/src/Gdpr/Attribute/Request/SearchTerms.php index 3780a417f..eca9489a0 100644 --- a/src/Gdpr/Attribute/Request/SearchTerms.php +++ b/src/Gdpr/Attribute/Request/SearchTerms.php @@ -29,21 +29,41 @@ final readonly class SearchTerms { public function __construct( - #[Property(description: 'The ID to search for.', type: 'string', example: '1', nullable: true)] - #[Type('string')] - public ?string $id = null, + #[Property( + description: 'The ID to search for.', + type: 'int', + nullable: true, + example: 3 + )] + #[Type('int')] + private ?int $id = null, - #[Property(description: 'The first name to search for.', type: 'string', example: 'John', nullable: true)] + #[Property( + description: 'The first name to search for.', + type: 'string', + nullable: true, + example: 'John' + )] #[Type('string')] - public ?string $firstname = null, + private ?string $firstname = null, - #[Property(description: 'The last name to search for.', type: 'string', example: 'Doe', nullable: true)] + #[Property( + description: 'The last name to search for.', + type: 'string', + nullable: true, + example: 'Doe' + )] #[Type('string')] - public ?string $lastname = null, + private ?string $lastname = null, - #[Property(description: 'The email address to search for.', type: 'string', example: '', nullable: true)] + #[Property( + description: 'The email address to search for.', + type: 'string', + nullable: true, + example: 'john.doe@example.com' + )] #[Type('string')] - public ?string $email = null, + private ?string $email = null, ) { if ($this->id === null && $this->firstname === null && @@ -54,7 +74,7 @@ public function __construct( } } - public function getId(): ?string + public function getId(): ?int { return $this->id; } diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index 7022230a2..2b3a60cee 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -23,9 +23,11 @@ use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Header\ContentDisposition; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\SuccessResponse; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Config\Tags; +use Pimcore\Bundle\StudioBackendBundle\Util\Constant\Asset\MimeTypes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; +use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseHeaders; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -69,8 +71,8 @@ public function __construct( )] #[SuccessResponse( description: 'gdpr_export_success_response', - content: new MediaType('application/json'), - headers: [new ContentDisposition('inline')] + content: [new MediaType(MimeTypes::GENERIC->value)], + headers: [new ContentDisposition(HttpResponseHeaders::ATTACHMENT_TYPE->value)] )] #[DefaultResponses([ HttpResponseCodes::UNAUTHORIZED, @@ -81,7 +83,7 @@ public function __construct( public function startExport( int $id, #[MapQueryParameter] string $providerKey - ): StreamedResponse { - return $this->gdprManagerService->getExportDataAsJson($id, $providerKey); + ): Response { + return $this->gdprManagerService->getExportData($id, $providerKey); } } diff --git a/src/Gdpr/Controller/GetDataProviderController.php b/src/Gdpr/Controller/GetDataProviderController.php index 35b334916..700b0b4de 100644 --- a/src/Gdpr/Controller/GetDataProviderController.php +++ b/src/Gdpr/Controller/GetDataProviderController.php @@ -15,7 +15,6 @@ use OpenApi\Attributes\Get; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataProvider; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Property\GenericCollection; @@ -47,15 +46,12 @@ public function __construct( parent::__construct($serializer); } - /** - * @throws NotFoundException - */ #[Route( '/gdpr/providers', name: 'pimcore_studio_api_gdpr_providers', methods: ['GET'])] #[IsGranted(UserPermissions::GDPR->value)] - #[GET( + #[Get( path: self::PREFIX . '/gdpr/providers', operationId: 'gdpr_list_providers', description: 'gdpr_list_providers_description', @@ -70,7 +66,6 @@ public function __construct( #[DefaultResponses([ HttpResponseCodes::UNAUTHORIZED, HttpResponseCodes::FORBIDDEN, - HttpResponseCodes::NOT_FOUND, ])] public function getProvidersList(): JsonResponse { diff --git a/src/Gdpr/Controller/SearchDataProviderController.php b/src/Gdpr/Controller/SearchDataProviderController.php index a64744f32..f75297925 100644 --- a/src/Gdpr/Controller/SearchDataProviderController.php +++ b/src/Gdpr/Controller/SearchDataProviderController.php @@ -16,17 +16,20 @@ use OpenApi\Attributes\Post; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\GdprRequestBody; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; +use Pimcore\Bundle\StudioBackendBundle\Filter\Attribute\Request\CollectionRequestBody; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultProperty; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\MappedParameter\CollectionFilterParameter; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Query\TextFieldParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\CollectionJson; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\SuccessResponse; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Config\Tags; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; +use Pimcore\Bundle\StudioBackendBundle\Util\Trait\PaginatedResponseTrait; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -37,6 +40,8 @@ */ final class SearchDataProviderController extends AbstractApiController { + use PaginatedResponseTrait; + public function __construct( SerializerInterface $serializer, private readonly GdprManagerServiceInterface $gdprManagerService, @@ -54,14 +59,27 @@ public function __construct( name: 'pimcore_studio_api_gdpr_search', methods: ['POST'])] #[IsGranted(UserPermissions::GDPR->value)] - #[POST( + #[Post( path: self::PREFIX . '/gdpr/search', operationId: 'gdpr_search_data', description: 'gdpr_search_data_description', summary: 'gdpr_search_data_summary', tags: [Tags::GDPR->value] )] - #[GdprRequestBody] + #[CollectionRequestBody( + columnFiltersExample: '[' . + '{"type":"firstname", "filterValue": "John"},' . + '{"type":"lastname", "filterValue": "Doe"},' . + '{"type":"email", "filterValue": "john.doe@mail.com"},'. + '{"type":"id", "filterValue": 1}' + . ']', + sortFilterExample: '{"key":"id", "direction":"ASC"}' + )] + #[TextFieldParameter( + name: 'provider', + description: 'Define the data provider to search in.', + example: 'assets' + )] #[SuccessResponse( description: 'gdpr_search_data_success_response', content: new CollectionJson( @@ -77,11 +95,16 @@ public function __construct( HttpResponseCodes::UNPROCESSABLE_CONTENT, ])] public function searchData( - #[MapRequestPayload] GdprStructuredSearchRequest $request + #[MapRequestPayload] CollectionFilterParameter $parameters, + #[MapQueryParameter] string $provider ): JsonResponse { - $collection = $this->gdprManagerService->search($request); + $collection = $this->gdprManagerService->search($parameters, $provider); - return $this->jsonResponse($collection); + return $this->getPaginatedCollection( + $this->serializer, + $collection->getItems(), + $collection->getTotalItems() + ); } } diff --git a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php b/src/Gdpr/MappedParameter/GdprStructuredSearchParameters.php similarity index 62% rename from src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php rename to src/Gdpr/MappedParameter/GdprStructuredSearchParameters.php index 32d92fb40..c56155dec 100644 --- a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php +++ b/src/Gdpr/MappedParameter/GdprStructuredSearchParameters.php @@ -13,6 +13,7 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter; +use Pimcore\Bundle\StudioBackendBundle\Filter\MappedParameter\FilterParameter; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\NotBlank; @@ -23,7 +24,7 @@ /** * @internal */ -final readonly class GdprStructuredSearchRequest +final readonly class GdprStructuredSearchParameters { /** * @param string[] $providers @@ -31,11 +32,32 @@ public function __construct( #[NotBlank] #[All(new Type('string'))] - public array $providers, + private array $providers, #[Valid] #[NotNull] - public SearchTerms $searchTerms + private SearchTerms $searchTerms, + + #[Valid] + private FilterParameter $filters ) { } + + /** + * @return string[] + */ + public function getProviders(): array + { + return $this->providers; + } + + public function getSearchTerms(): SearchTerms + { + return $this->searchTerms; + } + + public function getFilters(): FilterParameter + { + return $this->filters; + } } diff --git a/src/Gdpr/Provider/AssetsProvider.php b/src/Gdpr/Provider/AssetsProvider.php new file mode 100644 index 000000000..47feefc33 --- /dev/null +++ b/src/Gdpr/Provider/AssetsProvider.php @@ -0,0 +1,178 @@ +assetConfig = $gdprConfig['assets'] ?? []; + } + + /** + * {@inheritdoc} + */ + public function findData(FilterParameter $filter): Collection + { + $query = $this->query->createAssetQuery(); + + $query->excludeFolders(); + + $idFilter = $filter->getSimpleColumnFilterByType('id'); + if ($idFilter !== null) { + $query->filterInteger('id', (int)$idFilter->getFilterValue()); + } + + $textFilterTypes = ['firstname', 'lastname', 'email']; + $searchTerms = []; + + foreach ($textFilterTypes as $filterType) { + $textFilter = $filter->getSimpleColumnFilterByType($filterType); + if ($textFilter === null) { + continue; + } + + $value = trim((string)$textFilter->getFilterValue()); + if ($value !== '') { + $searchTerms[] = $value; + } + } + + if ($searchTerms !== []) { + $query->filterMultiMatch(implode(' ', $searchTerms), [], 'cross_fields', 'and'); + } + + $query->filterMultiSelect('type', $this->assetConfig['types']); + + $this->applySearchOptions($query, $filter); + + $searchResult = $this->searchService->searchAssets($query); + + $columns = $this->getAvailableColumns(); + + $items = $searchResult->getItems(); + + $rows = array_map( + fn ($item) => new GdprDataRow([ + 'type' => $item->getType(), + 'id' => $item->getId(), + 'fullPath' => $item->getFullPath(), + 'subType' => $item->getMimeType(), + '__gdprIsDeletable' => true, + ], $columns), + $items + ); + + return new Collection( + totalItems: $searchResult->getTotalItems(), + items: $rows + ); + } + + private function applySearchOptions(QueryInterface $query, FilterParameter $options): void + { + $query->setPage($options->getPage()); + $query->setPageSize($options->getPageSize()); + + $sortFilter = $options->getSortFilter(); + + if ($sortFilter->getKey() && $sortFilter->getDirection()) { + $directionEnum = strtolower($sortFilter->getDirection()) === SortDirection::DESC->value + ? SortDirection::DESC + : SortDirection::ASC; + + $query->orderByField($sortFilter->getKey(), $directionEnum); + } + + } + + public function getDeleteSwaggerOperationId(): string + { + return 'pimcore_studio_api_assets_batch_delete'; + } + + /** + * {@inheritdoc} + */ + public function getSingleItemForDownload(int $id): Response + { + $asset = Asset::getById($id); + + if (!$asset) { + throw new NotFoundException('Asset Not Found', $id); + } + + return $this->assetExporter->doExportData($asset); + } + + public function getName(): string + { + return 'Assets'; + } + + public function getKey(): string + { + return 'assets'; + } + + public function getSortPriority(): int + { + return 8; + } + + /** + * {@inheritdoc} + */ + public function getRequiredPermissions(): array + { + return [UserPermissions::ASSETS->value]; + } + + /** + * {@inheritdoc} + */ + public function getAvailableColumns(): array + { + return [ + new GdprDataColumn('type', 'Type'), + new GdprDataColumn('id', 'ID'), + new GdprDataColumn('fullPath', 'Full Path'), + new GdprDataColumn('subType', 'Type'), + new GdprDataColumn('__gdprIsDeletable', 'Is Deletable'), + ]; + } +} diff --git a/src/Gdpr/Provider/DataObjectProvider.php b/src/Gdpr/Provider/DataObjectProvider.php new file mode 100644 index 000000000..9901233ec --- /dev/null +++ b/src/Gdpr/Provider/DataObjectProvider.php @@ -0,0 +1,198 @@ +dataObjectConfig = $gdprConfig['dataObjects'] ?? []; + } + + /** + * {@inheritdoc} + */ + public function findData(FilterParameter $filter): Collection + { + $query = $this->query->createDataObjectQuery(); + + $query->excludeFolders(); + + $idFilter = $filter->getSimpleColumnFilterByType('id'); + if ($idFilter !== null) { + $query->filterInteger('id', (int)$idFilter->getFilterValue()); + } + + $textFilterTypes = ['firstname', 'lastname', 'email']; + $searchTerms = []; + + foreach ($textFilterTypes as $filterType) { + $textFilter = $filter->getSimpleColumnFilterByType($filterType); + if ($textFilter === null) { + continue; + } + + $value = trim((string)$textFilter->getFilterValue()); + if ($value !== '') { + $searchTerms[] = $value; + } + } + + if ($searchTerms !== []) { + $query->filterMultiMatch(implode(' ', $searchTerms), [], 'cross_fields', 'and'); + } + + $this->applySearchOptions($query, $filter); + + $searchResult = $this->searchService->searchDataObjects($query); + + $columns = $this->getAvailableColumns(); + + $items = $searchResult->getItems(); + + $rows = array_map( + fn ($item) => new GdprDataRow([ + 'type' => $item->getType(), + 'id' => $item->getId(), + 'fullPath' => $item->getFullPath(), + 'className' => $item->getClassName(), + '__gdprIsDeletable' => + $this->dataObjectConfig['classes'][$item->getClassName()]['allowDelete'] ?? false, + ], $columns), + $items + ); + + return new Collection( + totalItems: $searchResult->getTotalItems(), + items: $rows + ); + + } + + private function applySearchOptions(QueryInterface $query, FilterParameter $options): void + { + $query->setPage($options->getPage()); + $query->setPageSize($options->getPageSize()); + + $sortFilter = $options->getSortFilter(); + + if ($sortFilter->getKey() && $sortFilter->getDirection()) { + $directionEnum = strtolower($sortFilter->getDirection()) === SortDirection::DESC->value + ? SortDirection::DESC + : SortDirection::ASC; + + $query->orderByField($sortFilter->getKey(), $directionEnum); + } + + } + + public function getDeleteSwaggerOperationId(): string + { + return 'data_object_batch_delete'; + } + + /** + * {@inheritdoc} + */ + public function getSingleItemForDownload(int $id): array + { + try { + $object = DataObject::getById((int)$id); + } catch (NotFoundException) { + throw new NotFoundException('Data Object Not Found', $id); + } + + if (!$object instanceof Concrete) { + throw new NotFoundException('Requested object is not a Concrete data object', $id); + } + + $export = [ + 'id' => $object->getId(), + 'fullPath' => $object->getFullPath(), + ]; + + $properties = $object->getProperties(); + $finalProperties = []; + + foreach ($properties as $property) { + $finalProperties[] = $property->serialize(); + } + + $export['properties'] = $finalProperties; + + $this->objectExporter->doExportObject($object, $export); + + return $export; + } + + public function getName(): string + { + return 'Data Objects'; + } + + public function getKey(): string + { + return 'data_objects'; + } + + public function getSortPriority(): int + { + return 10; + } + + /** + * {@inheritdoc} + */ + public function getRequiredPermissions(): array + { + return [UserPermissions::DATA_OBJECTS->value]; + } + + /** + * {@inheritdoc} + */ + public function getAvailableColumns(): array + { + return [ + new GdprDataColumn('type', 'Type'), + new GdprDataColumn('id', 'ID'), + new GdprDataColumn('fullPath', 'Full Path'), + new GdprDataColumn('className', 'Class Name'), + new GdprDataColumn('__gdprIsDeletable', 'Is Deletable'), + ]; + } +} diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index d546101ad..2fbee1e54 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -13,18 +13,15 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; +use Pimcore\Bundle\StudioBackendBundle\Filter\MappedParameter\FilterParameter; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; +use Pimcore\Bundle\StudioBackendBundle\Response\Collection; +use Symfony\Component\HttpFoundation\Response; interface DataProviderInterface { - /** - * @return GdprDataRow[] - */ - public function findData(SearchTerms $terms): array; + public function findData(FilterParameter $filter): Collection; public function getDeleteSwaggerOperationId(): string; @@ -46,7 +43,6 @@ public function getRequiredPermissions(): array; /** * @throws NotFoundException - * @throws ForbiddenException */ - public function getSingleItemForDownload(int $id): array|object; + public function getSingleItemForDownload(int $id): array|Response; } diff --git a/src/Gdpr/Provider/Legacy/AssetExporter.php b/src/Gdpr/Provider/Legacy/AssetExporter.php new file mode 100644 index 000000000..2513c8032 --- /dev/null +++ b/src/Gdpr/Provider/Legacy/AssetExporter.php @@ -0,0 +1,88 @@ +getId(); + $webAsset['fullpath'] = $theAsset->getRealFullPath(); + $properties = $theAsset->getProperties(); + $finalProperties = []; + + foreach ($properties as $property) { + $finalProperties[] = $property->serialize(); + } + + $webAsset['properties'] = $finalProperties; + $webAsset['customSettings'] = $theAsset->getCustomSettings(); + + $resultItem = json_decode(json_encode($webAsset), true); + unset($resultItem['data']); + + return $resultItem; + } + + public function doExportData(Asset $asset): Response + { + $exportIds = []; + $exportIds[$asset->getId()] = true; + + $file = tempnam('/tmp', 'zip'); + $zip = new ZipArchive(); + $zip->open($file, ZipArchive::OVERWRITE); + + foreach (array_keys($exportIds) as $id) { + $theAsset = Asset::getById($id); + + $resultItem = $this->doexportAsset($theAsset); + $resultItem = json_encode($resultItem); + + $zip->addFromString($asset->getFilename() . '.json', $resultItem); + + if (!$theAsset instanceof Asset\Folder) { + $zip->addFromString($theAsset->getFilename(), $theAsset->getData()); + } + } + + $zip->close(); + + $size = filesize($file); + $content = file_get_contents($file); + unlink($file); + + $response = new Response($content); + $response->headers->set('Content-Type', 'application/zip'); + $response->headers->set('Content-Length', (string) $size); + $response->headers->set('Content-Disposition', 'attachment; filename="' . $asset->getFilename() . '.zip"'); + + return $response; + } +} diff --git a/src/Gdpr/Provider/Legacy/AssetExporterInterface.php b/src/Gdpr/Provider/Legacy/AssetExporterInterface.php new file mode 100644 index 000000000..4fec14d72 --- /dev/null +++ b/src/Gdpr/Provider/Legacy/AssetExporterInterface.php @@ -0,0 +1,25 @@ +getClass()->getFieldDefinitions(); + + foreach ($fDefs as $fd) { + $getter = 'get' . ucfirst($fd->getName()); + $value = $object->$getter(); + + if ($fd instanceof Data\Fieldcollections && $value instanceof Fieldcollection) { + self::doExportFieldcollection($result, $value); + } elseif ($fd instanceof Data\Objectbricks && $value instanceof Objectbrick) { + self::doExportBrick($result, $value); + } else { + if ($fd instanceof NormalizerInterface + && $fd instanceof DataObject\ClassDefinition\Data) { + $marshalledValue = $fd->normalize($value); + $result[$fd->getName()] = $marshalledValue; + } + } + } + } + + private function doExportBrick(array &$result, Objectbrick $container): void + { + $allowedBrickTypes = $container->getAllowedBrickTypes(); + $resultContainer = []; + foreach ($allowedBrickTypes as $brickType) { + $brickDef = Objectbrick\Definition::getByKey($brickType); + $brickGetter = 'get' . ucfirst($brickType); + $brickValue = $container->$brickGetter(); + + if ($brickValue instanceof Objectbrick\Data\AbstractData) { + $resultContainer[$brickType] = []; + $fDefs = $brickDef->getFieldDefinitions(); + foreach ($fDefs as $fd) { + $getter = 'get' . ucfirst($fd->getName()); + $value = $brickValue->$getter(); + if ($fd instanceof NormalizerInterface + && $fd instanceof DataObject\ClassDefinition\Data) { + $marshalledValue = $fd->normalize($value); + $resultContainer[$brickType][$fd->getName()] = $marshalledValue; + } + } + } + } + $result[$container->getFieldname()] = $resultContainer; + } + + private function doExportFieldcollection(array &$result, Fieldcollection $container): void + { + $resultContainer = []; + + $items = $container->getItems(); + foreach ($items as $item) { + $type = $item->getType(); + + $itemValues = []; + + $itemContainerDefinition = Fieldcollection\Definition::getByKey($type); + $fDefs = $itemContainerDefinition->getFieldDefinitions(); + + foreach ($fDefs as $fd) { + $getter = 'get' . ucfirst($fd->getName()); + $value = $item->$getter(); + + if ($fd instanceof NormalizerInterface + && $fd instanceof DataObject\ClassDefinition\Data) { + $marshalledValue = $fd->normalize($value); + $itemValues[$fd->getName()] = $marshalledValue; + } + } + + $resultContainer[] = [ + 'type' => $type, + 'value' => $itemValues, + ]; + } + $result[$container->getFieldname()] = $resultContainer; + } +} diff --git a/src/Gdpr/Provider/Legacy/ObjectExporterInterface.php b/src/Gdpr/Provider/Legacy/ObjectExporterInterface.php new file mode 100644 index 000000000..0b2835476 --- /dev/null +++ b/src/Gdpr/Provider/Legacy/ObjectExporterInterface.php @@ -0,0 +1,27 @@ + $result + */ + public function doExportObject(Concrete $object, array &$result = []): void; +} diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index d5acfd810..e4db02f66 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -1,4 +1,5 @@ id !== null) { + $idFilter = $filter->getSimpleColumnFilterByType('id'); + if ($idFilter !== null) { $listing->addConditionParam( 'id = :id', - ['id' => $terms->id] + ['id' => $idFilter->getFilterValue()] ); } - if ($terms->firstname !== null) { + $firstnameFilter = $filter->getSimpleColumnFilterByType('firstname'); + if ($firstnameFilter !== null) { $listing->addConditionParam( 'firstname LIKE :firstname', - ['firstname' => '%' . $terms->firstname . '%'] + ['firstname' => '%' . $firstnameFilter->getFilterValue() . '%'] ); } - if ($terms->lastname !== null) { + $lastnameFilter = $filter->getSimpleColumnFilterByType('lastname'); + if ($lastnameFilter !== null) { $listing->addConditionParam( 'lastname LIKE :lastname', - ['lastname' => '%' . $terms->lastname . '%'] + ['lastname' => '%' . $lastnameFilter->getFilterValue() . '%'] ); } - if ($terms->email !== null) { + $emailFilter = $filter->getSimpleColumnFilterByType('email'); + if ($emailFilter !== null) { $listing->addConditionParam( 'email LIKE :email', - ['email' => '%' . $terms->email . '%'] + ['email' => '%' . $emailFilter->getFilterValue() . '%'] ); } + $this->applySearchOptions($listing, $filter); + $users = $listing->getUsers(); $columns = $this->getAvailableColumns(); - return array_map( + $rows = array_map( fn ($user) => new GdprDataRow( [ 'id' => $user->getId(), @@ -87,6 +99,28 @@ public function findData(SearchTerms $terms): array ), $users ); + + return new Collection( + totalItems: $listing->getTotalCount(), + items: $rows + ); + } + + private function applySearchOptions(Listing $listing, FilterParameter $options): void + { + $listing->setOffset($options->getStart()); + $listing->setLimit($options->getPageSize()); + + $sortFilter = $options->getSortFilter(); + + if ($sortFilter->getKey() && $sortFilter->getDirection()) { + $listing->setOrderKey($sortFilter->getKey()); + $listing->setOrder( + strtolower($sortFilter->getDirection()) === SortDirection::DESC->value + ? SortDirection::DESC->value + : SortDirection::ASC->value + ); + } } public function getDeleteSwaggerOperationId(): string @@ -99,27 +133,20 @@ public function getDeleteSwaggerOperationId(): string */ public function getSingleItemForDownload(int $id): array { - $listing = new Listing(); - $listing->setCondition('id = ?', [$id]); - $listing->setLimit(1); - - $users = $listing->getUsers(); + $user = $this->userResolver->getById($id); - if (empty($users)) { + if (!$user) { throw new NotFoundException('Pimcore User', $id); } - $user = $users[0]; + $userData = $user->getObjectVars(); - return [ - 'id' => $user->getId(), - 'name' => $user->getName(), - 'firstname' => $user->getFirstname(), - 'lastname' => $user->getLastname(), - 'email' => $user->getEmail(), - 'versions' => $this->getVersionDataForUser($user), - 'usageLog' => $this->getUsageLogDataForUser($user), - ]; + unset($userData['password']); + + $userData['versions'] = $this->getVersionDataForUser($user); + $userData['usageLog'] = $this->getUsageLogDataForUser($user); + + return $userData; } protected function getVersionDataForUser(User\AbstractUser $user): array diff --git a/src/Gdpr/Schema/GdprSearchResult.php b/src/Gdpr/Schema/GdprSearchResult.php index 97ce2411b..26bb7b781 100644 --- a/src/Gdpr/Schema/GdprSearchResult.php +++ b/src/Gdpr/Schema/GdprSearchResult.php @@ -37,14 +37,21 @@ public function __construct( type: 'string', example: 'data_objects' )] - private readonly string $providerKey, + private string $providerKey, #[Property( description: 'The list of results found by this provider', type: 'array', items: new Items(ref: GdprDataRow::class) )] - private readonly array $results, + private array $results, + + #[Property( + description: 'The total number of sub-items for each data providers', + type: 'integer', + example: 5 + )] + private int $totalSubItems, ) { } @@ -60,4 +67,9 @@ public function getResults(): array { return $this->results; } + + public function getTotalSubItems(): int + { + return $this->totalSubItems; + } } diff --git a/src/Gdpr/Schema/GdprSearchResultCollection.php b/src/Gdpr/Schema/GdprSearchResultCollection.php index 7033a253e..d4f9884ff 100644 --- a/src/Gdpr/Schema/GdprSearchResultCollection.php +++ b/src/Gdpr/Schema/GdprSearchResultCollection.php @@ -42,8 +42,13 @@ public function __construct( type: 'array', items: new Items(ref: GdprSearchResult::class) )] - private readonly array $items, + + #[Property( + description: 'Total number of items across all pages', + type: 'integer' + )] + private readonly int $totalItems, ) { } @@ -54,4 +59,9 @@ public function getItems(): array { return $this->items; } + + public function getTotalItems(): int + { + return $this->totalItems; + } } diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 73125fcd9..a4481b65c 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -18,16 +18,17 @@ use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprDataProviderEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprSearchResultEvent; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataProvider; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResult; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; +use Pimcore\Bundle\StudioBackendBundle\MappedParameter\CollectionFilterParameter; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseHeaders; use Pimcore\Bundle\StudioBackendBundle\Util\Trait\StreamedResponseTrait; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function count; @@ -48,9 +49,6 @@ public function __construct( ) { } - /** - * {@inheritdoc} - */ public function getAvailableProviders(): Collection { $providers = $this->sortProviders($this->loader->getDataProviders()); @@ -58,36 +56,29 @@ public function getAvailableProviders(): Collection return $this->getDataProviderCollection($providers); } - /** - * {@inheritdoc} - */ - public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection + public function search(CollectionFilterParameter $parameters, string $providerType): GdprSearchResultCollection { $allResults = []; - foreach ($request->providers as $providerKey) { - $provider = $this->loader->resolve($providerKey); + $providerClass = $this->loader->resolve($providerType); - $this->checkProviderPermission($provider); + $this->checkProviderPermission($providerClass); - $results = $provider->findData($request->searchTerms); + $results = $providerClass->findData($parameters->getFilters()); - if (!empty($results)) { - $allResults[] = new GdprSearchResult( - providerKey: $providerKey, - results: $results - ); - } + if (!empty($results->getItems())) { + $allResults[] = new GdprSearchResult( + providerKey: $providerType, + results: $results->getItems(), + totalSubItems: $results->getTotalItems() + ); } return $this->getSearchResultCollection($allResults); } - /** - * {@inheritdoc} - */ - public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse + public function getExportData(int $id, string $providerKey): Response { $provider = $this->loader->resolve($providerKey); @@ -131,7 +122,7 @@ private function getDataProviderCollection(array $providers): Collection */ private function getSearchResultCollection(array $results): GdprSearchResultCollection { - $collection = new GdprSearchResultCollection($results); + $collection = new GdprSearchResultCollection($results, count($results)); $this->eventDispatcher->dispatch( new GdprSearchResultEvent($collection), @@ -141,8 +132,14 @@ private function getSearchResultCollection(array $results): GdprSearchResultColl return $collection; } - private function createExportResponse(mixed $data, string $providerKey, int $id): StreamedResponse + private function createExportResponse(mixed $data, string $providerKey, int $id): Response { + // If $data is a Response (e.g., assets export), return it directly. + // Otherwise, assume $data is an array and encode it as pretty JSON for download. + if ($data instanceof Response) { + return $data; + } + try { $jsonData = json_encode($data, JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT); } catch (JsonException $e) { diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index c8531e6d7..6ee2a4f52 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -15,11 +15,11 @@ use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataProvider; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; +use Pimcore\Bundle\StudioBackendBundle\MappedParameter\CollectionFilterParameter; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; -use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\Response; /** * @internal @@ -34,13 +34,13 @@ public function getAvailableProviders(): Collection; /** * Searches for data in the specified providers. * - * @throws ForbiddenException + * @throws ForbiddenException|NotFoundException */ - public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection; + public function search(CollectionFilterParameter $parameters, string $provider): GdprSearchResultCollection; /** * @throws ForbiddenException * @throws NotFoundException */ - public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse; + public function getExportData(int $id, string $providerKey): Response; } diff --git a/src/Util/Constant/Asset/MimeTypes.php b/src/Util/Constant/Asset/MimeTypes.php index d3c67014e..e41c71648 100644 --- a/src/Util/Constant/Asset/MimeTypes.php +++ b/src/Util/Constant/Asset/MimeTypes.php @@ -33,4 +33,5 @@ enum MimeTypes: string case ZIP = 'application/zip'; case JSON = 'application/json'; case XLSX = 'application/xlsx'; + case GENERIC = '*/*'; }