From 10aca52f02841b3a1c8e1505d4800c90771b3ff0 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Mon, 10 Nov 2025 12:17:41 +0000 Subject: [PATCH 01/34] Progressing GDPR --- config/gdpr.yaml | 31 +++++ doc/05_Additional_Custom_Attributes.md | 3 +- .../CompilerPass/DataProviderPass.php | 46 +++++++ .../Attribute/Request/GdprRequestBody.php | 58 ++++++++ src/Gdpr/Attribute/Request/SearchTerms.php | 68 ++++++++++ .../Controller/GetDataProviderController.php | 86 ++++++++++++ .../SearchDataProviderController.php | 89 ++++++++++++ .../PreResponse/GdprDataProviderEvent.php | 36 +++++ .../GdprStructuredSearchRequest.php | 39 ++++++ src/Gdpr/Provider/AssetsProvider.php | 87 ++++++++++++ src/Gdpr/Provider/DataObjectProvider.php | 88 ++++++++++++ src/Gdpr/Provider/DataProviderInterface.php | 67 +++++++++ src/Gdpr/Provider/PimcoreUserProvider.php | 110 +++++++++++++++ src/Gdpr/Schema/GdprDataColumn.php | 53 ++++++++ src/Gdpr/Schema/GdprDataProvider.php | 76 +++++++++++ src/Gdpr/Schema/GdprSearchResult.php | 63 +++++++++ .../Schema/GdprSearchResultCollection.php | 53 ++++++++ src/Gdpr/Schema/GdprSearchResultProperty.php | 55 ++++++++ .../Service/DataProviderLoaderInterface.php | 35 +++++ src/Gdpr/Service/GdprManagerService.php | 127 ++++++++++++++++++ .../Service/GdprManagerServiceInterface.php | 40 ++++++ .../TaggedIteratorDataProviderLoader.php | 52 +++++++ src/OpenApi/Config/Tags.php | 5 + src/PimcoreStudioBackendBundle.php | 2 + translations/studio_api_docs.en.yaml | 3 + 25 files changed, 1371 insertions(+), 1 deletion(-) create mode 100644 config/gdpr.yaml create mode 100644 src/DependencyInjection/CompilerPass/DataProviderPass.php create mode 100644 src/Gdpr/Attribute/Request/GdprRequestBody.php create mode 100644 src/Gdpr/Attribute/Request/SearchTerms.php create mode 100644 src/Gdpr/Controller/GetDataProviderController.php create mode 100644 src/Gdpr/Controller/SearchDataProviderController.php create mode 100644 src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php create mode 100644 src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php create mode 100644 src/Gdpr/Provider/AssetsProvider.php create mode 100644 src/Gdpr/Provider/DataObjectProvider.php create mode 100644 src/Gdpr/Provider/DataProviderInterface.php create mode 100644 src/Gdpr/Provider/PimcoreUserProvider.php create mode 100644 src/Gdpr/Schema/GdprDataColumn.php create mode 100644 src/Gdpr/Schema/GdprDataProvider.php create mode 100644 src/Gdpr/Schema/GdprSearchResult.php create mode 100644 src/Gdpr/Schema/GdprSearchResultCollection.php create mode 100644 src/Gdpr/Schema/GdprSearchResultProperty.php create mode 100644 src/Gdpr/Service/DataProviderLoaderInterface.php create mode 100644 src/Gdpr/Service/GdprManagerService.php create mode 100644 src/Gdpr/Service/GdprManagerServiceInterface.php create mode 100644 src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php diff --git a/config/gdpr.yaml b/config/gdpr.yaml new file mode 100644 index 000000000..128a3fe83 --- /dev/null +++ b/config/gdpr.yaml @@ -0,0 +1,31 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + # controllers are imported separately to make sure they're public + # and have a tag that allows actions to type-hint services + Pimcore\Bundle\StudioBackendBundle\Gdpr\Controller\: + resource: "../src/Gdpr/Controller/*" + public: true + tags: ["controller.service_arguments"] + + # --- GDPR Service Layer --- + + Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerService + + Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\DataProviderLoaderInterface: + class: Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\Loader\TaggedIteratorDataProviderLoader + + # --- GDPR Providers --- + + 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"] + + Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\PimcoreUserProvider: + tags: ["pimcore.studio_backend.gdpr_data_provider"] \ No newline at end of file diff --git a/doc/05_Additional_Custom_Attributes.md b/doc/05_Additional_Custom_Attributes.md index 73bd83586..4d807fddb 100644 --- a/doc/05_Additional_Custom_Attributes.md +++ b/doc/05_Additional_Custom_Attributes.md @@ -157,4 +157,5 @@ final class AssetEvent extends AbstractPreResponseEvent - `pre_response.website_settings.item` - `pre_response.workflow_details` - `pre_response.notification_recipient` -- `pre_response.php_code_transformer` \ No newline at end of file +- `pre_response.php_code_transformer` +- `pre_response.data_provider` \ No newline at end of file diff --git a/src/DependencyInjection/CompilerPass/DataProviderPass.php b/src/DependencyInjection/CompilerPass/DataProviderPass.php new file mode 100644 index 000000000..830491aaf --- /dev/null +++ b/src/DependencyInjection/CompilerPass/DataProviderPass.php @@ -0,0 +1,46 @@ +findTaggedServiceIds(TaggedIteratorDataProviderLoader::DATA_PROVIDER_TAG), + + ] + ); + + foreach ($taggedServices as $environmentType) { + $this->checkInterface($environmentType, DataProviderInterface::class); + } + } +} diff --git a/src/Gdpr/Attribute/Request/GdprRequestBody.php b/src/Gdpr/Attribute/Request/GdprRequestBody.php new file mode 100644 index 000000000..e22ac9ec7 --- /dev/null +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -0,0 +1,58 @@ +id; + } + + public function getFirstname(): ?string + { + return $this->firstname; + } + + public function getLastname(): ?string + { + return $this->lastname; + } + + public function getEmail(): ?string + { + return $this->email; + } +} diff --git a/src/Gdpr/Controller/GetDataProviderController.php b/src/Gdpr/Controller/GetDataProviderController.php new file mode 100644 index 000000000..22bc25344 --- /dev/null +++ b/src/Gdpr/Controller/GetDataProviderController.php @@ -0,0 +1,86 @@ +value)] + #[GET( + path: self::PREFIX . '/gdpr/providers', + operationId: 'gdpr_list_providers', + summary: 'gdpr_list_providers_summary', + description: 'gdpr_list_providers_description', + tags: [Tags::GDPR->value] + )] + #[SuccessResponse( + description: 'gdpr_list_providers_success_response', + content: new CollectionJson(new GenericCollection(GdprDataProvider::class)) + )] + + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::FORBIDDEN, + HttpResponseCodes::NOT_FOUND, + ])] + + public function getProvidersList(): JsonResponse + { + $collection = $this->gdprManagerService->getAvailableProviders(); + + return $this->getPaginatedCollection( + $this->serializer, + $collection->getItems(), + $collection->getTotalItems() + ); + } +} diff --git a/src/Gdpr/Controller/SearchDataProviderController.php b/src/Gdpr/Controller/SearchDataProviderController.php new file mode 100644 index 000000000..e19e4a2c3 --- /dev/null +++ b/src/Gdpr/Controller/SearchDataProviderController.php @@ -0,0 +1,89 @@ +value)] + #[POST( + path: self::PREFIX . '/gdpr/search', + operationId: 'gdpr_search_data', + summary: 'gdpr_search_data_summary', + description: 'gdpr_search_data_description', + tags: [Tags::GDPR->value] + )] + #[GdprRequestBody] + #[SuccessResponse( + description: 'gdpr_search_data_success_response', + content: new CollectionJson( + collection: new GdprSearchResultProperty() + ) + )] + + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::FORBIDDEN, + HttpResponseCodes::NOT_FOUND, + HttpResponseCodes::BAD_REQUEST, + ])] + + public function searchData( + #[MapRequestPayload] GdprStructuredSearchRequest $request + ): JsonResponse { + + $collection = $this->gdprManagerService->search($request); + + return $this->jsonResponse($collection); + } + +} diff --git a/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php new file mode 100644 index 000000000..b2ad7b4f3 --- /dev/null +++ b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php @@ -0,0 +1,36 @@ +provider); + } + + public function getProvider(): GdprDataProvider + { + return $this->provider; + } +} + diff --git a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php new file mode 100644 index 000000000..96f2eef20 --- /dev/null +++ b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php @@ -0,0 +1,39 @@ + 1, + 'path' => '/test/assets-1', + 'description' => 'Hello World', + ], + [ + 'id' => 2, + 'path' => '/test/assets-2', + 'description' => 'Hello Pimcore', + ], + [ + 'id' => 3, + 'path' => '/test/assets-3', + 'description' => 'Hello Order', + ], + ]; + + return $results; + } + public function getRequiredPermission(): UserPermissions + { + return UserPermissions::DATA_OBJECTS; + } + + public function getName(): string + { + return 'Assets'; + } + + public function getKey(): string + { + return 'assets'; + } + + public function getSortPriority(): int + { + // Give it a high priority so it shows up first in the list. + return 9; + } + + public function getAvailableColumns(): array + { + return [ + new GdprDataColumn( + key: 'id', + label: 'ID' + ), + new GdprDataColumn( + key: 'path', + label: 'Path' + ), + new GdprDataColumn( + key: 'className', + label: 'Class Name' + ), + ]; + } +} diff --git a/src/Gdpr/Provider/DataObjectProvider.php b/src/Gdpr/Provider/DataObjectProvider.php new file mode 100644 index 000000000..2457ae882 --- /dev/null +++ b/src/Gdpr/Provider/DataObjectProvider.php @@ -0,0 +1,88 @@ + 1, + 'path' => '/test/object-1', + 'className' => 'Customer', + ], + [ + 'id' => 2, + 'path' => '/test/object-2', + 'className' => 'Customer', + ], + [ + 'id' => 3, + 'path' => '/test/object-3', + 'className' => 'Order', + ], + ]; + + return $results; + } + + public function getName(): string + { + return 'Data Objects'; + } + + public function getKey(): string + { + return 'data_objects'; + } + + public function getSortPriority(): int + { + // Give it a high priority so it shows up first in the list. + return 10; + } + + public function getAvailableColumns(): array + { + return [ + new GdprDataColumn( + key: 'id', + label: 'ID' + ), + new GdprDataColumn( + key: 'path', + label: 'Path' + ), + new GdprDataColumn( + key: 'className', + label: 'Class Name' + ), + ]; + } + + public function getRequiredPermission(): UserPermissions + { + return UserPermissions::DATA_OBJECTS; + } +} \ No newline at end of file diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php new file mode 100644 index 000000000..c300a7074 --- /dev/null +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -0,0 +1,67 @@ + A list of found elements (Objects, Assets, etc.) + */ + public function findData(?SearchTerms $terms): array; + + /** + * Returns the human-readable name for this provider. + * + * @return string + */ + public function getName(): string; + + /** + * Returns the unique identifying key for this provider. + * @return string + */ + public function getKey(): string; + + /** + * A higher number means a higher priority (appears first). + * @return int + */ + public function getSortPriority(): int; + + /** + * Returns the list of available columns for the result data. + * + * @return GdprDataColumn[] + */ + public function getAvailableColumns(): array; + + /** + * Returns the general UserPermission required to run this provider. + * + * @return UserPermissions + */ + public function getRequiredPermission(): UserPermissions; + +} diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php new file mode 100644 index 000000000..d53ed0668 --- /dev/null +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -0,0 +1,110 @@ +id !== null) { + $conditionParts[] = 'id = ?'; + $params[] = $terms->id; + } + if ($terms->firstname !== null) { + $conditionParts[] = 'firstname LIKE ?'; + $params[] = '%' . $terms->firstname . '%'; + } + if ($terms->lastname !== null) { + $conditionParts[] = 'lastname LIKE ?'; + $params[] = '%' . $terms->lastname . '%'; + } + if ($terms->email !== null) { + $conditionParts[] = 'email LIKE ?'; + $params[] = '%' . $terms->email . '%'; + } + } + + + // If we have conditions, apply them. + //check the result and test it , pending + if (!empty($conditionParts)) { + $listing->setCondition(implode(' OR ', $conditionParts), $params); + } + + $users = $listing->getUsers(); + + $results = []; + foreach ($users as $user) { + $results[] = [ + 'id' => $user->getId(), + 'name' => $user->getName(), + // 'firstname' => $user->getFirstname(), + // 'lastname' => $user->getLastname(), + // 'email' => $user->getEmail(), + ]; + } + + return $results; + } + + public function getName(): string + { + return 'Pimcore Users'; + } + + public function getKey(): string + { + return 'pimcore_users'; + } + + public function getSortPriority(): int + { + return 5; + } + + public function getRequiredPermission(): UserPermissions + { + return UserPermissions::PIMCORE_USER; + } + + public function getAvailableColumns(): array + { + return [ + new GdprDataColumn('id', 'ID'), + new GdprDataColumn('name', 'Username'), + // new GdprDataColumn('firstname', 'First Name'), + // new GdprDataColumn('lastname', 'Last Name'), + // new GdprDataColumn('email', 'Email'), + ]; + //How to get email firstname lastname + } + +} \ No newline at end of file diff --git a/src/Gdpr/Schema/GdprDataColumn.php b/src/Gdpr/Schema/GdprDataColumn.php new file mode 100644 index 000000000..d2edd1bb7 --- /dev/null +++ b/src/Gdpr/Schema/GdprDataColumn.php @@ -0,0 +1,53 @@ +key; + } + + public function getLabel(): string + { + return $this->label; + } +} diff --git a/src/Gdpr/Schema/GdprDataProvider.php b/src/Gdpr/Schema/GdprDataProvider.php new file mode 100644 index 000000000..23cae8149 --- /dev/null +++ b/src/Gdpr/Schema/GdprDataProvider.php @@ -0,0 +1,76 @@ + $columns + */ + public function __construct( + #[Property( + description: 'Unique key of the provider', + type: 'string', + example: 'data_objects' + )] + private readonly string $key, + + #[Property( + description: 'Label of the provider', + type: 'string', + example: 'Data Objects' + )] + private readonly string $label, + + #[Property( + description: 'List of column definitions for the result grid', + type: 'array', + items: new Items(ref: GdprDataColumn::class) + )] + private readonly array $columns, + ) { + } + + public function getKey(): string + { + return $this->key; + } + + public function getLabel(): string + { + return $this->label; + } + + /** + * @return GdprDataColumn[] + */ + public function getColumns(): array + { + return $this->columns; + } +} \ No newline at end of file diff --git a/src/Gdpr/Schema/GdprSearchResult.php b/src/Gdpr/Schema/GdprSearchResult.php new file mode 100644 index 000000000..78e106289 --- /dev/null +++ b/src/Gdpr/Schema/GdprSearchResult.php @@ -0,0 +1,63 @@ +> $results + */ + public function __construct( + #[Property( + description: 'The key of the provider these results came from single provider', + type: 'string', + example: 'data_objects' + )] + private string $providerKey, + + #[Property( + description: 'The list of results found by this provider', + type: 'array', + items: new Items(type: 'object', example: '{"id": 1, "path": "/data/customer/1"}') + )] + private array $results, + ) { + } + + public function getProviderKey(): string + { + return $this->providerKey; + } + + /** + * @return array> + */ + public function getResults(): array + { + return $this->results; + } +} \ No newline at end of file diff --git a/src/Gdpr/Schema/GdprSearchResultCollection.php b/src/Gdpr/Schema/GdprSearchResultCollection.php new file mode 100644 index 000000000..262466d8f --- /dev/null +++ b/src/Gdpr/Schema/GdprSearchResultCollection.php @@ -0,0 +1,53 @@ + $items + */ + public function __construct( + #[Property( + description: 'List of search results, grouped by provider', + type: 'array', + items: new Items(ref: GdprSearchResult::class) + )] + private array $items, + ) { + } + + /** + * @return array + */ + public function getItems(): array + { + return $this->items; + } + +} \ No newline at end of file diff --git a/src/Gdpr/Schema/GdprSearchResultProperty.php b/src/Gdpr/Schema/GdprSearchResultProperty.php new file mode 100644 index 000000000..ef596f2f3 --- /dev/null +++ b/src/Gdpr/Schema/GdprSearchResultProperty.php @@ -0,0 +1,55 @@ + + */ + public function getDataProviders(): array; + + /** + * @throws NotFoundException + */ + public function resolve(string $key): DataProviderInterface; +} \ No newline at end of file diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php new file mode 100644 index 000000000..e458a3810 --- /dev/null +++ b/src/Gdpr/Service/GdprManagerService.php @@ -0,0 +1,127 @@ +sortProviders($this->loader->getDataProviders()); + + return $this->getDataProviderCollection($providers); + } + + public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection + { + $allResults = []; + $currentUser = $this->securityService->getCurrentUser(); + + foreach ($request->providers as $providerKey) { + $provider = $this->loader->resolve($providerKey); + + $permission = $provider->getRequiredPermission(); + + // Check if the current user has the required permission to access the provider + if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { + throw new ForbiddenException( + sprintf( + 'Not allowed to access the targeted provider "%s". Required permission: "%s"', + $providerKey, + $permission->value + ) + ); + } + + $results = $provider->findData($request->searchTerms); + + if (!empty($results)) { + $allResults[] = new GdprSearchResult( + providerKey: $providerKey, + results: $results + ); + } + } + + return new GdprSearchResultCollection($allResults); + } + + /** + * @param array $providers + * + * @return Collection + */ + private function getDataProviderCollection(array $providers): Collection + { + $items = []; + + foreach ($providers as $key => $provider) { + $item = new GdprDataProvider( + key: $key, + label: $provider->getName(), + columns: $provider->getAvailableColumns(), + ); + + $this->eventDispatcher->dispatch( + new GdprDataProviderEvent($item), + GdprDataProviderEvent::EVENT_NAME + ); + + $items[] = $item; + } + + + return new Collection(count($items), $items); + } + + /** + * Sorts the providers by priority. + * + * @param array $providers + * + * @return array + */ + private function sortProviders(array $providers): array + { + // Higher number = Higher priority. + uasort($providers, static fn (DataProviderInterface $a, DataProviderInterface $b): int + => $b->getSortPriority() <=> $a->getSortPriority() + ); + + return $providers; + } +} diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php new file mode 100644 index 000000000..32324a8a1 --- /dev/null +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -0,0 +1,40 @@ + + */ + public function getAvailableProviders(): Collection; + + /** + * Searches for data in the specified providers. + * + * @throws ForbiddenException + */ + public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection; +} diff --git a/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php b/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php new file mode 100644 index 000000000..ea10c832e --- /dev/null +++ b/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php @@ -0,0 +1,52 @@ +providers as $provider) { + $providers[$provider->getKey()] = $provider; + } + + return $providers; + } + + public function resolve(string $key): DataProviderInterface + { + foreach ($this->providers as $provider) { + if ($provider->getKey() === $key) { + return $provider; + } + } + + throw new NotFoundException('GDPR data provider', $key, 'key'); + } +} diff --git a/src/OpenApi/Config/Tags.php b/src/OpenApi/Config/Tags.php index 0bc6d6c33..4cf0132a4 100644 --- a/src/OpenApi/Config/Tags.php +++ b/src/OpenApi/Config/Tags.php @@ -74,6 +74,10 @@ name: Tags::Export->value, description: 'tag_export_description' )] +#[Tag( + name: Tags::GDPR->value, + description: 'tag_gdpr_description' +)] #[Tag( name: Tags::Mercure->value, description: 'tag_mercure_description' @@ -181,6 +185,7 @@ enum Tags: string case ExecutionEngine = 'Execution Engine'; case Emails = 'E-Mails'; case Export = 'Export'; + case GDPR = 'GDPR Data Extractor'; case Mercure = 'Mercure'; case Metadata = 'Metadata'; case Notes = 'Notes'; diff --git a/src/PimcoreStudioBackendBundle.php b/src/PimcoreStudioBackendBundle.php index 6a9575d4e..af56006c5 100644 --- a/src/PimcoreStudioBackendBundle.php +++ b/src/PimcoreStudioBackendBundle.php @@ -27,6 +27,7 @@ use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\MercureTopicsProviderPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\PatchAdapterPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\PhpCodeTransformerPass; +use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\DataProviderPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\SettingsProviderPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\TransformerPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\UpdateAdapterPass; @@ -94,6 +95,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new FieldDefinitionResolverPass()); $container->addCompilerPass(new TransformerPass()); $container->addCompilerPass(new PhpCodeTransformerPass()); + $container->addCompilerPass(new DataProviderPass()); $container->addCompilerPass(new DocumentTypeAdapterPass()); } diff --git a/translations/studio_api_docs.en.yaml b/translations/studio_api_docs.en.yaml index bcf4f5903..85853b785 100644 --- a/translations/studio_api_docs.en.yaml +++ b/translations/studio_api_docs.en.yaml @@ -1413,3 +1413,6 @@ data_object_get_phpcode_transformers_description: | These transformers can be applied to grid columns to dynamically modify or format values using PHP logic. data_object_get_phpcode_transformers_summary: List PHPCode transformers data_object_get_phpcode_transformers_success_response: List of available PHPCode transformers +gdpr_list_providers_summary: List of available GDPR providers +gdpr_list_providers_description: Returns a list of all configured GDPR providers that can be used for data compliance operations. +gdpr_list_providers_success_response: Successfully retrieved the list of GDPR providers \ No newline at end of file From d1e2af31b300755f2ff9ccf4aab80b1aabcb2796 Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:22:28 +0000 Subject: [PATCH 02/34] Apply php-cs-fixer changes --- .../Attribute/Request/GdprRequestBody.php | 12 +++---- src/Gdpr/Attribute/Request/SearchTerms.php | 9 ++--- .../Controller/GetDataProviderController.php | 15 ++++---- .../SearchDataProviderController.php | 25 ++++++-------- .../PreResponse/GdprDataProviderEvent.php | 5 ++- .../GdprStructuredSearchRequest.php | 6 ++-- src/Gdpr/Provider/AssetsProvider.php | 10 +++--- src/Gdpr/Provider/DataObjectProvider.php | 12 +++---- src/Gdpr/Provider/DataProviderInterface.php | 11 +++--- src/Gdpr/Provider/PimcoreUserProvider.php | 10 +++--- src/Gdpr/Schema/GdprDataColumn.php | 4 +-- src/Gdpr/Schema/GdprDataProvider.php | 6 ++-- src/Gdpr/Schema/GdprSearchResult.php | 6 ++-- .../Schema/GdprSearchResultCollection.php | 7 ++-- src/Gdpr/Schema/GdprSearchResultProperty.php | 6 ++-- .../Service/DataProviderLoaderInterface.php | 2 +- src/Gdpr/Service/GdprManagerService.php | 34 +++++++++---------- .../Service/GdprManagerServiceInterface.php | 5 ++- .../TaggedIteratorDataProviderLoader.php | 4 +-- src/PimcoreStudioBackendBundle.php | 2 +- 20 files changed, 92 insertions(+), 99 deletions(-) diff --git a/src/Gdpr/Attribute/Request/GdprRequestBody.php b/src/Gdpr/Attribute/Request/GdprRequestBody.php index e22ac9ec7..422ab99b6 100644 --- a/src/Gdpr/Attribute/Request/GdprRequestBody.php +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -7,18 +7,17 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request; use Attribute; -use OpenApi\Attributes\JsonContent; use OpenApi\Attributes\Items; +use OpenApi\Attributes\JsonContent; use OpenApi\Attributes\Property; use OpenApi\Attributes\RequestBody; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; /** * @internal @@ -38,9 +37,9 @@ public function __construct() description: 'A list of provider keys to search (e.g., data_objects, emails, etc.)', type: 'array', items: new Items( - type: 'string', + type: 'string', example: 'data_objects' - ) + ) ), new Property( property: 'searchTerms', @@ -55,4 +54,3 @@ public function __construct() ); } } - diff --git a/src/Gdpr/Attribute/Request/SearchTerms.php b/src/Gdpr/Attribute/Request/SearchTerms.php index ae98d8d62..ec9fafc22 100644 --- a/src/Gdpr/Attribute/Request/SearchTerms.php +++ b/src/Gdpr/Attribute/Request/SearchTerms.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request; @@ -17,6 +17,7 @@ use OpenApi\Attributes\Schema; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Type; + /** * @internal */ @@ -41,12 +42,12 @@ public function __construct( public ?string $lastname = null, #[Property(description: 'The email address to search for.', type: 'string', nullable: true)] - #[Type('string')]//why is #[Email] constraint causing issues + #[Type('string')]//why is #[Email] constraint causing issues public ?string $email = null, ) { } - public function getId(): ?string + public function getId(): ?string { return $this->id; } diff --git a/src/Gdpr/Controller/GetDataProviderController.php b/src/Gdpr/Controller/GetDataProviderController.php index 22bc25344..a04715d7a 100644 --- a/src/Gdpr/Controller/GetDataProviderController.php +++ b/src/Gdpr/Controller/GetDataProviderController.php @@ -16,11 +16,13 @@ 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; +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\OpenApi\Attribute\Property\GenericCollection; -use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\CollectionJson; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Bundle\StudioBackendBundle\Util\Trait\PaginatedResponseTrait; @@ -28,12 +30,10 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataProvider; - /** * Returns the list of available GDPR providers. + * * @internal */ final class GetDataProviderController extends AbstractApiController @@ -51,8 +51,8 @@ public function __construct( * @throws NotFoundException */ #[Route( - '/gdpr/providers', - name: 'pimcore_studio_api_gdpr_providers', + '/gdpr/providers', + name: 'pimcore_studio_api_gdpr_providers', methods: ['GET'])] #[IsGranted(UserPermissions::GDPR->value)] #[GET( @@ -72,7 +72,6 @@ public function __construct( HttpResponseCodes::FORBIDDEN, HttpResponseCodes::NOT_FOUND, ])] - public function getProvidersList(): JsonResponse { $collection = $this->gdprManagerService->getAvailableProviders(); diff --git a/src/Gdpr/Controller/SearchDataProviderController.php b/src/Gdpr/Controller/SearchDataProviderController.php index e19e4a2c3..637be88a8 100644 --- a/src/Gdpr/Controller/SearchDataProviderController.php +++ b/src/Gdpr/Controller/SearchDataProviderController.php @@ -17,27 +17,26 @@ 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\Gdpr\Schema\GdprSearchResultProperty; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; +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\OpenApi\Attribute\Response\Content\CollectionJson; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultProperty; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; /** * @internal */ final class SearchDataProviderController extends AbstractApiController { - public function __construct( SerializerInterface $serializer, private readonly GdprManagerServiceInterface $gdprManagerService, @@ -47,12 +46,12 @@ public function __construct( /** * Handles GDPR data search requests across different providers. - * + * * @throws NotFoundException - */ + */ #[Route( - '/gdpr/search', - name: 'pimcore_studio_api_gdpr_search', + '/gdpr/search', + name: 'pimcore_studio_api_gdpr_search', methods: ['POST'])] #[IsGranted(UserPermissions::GDPR->value)] #[POST( @@ -66,8 +65,8 @@ public function __construct( #[SuccessResponse( description: 'gdpr_search_data_success_response', content: new CollectionJson( - collection: new GdprSearchResultProperty() - ) + collection: new GdprSearchResultProperty() + ) )] #[DefaultResponses([ @@ -76,7 +75,6 @@ public function __construct( HttpResponseCodes::NOT_FOUND, HttpResponseCodes::BAD_REQUEST, ])] - public function searchData( #[MapRequestPayload] GdprStructuredSearchRequest $request ): JsonResponse { @@ -85,5 +83,4 @@ public function searchData( return $this->jsonResponse($collection); } - } diff --git a/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php index b2ad7b4f3..928e8520e 100644 --- a/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php +++ b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse; @@ -33,4 +33,3 @@ public function getProvider(): GdprDataProvider return $this->provider; } } - diff --git a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php index 96f2eef20..bf2848041 100644 --- a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php +++ b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter; @@ -36,4 +36,4 @@ public function __construct( public ?SearchTerms $searchTerms = null ) { } -} \ No newline at end of file +} diff --git a/src/Gdpr/Provider/AssetsProvider.php b/src/Gdpr/Provider/AssetsProvider.php index 6cda79360..edd4caf8d 100644 --- a/src/Gdpr/Provider/AssetsProvider.php +++ b/src/Gdpr/Provider/AssetsProvider.php @@ -7,20 +7,19 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; /** * @internal -*/ + */ final readonly class AssetsProvider implements DataProviderInterface { public function findData(?SearchTerms $terms): array @@ -46,6 +45,7 @@ public function findData(?SearchTerms $terms): array return $results; } + public function getRequiredPermission(): UserPermissions { return UserPermissions::DATA_OBJECTS; diff --git a/src/Gdpr/Provider/DataObjectProvider.php b/src/Gdpr/Provider/DataObjectProvider.php index 2457ae882..89dfd2e53 100644 --- a/src/Gdpr/Provider/DataObjectProvider.php +++ b/src/Gdpr/Provider/DataObjectProvider.php @@ -7,19 +7,19 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; + /** * @internal -*/ + */ final readonly class DataObjectProvider implements DataProviderInterface { public function findData(?SearchTerms $terms): array @@ -85,4 +85,4 @@ public function getRequiredPermission(): UserPermissions { return UserPermissions::DATA_OBJECTS; } -} \ No newline at end of file +} diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index c300a7074..f0978e7ec 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; @@ -18,6 +18,7 @@ use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Model\Element\AbstractElement; + /** * @internal */ @@ -27,6 +28,7 @@ interface DataProviderInterface * Searches for personal data within this provider's domain. * * @param SearchTerms|null $terms The search values (can be null if none provided) + * * @return array A list of found elements (Objects, Assets, etc.) */ public function findData(?SearchTerms $terms): array; @@ -40,17 +42,19 @@ public function getName(): string; /** * Returns the unique identifying key for this provider. + * * @return string */ public function getKey(): string; /** * A higher number means a higher priority (appears first). + * * @return int */ public function getSortPriority(): int; - /** + /** * Returns the list of available columns for the result data. * * @return GdprDataColumn[] @@ -63,5 +67,4 @@ public function getAvailableColumns(): array; * @return UserPermissions */ public function getRequiredPermission(): UserPermissions; - } diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index d53ed0668..7b89a9bd1 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; @@ -51,7 +51,6 @@ public function findData(?SearchTerms $terms): array $params[] = '%' . $terms->email . '%'; } } - // If we have conditions, apply them. //check the result and test it , pending @@ -72,7 +71,7 @@ public function findData(?SearchTerms $terms): array ]; } - return $results; + return $results; } public function getName(): string @@ -106,5 +105,4 @@ public function getAvailableColumns(): array ]; //How to get email firstname lastname } - -} \ No newline at end of file +} diff --git a/src/Gdpr/Schema/GdprDataColumn.php b/src/Gdpr/Schema/GdprDataColumn.php index d2edd1bb7..3bccf6b79 100644 --- a/src/Gdpr/Schema/GdprDataColumn.php +++ b/src/Gdpr/Schema/GdprDataColumn.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; diff --git a/src/Gdpr/Schema/GdprDataProvider.php b/src/Gdpr/Schema/GdprDataProvider.php index 23cae8149..f16199031 100644 --- a/src/Gdpr/Schema/GdprDataProvider.php +++ b/src/Gdpr/Schema/GdprDataProvider.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; @@ -73,4 +73,4 @@ public function getColumns(): array { return $this->columns; } -} \ No newline at end of file +} diff --git a/src/Gdpr/Schema/GdprSearchResult.php b/src/Gdpr/Schema/GdprSearchResult.php index 78e106289..dbee9f692 100644 --- a/src/Gdpr/Schema/GdprSearchResult.php +++ b/src/Gdpr/Schema/GdprSearchResult.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; @@ -60,4 +60,4 @@ public function getResults(): array { return $this->results; } -} \ No newline at end of file +} diff --git a/src/Gdpr/Schema/GdprSearchResultCollection.php b/src/Gdpr/Schema/GdprSearchResultCollection.php index 262466d8f..30561d166 100644 --- a/src/Gdpr/Schema/GdprSearchResultCollection.php +++ b/src/Gdpr/Schema/GdprSearchResultCollection.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; @@ -49,5 +49,4 @@ public function getItems(): array { return $this->items; } - -} \ No newline at end of file +} diff --git a/src/Gdpr/Schema/GdprSearchResultProperty.php b/src/Gdpr/Schema/GdprSearchResultProperty.php index ef596f2f3..2e6143ec3 100644 --- a/src/Gdpr/Schema/GdprSearchResultProperty.php +++ b/src/Gdpr/Schema/GdprSearchResultProperty.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; @@ -52,4 +52,4 @@ public function __construct() ) ); } -} \ No newline at end of file +} diff --git a/src/Gdpr/Service/DataProviderLoaderInterface.php b/src/Gdpr/Service/DataProviderLoaderInterface.php index 81c125099..11e556087 100644 --- a/src/Gdpr/Service/DataProviderLoaderInterface.php +++ b/src/Gdpr/Service/DataProviderLoaderInterface.php @@ -32,4 +32,4 @@ public function getDataProviders(): array; * @throws NotFoundException */ public function resolve(string $key): DataProviderInterface; -} \ No newline at end of file +} diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index e458a3810..c2d5a811b 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -13,18 +13,19 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Service; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprDataProviderEvent; +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\Gdpr\MappedParameter\GdprStructuredSearchRequest; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\DataProviderLoaderInterface; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use function count; +use function sprintf; /** * @internal @@ -55,7 +56,7 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo $provider = $this->loader->resolve($providerKey); $permission = $provider->getRequiredPermission(); - + // Check if the current user has the required permission to access the provider if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { throw new ForbiddenException( @@ -65,19 +66,19 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo $permission->value ) ); - } + } - $results = $provider->findData($request->searchTerms); + $results = $provider->findData($request->searchTerms); - if (!empty($results)) { - $allResults[] = new GdprSearchResult( - providerKey: $providerKey, - results: $results - ); + if (!empty($results)) { + $allResults[] = new GdprSearchResult( + providerKey: $providerKey, + results: $results + ); + } } - } - return new GdprSearchResultCollection($allResults); + return new GdprSearchResultCollection($allResults); } /** @@ -103,12 +104,11 @@ private function getDataProviderCollection(array $providers): Collection $items[] = $item; } - return new Collection(count($items), $items); } - /** + /** * Sorts the providers by priority. * * @param array $providers diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index 32324a8a1..eb4323b59 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -13,11 +13,10 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Service; -use Pimcore\Bundle\StudioBackendBundle\Response\Collection; -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\Gdpr\Schema\GdprSearchResultCollection; +use Pimcore\Bundle\StudioBackendBundle\Response\Collection; /** * @internal diff --git a/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php b/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php index ea10c832e..d210c39ec 100644 --- a/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php +++ b/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\Loader; diff --git a/src/PimcoreStudioBackendBundle.php b/src/PimcoreStudioBackendBundle.php index af56006c5..ac73fb5e3 100644 --- a/src/PimcoreStudioBackendBundle.php +++ b/src/PimcoreStudioBackendBundle.php @@ -18,6 +18,7 @@ use Pimcore\Bundle\GenericExecutionEngineBundle\PimcoreGenericExecutionEngineBundle; use Pimcore\Bundle\StaticResolverBundle\PimcoreStaticResolverBundle; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\DataIndexFilterPass; +use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\DataProviderPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\DocumentTypeAdapterPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\FieldDefinitionResolverPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\FilterMapperPass; @@ -27,7 +28,6 @@ use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\MercureTopicsProviderPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\PatchAdapterPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\PhpCodeTransformerPass; -use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\DataProviderPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\SettingsProviderPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\TransformerPass; use Pimcore\Bundle\StudioBackendBundle\DependencyInjection\CompilerPass\UpdateAdapterPass; From 28fc4da826f4297eee017edf21499843d6c01821 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 12 Nov 2025 08:48:10 +0000 Subject: [PATCH 03/34] Addition of new manager function to run job and fixes --- config/gdpr.yaml | 9 -- doc/10_Extending_Studio/11_Gdpr.md | 143 ++++++++++++++++++ src/Gdpr/Controller/DownloadController.php | 69 +++++++++ src/Gdpr/Controller/ExportController.php | 82 ++++++++++ src/Gdpr/Provider/DataProviderInterface.php | 6 +- src/Gdpr/Schema/GdprDataColumn.php | 3 + src/Gdpr/Schema/GdprDataProvider.php | 5 +- src/Gdpr/Schema/GdprExportJob.php | 26 ++++ src/Gdpr/Schema/GdprExportJobCollection.php | 53 +++++++ src/Gdpr/Schema/GdprSearchResultProperty.php | 6 - src/Gdpr/Service/GdprManagerService.php | 48 +++++- .../Service/GdprManagerServiceInterface.php | 11 ++ 12 files changed, 443 insertions(+), 18 deletions(-) create mode 100644 doc/10_Extending_Studio/11_Gdpr.md create mode 100644 src/Gdpr/Controller/DownloadController.php create mode 100644 src/Gdpr/Controller/ExportController.php create mode 100644 src/Gdpr/Schema/GdprExportJob.php create mode 100644 src/Gdpr/Schema/GdprExportJobCollection.php diff --git a/config/gdpr.yaml b/config/gdpr.yaml index 128a3fe83..487f764a0 100644 --- a/config/gdpr.yaml +++ b/config/gdpr.yaml @@ -20,12 +20,3 @@ services: class: Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\Loader\TaggedIteratorDataProviderLoader # --- GDPR Providers --- - - 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"] - - Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\PimcoreUserProvider: - tags: ["pimcore.studio_backend.gdpr_data_provider"] \ No newline at end of file diff --git a/doc/10_Extending_Studio/11_Gdpr.md b/doc/10_Extending_Studio/11_Gdpr.md new file mode 100644 index 000000000..8e0f2ce32 --- /dev/null +++ b/doc/10_Extending_Studio/11_Gdpr.md @@ -0,0 +1,143 @@ +# Extending GDPR Data Providers + +The GDPR Data Provider system provides a centralized interface to find and export personal data from any part of your Pimcore application. You can add new data sources (like Data Objects, Assets, Users, or any custom entity) by creating your own provider. + +New providers are created by implementing the `Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface` and tagging your class as a service with `pimcore.studio_backend.gdpr_data_provider`. + +## How does it work + +The `GdprManagerService` acts as the central coordinator for all registered providers. It automatically discovers your tagged service. + +### 🔎 For Searching + +1. The manager loads all tagged providers to build the search interface. It calls your provider's `getName()`, `getKey()`, `getSortPriority()`, and `getAvailableColumns()` methods. +2. When a user performs a search, the manager first checks `getRequiredPermission()` to see if the current user is allowed to use your provider. +3. If permitted, the manager calls your provider's `findData()` method, passing the user's search terms. The results are then displayed in the grid. + +### For Exporting + +The export process is a two-step flow handled by your provider: + +1. **Start Job:** The manager calls `startJobExecution()`. Your provider is responsible for starting a background process (e.g., a Symfony Messenger job) and immediately returning a **unique Job ID** (as a string). +2. **Get File:** When the user clicks the download button for that job, the manager loops through all providers and calls `ownsJob($jobId)` on each one to find the correct owner. + - Once the owner is found, the manager calls `getExportFile($jobId)`. + - Your provider is then responsible for finding the completed job's file, checking specific permissions (e.g., "does this user own this job?"), and streaming the file back as a `StreamedResponse`. + +## Example Data Provider + +Here is a example of a provider that searches for **Customer** data objects. + +```php + +final class CustomerObjectProvider implements DataProviderInterface +{ + + public function __construct( + ) { + } + + /** + * A unique key for your provider. + */ + public function getKey(): string + { + return 'customers'; + } + + /** + * A human-friendly name shown in the UI. + */ + public function getName(): string + { + return 'Customer Objects'; + } + + /** + * Sort order for the UI. Higher numbers appear first. + */ + public function getSortPriority(): int + { + return 10; + } + + /** + * The general permission needed to use this provider. + */ + public function getRequiredPermission(): UserPermissions + { + // Users must have 'objects' permission to use this provider + return UserPermissions::OBJECTS; + } + + /** + * Defines the columns for the search result grid. + * The 'key' must match the key in the array returned by findData(). + * + * @return GdprDataColumn[] + */ + public function getAvailableColumns(): array + { + return [ + new GdprDataColumn('id', 'ID'), + new GdprDataColumn('email', 'Email Address'), + new GdDprDataColumn('path', 'Full Path'), + ]; + } + + /** + * The core search logic. + * + * @return array> + */ + public function findData(?SearchTerms $terms): array + { + //Finds the data matched with search terms + + } + + /** + * Starts the background export job. + * + * @return string The unique Job ID + */ + public function startJobExecution(GdprStructuredSearchRequest $request): string + { + $jobId = '1';//Create a job id + + return $jobId; + } + + /** + * A quick check to see if this provider is responsible for a job. + * This should be a fast check (e.g., checking a job type or ID prefix). + */ + public function ownsJob(int $jobRunId): bool + { + // return $this->jobService->doesJobExist? + + } + + /** + * Finds the completed job file and streams it. + * This is only called after ownsJob() returns true. + */ + public function getExportFile(int $jobRunId): StreamedResponse + { + // 1. Find the job in your storage + + // 2. If the job doesn't exist (or isn't yours), you MUST throw this + // if ($job === null) { + // throw new NotFoundException('Export job not found'); + // } + + + // 3. Find the file on disk (or stream from S3, etc.) + // $filePath = $this->fileService->getFilePath($job->getFileName()); + // if (!$this->fileService->exists($filePath)) { + // throw new NotFoundException('Export file is missing or not yet generated.'); + // } + + //Return response + } +} +``` diff --git a/src/Gdpr/Controller/DownloadController.php b/src/Gdpr/Controller/DownloadController.php new file mode 100644 index 000000000..35568767f --- /dev/null +++ b/src/Gdpr/Controller/DownloadController.php @@ -0,0 +1,69 @@ +value)] + #[Get( + path: self::PREFIX . '/gdpr/export/download/{jobId}', + operationId: 'download_gdpr_export', + summary: 'download_gdpr_export_summary', + description: 'download_gdpr_export_description', + tags: [Tags::Export->name] + )] + #[SuccessResponse( + description: 'The exported file (CSV or XLSX)' + )] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::FORBIDDEN, + HttpResponseCodes::NOT_FOUND, + ])] + + public function download(int $jobId): StreamedResponse + { + return $this->gdprManagerService->getExportFile( + $jobId + ); + } +} \ No newline at end of file diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php new file mode 100644 index 000000000..f1e21ae42 --- /dev/null +++ b/src/Gdpr/Controller/ExportController.php @@ -0,0 +1,82 @@ +value)] + #[GET(summary: 'Start background export job', tags: ['GDPR'])] + #[GET( + path: self::PREFIX . '/gdpr/export/start', + operationId: 'start_gdpr_export', + summary: 'start_gdpr_export_summary', + description: 'start_gdpr_export_description', + tags: [Tags::Export->name] + )] + #[GdprRequestBody] + #[SuccessResponse( + description: 'Job accepted and started', + content: new JsonContent( + properties: [ + new Property(property: 'jobId', type: 'string', example: '123e4567...'), + new Property(property: 'status', type: 'string', example: 'started'), + ] + ) + )] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::FORBIDDEN, + HttpResponseCodes::BAD_REQUEST, + ])] + public function startExport( + #[MapRequestPayload] GdprStructuredSearchRequest $request + ): JsonResponse { + $jobId = $this->gdprManagerService->startBackgroundExport($request); + + return new JsonResponse(['jobId' => $jobId, 'status' => 'started'], 202); + } +} \ No newline at end of file diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index f0978e7ec..da9fca1ad 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -14,8 +14,12 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; +use Symfony\Component\HttpFoundation\StreamedResponse; + use Pimcore\Model\Element\AbstractElement; diff --git a/src/Gdpr/Schema/GdprDataColumn.php b/src/Gdpr/Schema/GdprDataColumn.php index 3bccf6b79..c229d2682 100644 --- a/src/Gdpr/Schema/GdprDataColumn.php +++ b/src/Gdpr/Schema/GdprDataColumn.php @@ -16,6 +16,9 @@ use OpenApi\Attributes\Property; use OpenApi\Attributes\Schema; +/** + * @internal + */ #[Schema( title: 'GDPR Data Column', description: 'A single column definition for the GDPR data result grid', diff --git a/src/Gdpr/Schema/GdprDataProvider.php b/src/Gdpr/Schema/GdprDataProvider.php index f16199031..9dbd256b3 100644 --- a/src/Gdpr/Schema/GdprDataProvider.php +++ b/src/Gdpr/Schema/GdprDataProvider.php @@ -19,9 +19,12 @@ use Pimcore\Bundle\StudioBackendBundle\Util\Schema\AdditionalAttributesInterface; use Pimcore\Bundle\StudioBackendBundle\Util\Trait\AdditionalAttributesTrait; +/** + * @internal + */ #[Schema( title: 'GDPR Data Provider', - description: 'Represents a single data source (e.g., "Data Objects", "Sent Mails") available for searching in the GDPR Data Extractor tool.', + description: 'GDPR Data Extractor search source(e.g., "Data Objects", "Pimcore user").', required: ['key', 'label', 'columns'], type: 'object', )] diff --git a/src/Gdpr/Schema/GdprExportJob.php b/src/Gdpr/Schema/GdprExportJob.php new file mode 100644 index 000000000..42d198709 --- /dev/null +++ b/src/Gdpr/Schema/GdprExportJob.php @@ -0,0 +1,26 @@ + $items + */ + public function __construct( + #[Property( + description: 'List of started export jobs', + type: 'array', + items: new Items(ref: GdprExportJob::class) + )] + + private readonly array $items, + ) { + } + + /** + * @return array + */ + public function getItems(): array + { + return $this->items; + } +} \ No newline at end of file diff --git a/src/Gdpr/Schema/GdprSearchResultProperty.php b/src/Gdpr/Schema/GdprSearchResultProperty.php index 2e6143ec3..e0ec41269 100644 --- a/src/Gdpr/Schema/GdprSearchResultProperty.php +++ b/src/Gdpr/Schema/GdprSearchResultProperty.php @@ -42,12 +42,6 @@ public function __construct() type: 'array', items: new Items(type: 'object', example: '{"id": 1, "path": "/data/customer/1"}') ), - new Property( - property: 'additionalAttributes', - description: 'Additional attributes for the search result', - type: 'object', - nullable: true - ), ] ) ); diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index c2d5a811b..f64ab47d3 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -14,6 +14,7 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Service; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprDataProviderEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface; @@ -36,7 +37,6 @@ public function __construct( private DataProviderLoaderInterface $loader, private EventDispatcherInterface $eventDispatcher, private SecurityServiceInterface $securityService, - private AuthorizationCheckerInterface $authChecker ) { } @@ -81,6 +81,52 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo return new GdprSearchResultCollection($allResults); } + public function startBackgroundExport(GdprStructuredSearchRequest $request): GdprExportJobCollection + { + $jobs = []; + $currentUser = $this->securityService->getCurrentUser(); + + foreach ($request->providers as $providerKey) { + $provider = $this->loader->resolve($providerKey); + + $permission = $provider->getRequiredPermission(); + if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { + throw new ForbiddenException("Not allowed for provider: $providerKey"); + } + + $jobId = $provider->startJobExecution($request); + $jobs[] = new GdprExportJob($providerKey, $jobId); + } + + return new GdprExportJobCollection($jobs); + } + + /** + * @throws ForbiddenException + * @throws NotFoundException + */ + public function getExportFile(int $jobId): StreamedResponse + { + $currentUser = $this->securityService->getCurrentUser(); + + $providers = $this->loader->getDataProviders(); + + foreach ($providers as $provider) { + + $permission = $provider->getRequiredPermission(); + if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { + throw new ForbiddenException("Not allowed for provider: $provider"); + } + + if ($provider->ownsJob($jobId)) { + + return $provider->getExportFile($jobId); + } + } + + throw new NotFoundException('Export job with ID %d not found or access denied.', $jobId); + } + /** * @param array $providers * diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index eb4323b59..ead8ef90b 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -36,4 +36,15 @@ public function getAvailableProviders(): Collection; * @throws ForbiddenException */ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection; + + /** + * @throws ForbiddenException + */ + public function startBackgroundExport(GdprStructuredSearchRequest $request): GdprExportJobCollection; + + /** + * @throws ForbiddenException + * @throws NotFoundException + */ + public function getExportFile(int $jobRunId): StreamedResponse; } From afe09393f6dded19c0e52345c3409048dee3f76f Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Wed, 12 Nov 2025 08:50:48 +0000 Subject: [PATCH 04/34] Apply php-cs-fixer changes --- src/Gdpr/Controller/DownloadController.php | 7 +++---- src/Gdpr/Controller/ExportController.php | 18 +++++++++--------- src/Gdpr/Provider/DataProviderInterface.php | 6 +----- src/Gdpr/Schema/GdprExportJob.php | 8 ++++---- src/Gdpr/Schema/GdprExportJobCollection.php | 16 +++++++++++++--- src/Gdpr/Service/GdprManagerService.php | 9 ++++----- .../Service/GdprManagerServiceInterface.php | 2 +- 7 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/Gdpr/Controller/DownloadController.php b/src/Gdpr/Controller/DownloadController.php index 35568767f..2b541bbc3 100644 --- a/src/Gdpr/Controller/DownloadController.php +++ b/src/Gdpr/Controller/DownloadController.php @@ -15,16 +15,16 @@ use OpenApi\Attributes\Get; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; 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\Gdpr\Service\GdprManagerServiceInterface; +use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\HttpFoundation\StreamedResponse; /** * @internal @@ -59,11 +59,10 @@ public function __construct( HttpResponseCodes::FORBIDDEN, HttpResponseCodes::NOT_FOUND, ])] - public function download(int $jobId): StreamedResponse { return $this->gdprManagerService->getExportFile( $jobId ); } -} \ No newline at end of file +} diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index f1e21ae42..b44903a5d 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -14,17 +14,17 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Controller; use OpenApi\Attributes\Get; -use OpenApi\Attributes\Property; use OpenApi\Attributes\JsonContent; +use OpenApi\Attributes\Property; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\GdprRequestBody; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; 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\Gdpr\Service\GdprManagerServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Attribute\Route; @@ -44,13 +44,13 @@ public function __construct( } #[Route( - '/gdpr/export/start', - name: 'pimcore_studio_api_gdpr_export_start', + '/gdpr/export/start', + name: 'pimcore_studio_api_gdpr_export_start', methods: ['POST'] - )] + )] #[IsGranted(UserPermissions::GDPR->value)] #[GET(summary: 'Start background export job', tags: ['GDPR'])] - #[GET( + #[GET( path: self::PREFIX . '/gdpr/export/start', operationId: 'start_gdpr_export', summary: 'start_gdpr_export_summary', @@ -74,9 +74,9 @@ public function __construct( ])] public function startExport( #[MapRequestPayload] GdprStructuredSearchRequest $request - ): JsonResponse { + ): JsonResponse { $jobId = $this->gdprManagerService->startBackgroundExport($request); return new JsonResponse(['jobId' => $jobId, 'status' => 'started'], 202); } -} \ No newline at end of file +} diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index da9fca1ad..f0978e7ec 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -14,12 +14,8 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; -use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; -use Symfony\Component\HttpFoundation\StreamedResponse; - +use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Model\Element\AbstractElement; diff --git a/src/Gdpr/Schema/GdprExportJob.php b/src/Gdpr/Schema/GdprExportJob.php index 42d198709..70e85ebd3 100644 --- a/src/Gdpr/Schema/GdprExportJob.php +++ b/src/Gdpr/Schema/GdprExportJob.php @@ -7,15 +7,15 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; /** * @internal -*/ + */ final readonly class GdprExportJob { public function __construct( @@ -23,4 +23,4 @@ public function __construct( public string $jobId, ) { } -} \ No newline at end of file +} diff --git a/src/Gdpr/Schema/GdprExportJobCollection.php b/src/Gdpr/Schema/GdprExportJobCollection.php index c2cd6d388..c8f91a39c 100644 --- a/src/Gdpr/Schema/GdprExportJobCollection.php +++ b/src/Gdpr/Schema/GdprExportJobCollection.php @@ -12,6 +12,16 @@ declare(strict_types=1); +/** + * This source file is available under the terms of the + * Pimcore Open Core License (POCL) + * Full copyright and license information is available in + * LICENSE.md which is distributed with this source code. + * + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) + */ + namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; use OpenApi\Attributes\Items; @@ -38,8 +48,8 @@ public function __construct( type: 'array', items: new Items(ref: GdprExportJob::class) )] - - private readonly array $items, + + private readonly array $items, ) { } @@ -50,4 +60,4 @@ public function getItems(): array { return $this->items; } -} \ No newline at end of file +} diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index f64ab47d3..9f7392b6b 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -23,7 +23,6 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; -use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function count; use function sprintf; @@ -108,22 +107,22 @@ public function startBackgroundExport(GdprStructuredSearchRequest $request): Gdp public function getExportFile(int $jobId): StreamedResponse { $currentUser = $this->securityService->getCurrentUser(); - + $providers = $this->loader->getDataProviders(); foreach ($providers as $provider) { - + $permission = $provider->getRequiredPermission(); if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { throw new ForbiddenException("Not allowed for provider: $provider"); } if ($provider->ownsJob($jobId)) { - + return $provider->getExportFile($jobId); } } - + throw new NotFoundException('Export job with ID %d not found or access denied.', $jobId); } diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index ead8ef90b..591eb4514 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -39,7 +39,7 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo /** * @throws ForbiddenException - */ + */ public function startBackgroundExport(GdprStructuredSearchRequest $request): GdprExportJobCollection; /** From 86cf5e3e03727d1f5994dfa255850757650844dd Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 12 Nov 2025 09:05:48 +0000 Subject: [PATCH 05/34] Static code analysis fixes --- src/Gdpr/Provider/AssetsProvider.php | 87 -------------- src/Gdpr/Provider/DataObjectProvider.php | 88 -------------- src/Gdpr/Provider/PimcoreUserProvider.php | 108 ------------------ src/Gdpr/Service/GdprManagerService.php | 3 + .../Service/GdprManagerServiceInterface.php | 4 + 5 files changed, 7 insertions(+), 283 deletions(-) delete mode 100644 src/Gdpr/Provider/AssetsProvider.php delete mode 100644 src/Gdpr/Provider/DataObjectProvider.php delete mode 100644 src/Gdpr/Provider/PimcoreUserProvider.php diff --git a/src/Gdpr/Provider/AssetsProvider.php b/src/Gdpr/Provider/AssetsProvider.php deleted file mode 100644 index edd4caf8d..000000000 --- a/src/Gdpr/Provider/AssetsProvider.php +++ /dev/null @@ -1,87 +0,0 @@ - 1, - 'path' => '/test/assets-1', - 'description' => 'Hello World', - ], - [ - 'id' => 2, - 'path' => '/test/assets-2', - 'description' => 'Hello Pimcore', - ], - [ - 'id' => 3, - 'path' => '/test/assets-3', - 'description' => 'Hello Order', - ], - ]; - - return $results; - } - - public function getRequiredPermission(): UserPermissions - { - return UserPermissions::DATA_OBJECTS; - } - - public function getName(): string - { - return 'Assets'; - } - - public function getKey(): string - { - return 'assets'; - } - - public function getSortPriority(): int - { - // Give it a high priority so it shows up first in the list. - return 9; - } - - public function getAvailableColumns(): array - { - return [ - new GdprDataColumn( - key: 'id', - label: 'ID' - ), - new GdprDataColumn( - key: 'path', - label: 'Path' - ), - new GdprDataColumn( - key: 'className', - label: 'Class Name' - ), - ]; - } -} diff --git a/src/Gdpr/Provider/DataObjectProvider.php b/src/Gdpr/Provider/DataObjectProvider.php deleted file mode 100644 index 89dfd2e53..000000000 --- a/src/Gdpr/Provider/DataObjectProvider.php +++ /dev/null @@ -1,88 +0,0 @@ - 1, - 'path' => '/test/object-1', - 'className' => 'Customer', - ], - [ - 'id' => 2, - 'path' => '/test/object-2', - 'className' => 'Customer', - ], - [ - 'id' => 3, - 'path' => '/test/object-3', - 'className' => 'Order', - ], - ]; - - return $results; - } - - public function getName(): string - { - return 'Data Objects'; - } - - public function getKey(): string - { - return 'data_objects'; - } - - public function getSortPriority(): int - { - // Give it a high priority so it shows up first in the list. - return 10; - } - - public function getAvailableColumns(): array - { - return [ - new GdprDataColumn( - key: 'id', - label: 'ID' - ), - new GdprDataColumn( - key: 'path', - label: 'Path' - ), - new GdprDataColumn( - key: 'className', - label: 'Class Name' - ), - ]; - } - - public function getRequiredPermission(): UserPermissions - { - return UserPermissions::DATA_OBJECTS; - } -} diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php deleted file mode 100644 index 7b89a9bd1..000000000 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ /dev/null @@ -1,108 +0,0 @@ -id !== null) { - $conditionParts[] = 'id = ?'; - $params[] = $terms->id; - } - if ($terms->firstname !== null) { - $conditionParts[] = 'firstname LIKE ?'; - $params[] = '%' . $terms->firstname . '%'; - } - if ($terms->lastname !== null) { - $conditionParts[] = 'lastname LIKE ?'; - $params[] = '%' . $terms->lastname . '%'; - } - if ($terms->email !== null) { - $conditionParts[] = 'email LIKE ?'; - $params[] = '%' . $terms->email . '%'; - } - } - - // If we have conditions, apply them. - //check the result and test it , pending - if (!empty($conditionParts)) { - $listing->setCondition(implode(' OR ', $conditionParts), $params); - } - - $users = $listing->getUsers(); - - $results = []; - foreach ($users as $user) { - $results[] = [ - 'id' => $user->getId(), - 'name' => $user->getName(), - // 'firstname' => $user->getFirstname(), - // 'lastname' => $user->getLastname(), - // 'email' => $user->getEmail(), - ]; - } - - return $results; - } - - public function getName(): string - { - return 'Pimcore Users'; - } - - public function getKey(): string - { - return 'pimcore_users'; - } - - public function getSortPriority(): int - { - return 5; - } - - public function getRequiredPermission(): UserPermissions - { - return UserPermissions::PIMCORE_USER; - } - - public function getAvailableColumns(): array - { - return [ - new GdprDataColumn('id', 'ID'), - new GdprDataColumn('name', 'Username'), - // new GdprDataColumn('firstname', 'First Name'), - // new GdprDataColumn('lastname', 'Last Name'), - // new GdprDataColumn('email', 'Email'), - ]; - //How to get email firstname lastname - } -} diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 9f7392b6b..5a771c801 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -24,6 +24,9 @@ use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJob; use function count; use function sprintf; diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index 591eb4514..56a68c5a5 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -17,6 +17,10 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataProvider; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; /** * @internal From bfdbf3ef67d61b0d18f617dc87d902083f137add Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:06:57 +0000 Subject: [PATCH 06/34] Apply php-cs-fixer changes --- src/Gdpr/Service/GdprManagerService.php | 6 +++--- src/Gdpr/Service/GdprManagerServiceInterface.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 5a771c801..89fc33a15 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -19,14 +19,14 @@ 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\GdprExportJob; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResult; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\StreamedResponse; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJob; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function count; use function sprintf; diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index 56a68c5a5..c68df1597 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -13,14 +13,14 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Service; +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\GdprExportJobCollection; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; use Symfony\Component\HttpFoundation\StreamedResponse; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; /** * @internal From f481324b3cf8a8c5c64032e836203f570f698d29 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 12 Nov 2025 09:26:00 +0000 Subject: [PATCH 07/34] Addition fixes --- src/Gdpr/Provider/DataProviderInterface.php | 29 ++++++++++++++++++--- src/Gdpr/Service/GdprManagerService.php | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index f0978e7ec..109594108 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -15,13 +15,15 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; +use Symfony\Component\HttpFoundation\StreamedResponse; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Model\Element\AbstractElement; -/** - * @internal - */ + interface DataProviderInterface { /** @@ -29,7 +31,8 @@ interface DataProviderInterface * * @param SearchTerms|null $terms The search values (can be null if none provided) * - * @return array A list of found elements (Objects, Assets, etc.) + + * @return array> */ public function findData(?SearchTerms $terms): array; @@ -67,4 +70,22 @@ public function getAvailableColumns(): array; * @return UserPermissions */ public function getRequiredPermission(): UserPermissions; + + /** + * @param GdprStructuredSearchRequest $request + * + * @return string Job ID + */ + public function startJobExecution(GdprStructuredSearchRequest $request): string; + + /** + * Checks if this provider is responsible for the given job. + */ + public function ownsJob(int $jobRunId): bool; + + /** + * @throws NotFoundException + * @throws ForbiddenException + */ + public function getExportFile(int $jobRunId): StreamedResponse; } diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 89fc33a15..fe83c08a2 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -117,7 +117,7 @@ public function getExportFile(int $jobId): StreamedResponse $permission = $provider->getRequiredPermission(); if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { - throw new ForbiddenException("Not allowed for provider: $provider"); + continue; } if ($provider->ownsJob($jobId)) { From 8ab68103e61096c583f53837e5664dc06b234fb5 Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:26:58 +0000 Subject: [PATCH 08/34] Apply php-cs-fixer changes --- src/Gdpr/Provider/DataProviderInterface.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index 109594108..5c042ebb8 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -13,16 +13,13 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; -use Symfony\Component\HttpFoundation\StreamedResponse; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; - -use Pimcore\Model\Element\AbstractElement; - +use Symfony\Component\HttpFoundation\StreamedResponse; interface DataProviderInterface { From c4bd9ea89c11b4721606e6a6c0801f6c8477a066 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 12 Nov 2025 09:38:24 +0000 Subject: [PATCH 09/34] Addition fixes --- src/Gdpr/Service/GdprManagerService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index fe83c08a2..5dbde6d20 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -19,14 +19,14 @@ 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\GdprExportJob; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResult; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; -use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJob; use function count; use function sprintf; @@ -116,7 +116,7 @@ public function getExportFile(int $jobId): StreamedResponse foreach ($providers as $provider) { $permission = $provider->getRequiredPermission(); - if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { + if (!$currentUser->isAllowed($permission->value)) { continue; } From 73f9b14012a692a7a3b3f113ceae4ebe8c4b2153 Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:42:27 +0000 Subject: [PATCH 10/34] Apply php-cs-fixer changes --- src/Gdpr/Service/GdprManagerService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 5dbde6d20..d8f5b4747 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -19,14 +19,14 @@ 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\GdprExportJob; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResult; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\StreamedResponse; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJob; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function count; use function sprintf; From 8654501983195e8b64c435e8bbc665ed98c01f57 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 12 Nov 2025 09:54:12 +0000 Subject: [PATCH 11/34] Addition fixes --- src/Gdpr/Service/GdprManagerService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index d8f5b4747..43742f15b 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -60,7 +60,7 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo $permission = $provider->getRequiredPermission(); // Check if the current user has the required permission to access the provider - if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { + if (!$currentUser->isAllowed($permission->value)) { throw new ForbiddenException( sprintf( 'Not allowed to access the targeted provider "%s". Required permission: "%s"', @@ -92,7 +92,7 @@ public function startBackgroundExport(GdprStructuredSearchRequest $request): Gdp $provider = $this->loader->resolve($providerKey); $permission = $provider->getRequiredPermission(); - if ($currentUser === null || !$currentUser->isAllowed($permission->value)) { + if (!$currentUser->isAllowed($permission->value)) { throw new ForbiddenException("Not allowed for provider: $providerKey"); } From 8eefb6aab1cdf495a35a7cfee3a477258a77a759 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Thu, 13 Nov 2025 23:16:47 +0000 Subject: [PATCH 12/34] pimcore user and finalize manager service --- config/gdpr.yaml | 2 + doc/10_Extending_Studio/11_Gdpr.md | 123 ++++++++------ .../Attribute/Request/GdprRequestBody.php | 23 +-- src/Gdpr/Controller/DownloadController.php | 68 -------- src/Gdpr/Controller/ExportController.php | 59 +++---- .../SearchDataProviderController.php | 22 +-- .../GdprStructuredSearchRequest.php | 6 +- src/Gdpr/Provider/DataProviderInterface.php | 19 +-- src/Gdpr/Provider/PimcoreUserProvider.php | 154 ++++++++++++++++++ src/Gdpr/Schema/GdprExportJob.php | 26 --- src/Gdpr/Schema/GdprExportJobCollection.php | 63 ------- src/Gdpr/Schema/GdprSearchResultProperty.php | 6 +- src/Gdpr/Service/GdprManagerService.php | 68 ++++---- .../Service/GdprManagerServiceInterface.php | 9 +- 14 files changed, 325 insertions(+), 323 deletions(-) delete mode 100644 src/Gdpr/Controller/DownloadController.php create mode 100644 src/Gdpr/Provider/PimcoreUserProvider.php delete mode 100644 src/Gdpr/Schema/GdprExportJob.php delete mode 100644 src/Gdpr/Schema/GdprExportJobCollection.php diff --git a/config/gdpr.yaml b/config/gdpr.yaml index 487f764a0..73f760b63 100644 --- a/config/gdpr.yaml +++ b/config/gdpr.yaml @@ -20,3 +20,5 @@ services: class: Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\Loader\TaggedIteratorDataProviderLoader # --- GDPR Providers --- + Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\PimcoreUserProvider: + tags: ["pimcore.studio_backend.gdpr_data_provider"] diff --git a/doc/10_Extending_Studio/11_Gdpr.md b/doc/10_Extending_Studio/11_Gdpr.md index 8e0f2ce32..4e16b16b6 100644 --- a/doc/10_Extending_Studio/11_Gdpr.md +++ b/doc/10_Extending_Studio/11_Gdpr.md @@ -2,7 +2,9 @@ The GDPR Data Provider system provides a centralized interface to find and export personal data from any part of your Pimcore application. You can add new data sources (like Data Objects, Assets, Users, or any custom entity) by creating your own provider. -New providers are created by implementing the `Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface` and tagging your class as a service with `pimcore.studio_backend.gdpr_data_provider`. +New providers are created by implementing the `Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface` and tagging your class as a service with `pimcore.studio_backend.gdpr_data_provider` in gdpr.yaml. + +If you're using the default service configuration, simply placing your class in the `src/Gdpr/Provider/` directory is all you need for it to be registered. ## How does it work @@ -14,25 +16,48 @@ The `GdprManagerService` acts as the central coordinator for all registered prov 2. When a user performs a search, the manager first checks `getRequiredPermission()` to see if the current user is allowed to use your provider. 3. If permitted, the manager calls your provider's `findData()` method, passing the user's search terms. The results are then displayed in the grid. -### For Exporting +### For Exporting (Direct Download) + +The export process is a "direct download" flow. -The export process is a two-step flow handled by your provider: +1. **Request:** The user makes a `GET` request to the export endpoint, specifying the item `id` in the URL and the `providerKey` as a query parameter. + `GET /pimcore-studio/api/gdpr/export-data/1?providerKey=pimcore_users` +2. **Logic:** The `GdprManagerService` resolves the one provider specified (`pimcore_users`). +3. **Permission Check:** It calls your provider's `getRequiredPermission()` to check if the user is allowed. +4. **Data Retrieval:** If permitted, the manager calls your provider's `getSingleItemForDownload(1)` method. +5. **Response:** Your provider returns the raw data (like a DataObject or an array). The `GdprManagerService` automatically serializes this data into a downloadable JSON file, a "Save As..." dialog in the user's browser. -1. **Start Job:** The manager calls `startJobExecution()`. Your provider is responsible for starting a background process (e.g., a Symfony Messenger job) and immediately returning a **unique Job ID** (as a string). -2. **Get File:** When the user clicks the download button for that job, the manager loops through all providers and calls `ownsJob($jobId)` on each one to find the correct owner. - - Once the owner is found, the manager calls `getExportFile($jobId)`. - - Your provider is then responsible for finding the completed job's file, checking specific permissions (e.g., "does this user own this job?"), and streaming the file back as a `StreamedResponse`. +--- ## Example Data Provider -Here is a example of a provider that searches for **Customer** data objects. +Here is an example of a provider that supports both searching and direct exporting for **Customer** data objects. ```php - +value)) { + return []; + } + + $listing = new DataObject\Customer\Listing(); + $listing->setCondition('email LIKE ?', ['%' . $terms->value . '%']); + $listing->load(); + + $results = []; + foreach ($listing as $customer) { + // The keys here MUST match the keys in getAvailableColumns() + $results[] = [ + 'id' => $customer->getId(), + 'email' => $customer->getEmail(), + 'path' => $customer->getFullPath(), + ]; + } + + return $results; } /** - * Starts the background export job. + * Fetches a single item's data for export. + * The returned data (array or object) will be serialized by the manager. * - * @return string The unique Job ID - */ - public function startJobExecution(GdprStructuredSearchRequest $request): string - { - $jobId = '1';//Create a job id - - return $jobId; - } - - /** - * A quick check to see if this provider is responsible for a job. - * This should be a fast check (e.g., checking a job type or ID prefix). + * @param int $id The ID of the item to fetch + * @return array|object The data to be serialized + * + * @throws NotFoundException + * @throws ForbiddenException */ - public function ownsJob(int $jobRunId): bool + public function getSingleItemForDownload(int $id): array|object { - // return $this->jobService->doesJobExist? + // 1. Find the item + $customer = Customer::getById($id); - } + if ($customer === null) { + throw new NotFoundException('Customer', $id); + } - /** - * Finds the completed job file and streams it. - * This is only called after ownsJob() returns true. - */ - public function getExportFile(int $jobRunId): StreamedResponse - { - // 1. Find the job in your storage - - // 2. If the job doesn't exist (or isn't yours), you MUST throw this - // if ($job === null) { - // throw new NotFoundException('Export job not found'); + // 2. (Optional) Check for specific permissions + // if ($this->securityService->isAllowedToSee($customer) === false) { + // throw new ForbiddenException('You are not allowed to export this item.'); // } + // 3. Return the data. + // The GdprManagerService will receive this and must serialize it. + return $customer; - // 3. Find the file on disk (or stream from S3, etc.) - // $filePath = $this->fileService->getFilePath($job->getFileName()); - // if (!$this->fileService->exists($filePath)) { - // throw new NotFoundException('Export file is missing or not yet generated.'); - // } - - //Return response } } + ``` diff --git a/src/Gdpr/Attribute/Request/GdprRequestBody.php b/src/Gdpr/Attribute/Request/GdprRequestBody.php index 422ab99b6..f804aab37 100644 --- a/src/Gdpr/Attribute/Request/GdprRequestBody.php +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -14,14 +14,13 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request; use Attribute; -use OpenApi\Attributes\Items; use OpenApi\Attributes\JsonContent; use OpenApi\Attributes\Property; use OpenApi\Attributes\RequestBody; /** * @internal - */ +*/ #[Attribute(Attribute::TARGET_METHOD)] final class GdprRequestBody extends RequestBody { @@ -30,23 +29,13 @@ public function __construct() parent::__construct( required: true, content: new JsonContent( - required: ['providers'], + required: ['providerName'], properties: [ new Property( - property: 'providers', - description: 'A list of provider keys to search (e.g., data_objects, emails, etc.)', - type: 'array', - items: new Items( - type: 'string', - example: 'data_objects' - ) - ), - new Property( - property: 'searchTerms', - description: 'The object containing the search values. Can also be empty.', - ref: SearchTerms::class, - type: 'object', - nullable: true + property: 'providerName', + description: 'The key of the single provider to search (e.g., pimcore_user)', + type: 'string', + example: 'pimcore_user' ), ], type: 'object', diff --git a/src/Gdpr/Controller/DownloadController.php b/src/Gdpr/Controller/DownloadController.php deleted file mode 100644 index 2b541bbc3..000000000 --- a/src/Gdpr/Controller/DownloadController.php +++ /dev/null @@ -1,68 +0,0 @@ -value)] - #[Get( - path: self::PREFIX . '/gdpr/export/download/{jobId}', - operationId: 'download_gdpr_export', - summary: 'download_gdpr_export_summary', - description: 'download_gdpr_export_description', - tags: [Tags::Export->name] - )] - #[SuccessResponse( - description: 'The exported file (CSV or XLSX)' - )] - #[DefaultResponses([ - HttpResponseCodes::UNAUTHORIZED, - HttpResponseCodes::FORBIDDEN, - HttpResponseCodes::NOT_FOUND, - ])] - public function download(int $jobId): StreamedResponse - { - return $this->gdprManagerService->getExportFile( - $jobId - ); - } -} diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index b44903a5d..5d49fee31 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -14,22 +14,22 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Controller; use OpenApi\Attributes\Get; -use OpenApi\Attributes\JsonContent; -use OpenApi\Attributes\Property; +use OpenApi\Attributes\Parameter; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\GdprRequestBody; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; 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\OpenApi\Attribute\Response\Content\MediaType; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Header\ContentDisposition; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\HttpFoundation\JsonResponse; /** * @internal @@ -44,28 +44,32 @@ public function __construct( } #[Route( - '/gdpr/export/start', + '/gdpr/export-data/{id}', name: 'pimcore_studio_api_gdpr_export_start', - methods: ['POST'] + methods: ['GET'], + requirements: ['id' => '\d+'] )] #[IsGranted(UserPermissions::GDPR->value)] - #[GET(summary: 'Start background export job', tags: ['GDPR'])] - #[GET( - path: self::PREFIX . '/gdpr/export/start', - operationId: 'start_gdpr_export', - summary: 'start_gdpr_export_summary', - description: 'start_gdpr_export_description', - tags: [Tags::Export->name] + #[Get( + path: self::PREFIX . '/gdpr/export-data/{id}', + operationId: 'gdpr_export', + summary: 'gdpr_export_summary', + description: 'gdpr_export_description', + tags: [Tags::Export->name], + parameters: [ + new Parameter( + name: 'providerKey', + in: 'query', + required: true, + description: 'The key of the single provider to export', + example: 'pimcore_user' + ) + ] )] - #[GdprRequestBody] #[SuccessResponse( - description: 'Job accepted and started', - content: new JsonContent( - properties: [ - new Property(property: 'jobId', type: 'string', example: '123e4567...'), - new Property(property: 'status', type: 'string', example: 'started'), - ] - ) + description: 'gdpr_export_success_response', + content: new MediaType('application/json'), + headers: [new ContentDisposition('inline')] )] #[DefaultResponses([ HttpResponseCodes::UNAUTHORIZED, @@ -73,10 +77,9 @@ public function __construct( HttpResponseCodes::BAD_REQUEST, ])] public function startExport( - #[MapRequestPayload] GdprStructuredSearchRequest $request - ): JsonResponse { - $jobId = $this->gdprManagerService->startBackgroundExport($request); - - return new JsonResponse(['jobId' => $jobId, 'status' => 'started'], 202); + int $id, + #[MapQueryParameter] string $providerKey + ): StreamedResponse { + return $this->gdprManagerService->getExportDataAsJson($id, $providerKey); } } diff --git a/src/Gdpr/Controller/SearchDataProviderController.php b/src/Gdpr/Controller/SearchDataProviderController.php index 637be88a8..f71af1be3 100644 --- a/src/Gdpr/Controller/SearchDataProviderController.php +++ b/src/Gdpr/Controller/SearchDataProviderController.php @@ -17,20 +17,20 @@ 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\Gdpr\Schema\GdprSearchResultProperty; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; -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\OpenApi\Attribute\Response\Content\CollectionJson; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultProperty; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; /** * @internal @@ -46,12 +46,12 @@ public function __construct( /** * Handles GDPR data search requests across different providers. - * + * * @throws NotFoundException - */ + */ #[Route( - '/gdpr/search', - name: 'pimcore_studio_api_gdpr_search', + '/gdpr/search', + name: 'pimcore_studio_api_gdpr_search', methods: ['POST'])] #[IsGranted(UserPermissions::GDPR->value)] #[POST( @@ -65,8 +65,8 @@ public function __construct( #[SuccessResponse( description: 'gdpr_search_data_success_response', content: new CollectionJson( - collection: new GdprSearchResultProperty() - ) + collection: new GdprSearchResultProperty() + ) )] #[DefaultResponses([ diff --git a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php index bf2848041..96f2eef20 100644 --- a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php +++ b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter; @@ -36,4 +36,4 @@ public function __construct( public ?SearchTerms $searchTerms = null ) { } -} +} \ No newline at end of file diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index 5c042ebb8..3ff829983 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -16,10 +16,8 @@ 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\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Symfony\Component\HttpFoundation\StreamedResponse; interface DataProviderInterface { @@ -69,20 +67,13 @@ public function getAvailableColumns(): array; public function getRequiredPermission(): UserPermissions; /** - * @param GdprStructuredSearchRequest $request + * Fetches a single item's data for export. + * The returned data will be serialized as JSON. + * @param int $id + * @return array|object * - * @return string Job ID - */ - public function startJobExecution(GdprStructuredSearchRequest $request): string; - - /** - * Checks if this provider is responsible for the given job. - */ - public function ownsJob(int $jobRunId): bool; - - /** * @throws NotFoundException * @throws ForbiddenException */ - public function getExportFile(int $jobRunId): StreamedResponse; + public function getSingleItemForDownload(int $id): array|object; } diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php new file mode 100644 index 000000000..b0b4c26a2 --- /dev/null +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -0,0 +1,154 @@ +id !== null) { + $conditionParts[] = 'id = ?'; + $params[] = $terms->id; + } + if ($terms->firstname !== null) { + $conditionParts[] = 'firstname LIKE ?'; + $params[] = '%' . $terms->firstname . '%'; + } + if ($terms->lastname !== null) { + $conditionParts[] = 'lastname LIKE ?'; + $params[] = '%' . $terms->lastname . '%'; + } + if ($terms->email !== null) { + $conditionParts[] = 'email LIKE ?'; + $params[] = '%' . $terms->email . '%'; + } + } + + // If we have conditions, apply them. + if (count($conditionParts) > 1) { // We have more than just the base "type = 'user'" + $listing->setCondition(implode(' OR ', $conditionParts), $params); + } else { + // Only the "type = 'user'" condition exists + $listing->setCondition($conditionParts[0]); + } + + $users = $listing->getUsers(); + + $results = []; + foreach ($users as $user) { + $results[] = [ + 'id' => $user->getId(), + 'name' => $user->getName(), + 'firstname' => $user->getFirstname(), + 'lastname' => $user->getLastname(), + 'email' => $user->getEmail(), + ]; + } + + return $results; + } + + /** + * Get all relevant GDPR data for a single user. + * Required by the manager service for single-item export. + * + * @param int $id + * @return array + * @throws NotFoundException + */ + public function getSingleItemForDownload(int $id): array + { + $listing = new Listing(); + $listing->setCondition('id = ? AND `type` = ?', [$id, 'user']); + $listing->setLimit(1); + + $users = $listing->getUsers(); + + if (empty($users)) { + throw new NotFoundException('Pimcore User', $id); + } + + $user = $users[0]; + + return [ + 'id' => $user->getId(), + 'parentId' => $user->getParentId(), + 'type' => $user->getType(), + 'name' => $user->getName(), + 'firstname' => $user->getFirstname(), + 'lastname' => $user->getLastname(), + 'email' => $user->getEmail(), + 'active' => $user->getActive(), + 'admin' => $user->isAdmin(), + 'language' => $user->getLanguage(), + ]; + } + + public function getName(): string + { + return 'Pimcore Users'; + } + + public function getKey(): string + { + return 'pimcore_users'; + } + + public function getSortPriority(): int + { + return 5; + } + + public function getRequiredPermission(): UserPermissions + { + return UserPermissions::PIMCORE_USER; + } + + public function getAvailableColumns(): array + { + return [ + new GdprDataColumn('id', 'ID'), + new GdprDataColumn('parentId', 'Parent ID'), + new GdprDataColumn('type', 'Type'), + new GdprDataColumn('name', 'Username'), + new GdprDataColumn('firstname', 'First Name'), + new GdprDataColumn('lastname', 'Last Name'), + new GdprDataColumn('email', 'Email'), + new GdprDataColumn('active', 'Active'), + new GdprDataColumn('admin', 'Admin'), + new GdprDataColumn('language', 'Language'), + ]; + } +} \ No newline at end of file diff --git a/src/Gdpr/Schema/GdprExportJob.php b/src/Gdpr/Schema/GdprExportJob.php deleted file mode 100644 index 70e85ebd3..000000000 --- a/src/Gdpr/Schema/GdprExportJob.php +++ /dev/null @@ -1,26 +0,0 @@ - $items - */ - public function __construct( - #[Property( - description: 'List of started export jobs', - type: 'array', - items: new Items(ref: GdprExportJob::class) - )] - - private readonly array $items, - ) { - } - - /** - * @return array - */ - public function getItems(): array - { - return $this->items; - } -} diff --git a/src/Gdpr/Schema/GdprSearchResultProperty.php b/src/Gdpr/Schema/GdprSearchResultProperty.php index e0ec41269..3dd922bcf 100644 --- a/src/Gdpr/Schema/GdprSearchResultProperty.php +++ b/src/Gdpr/Schema/GdprSearchResultProperty.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; @@ -46,4 +46,4 @@ public function __construct() ) ); } -} +} \ No newline at end of file diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 43742f15b..30de8fbea 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -19,14 +19,15 @@ 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\GdprExportJob; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprExportJobCollection; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResult; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; -use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +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\StreamedResponse; use function count; use function sprintf; @@ -35,6 +36,8 @@ */ final readonly class GdprManagerService implements GdprManagerServiceInterface { + use StreamedResponseTrait; + public function __construct( private DataProviderLoaderInterface $loader, private EventDispatcherInterface $eventDispatcher, @@ -83,50 +86,45 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo return new GdprSearchResultCollection($allResults); } - public function startBackgroundExport(GdprStructuredSearchRequest $request): GdprExportJobCollection - { - $jobs = []; - $currentUser = $this->securityService->getCurrentUser(); - - foreach ($request->providers as $providerKey) { - $provider = $this->loader->resolve($providerKey); - - $permission = $provider->getRequiredPermission(); - if (!$currentUser->isAllowed($permission->value)) { - throw new ForbiddenException("Not allowed for provider: $providerKey"); - } - - $jobId = $provider->startJobExecution($request); - $jobs[] = new GdprExportJob($providerKey, $jobId); - } - - return new GdprExportJobCollection($jobs); - } - /** * @throws ForbiddenException * @throws NotFoundException */ - public function getExportFile(int $jobId): StreamedResponse + public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse { $currentUser = $this->securityService->getCurrentUser(); - $providers = $this->loader->getDataProviders(); + $provider = $this->loader->resolve($providerKey); - foreach ($providers as $provider) { + $permission = $provider->getRequiredPermission(); + if (!$currentUser->isAllowed($permission->value)) { + throw new ForbiddenException("Not allowed for provider: {$provider->getKey()}"); + } - $permission = $provider->getRequiredPermission(); - if (!$currentUser->isAllowed($permission->value)) { - continue; - } + $data = $provider->getSingleItemForDownload($id);//id is a single item of a particular provider - if ($provider->ownsJob($jobId)) { + $jsonData = json_encode($data, JSON_PRETTY_PRINT); - return $provider->getExportFile($jobId); - } - } + $filename = sprintf('gdpr-export-%s-%d.json', $providerKey, $id); + $fileSize = strlen($jsonData); + + $headers = $this->getResponseHeaders( + mimeType: 'application/json', + fileSize: $fileSize, + filename: $filename, + contentDisposition: HttpResponseHeaders::ATTACHMENT_TYPE->value, // 'attachment' + additionalHeaders: [] + ); + + $response = new StreamedResponse( + function () use ($jsonData) { + echo $jsonData; + }, + HttpResponseCodes::SUCCESS->value, + $headers + ); - throw new NotFoundException('Export job with ID %d not found or access denied.', $jobId); + return $response; } /** diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index c68df1597..023d58335 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -17,11 +17,9 @@ 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\GdprExportJobCollection; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Symfony\Component\HttpFoundation\StreamedResponse; - /** * @internal */ @@ -41,14 +39,9 @@ public function getAvailableProviders(): Collection; */ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection; - /** - * @throws ForbiddenException - */ - public function startBackgroundExport(GdprStructuredSearchRequest $request): GdprExportJobCollection; - /** * @throws ForbiddenException * @throws NotFoundException */ - public function getExportFile(int $jobRunId): StreamedResponse; + public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse; } From 0e2edf3e1cf743b44370bec240a61a2bc213e344 Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:18:26 +0000 Subject: [PATCH 13/34] Apply php-cs-fixer changes --- .../Attribute/Request/GdprRequestBody.php | 2 +- src/Gdpr/Controller/ExportController.php | 13 +++++------ .../SearchDataProviderController.php | 22 +++++++++---------- .../GdprStructuredSearchRequest.php | 6 ++--- src/Gdpr/Provider/DataProviderInterface.php | 2 ++ src/Gdpr/Provider/PimcoreUserProvider.php | 12 +++++----- src/Gdpr/Schema/GdprSearchResultProperty.php | 6 ++--- src/Gdpr/Service/GdprManagerService.php | 11 +++++----- .../Service/GdprManagerServiceInterface.php | 1 + 9 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/Gdpr/Attribute/Request/GdprRequestBody.php b/src/Gdpr/Attribute/Request/GdprRequestBody.php index f804aab37..779459a68 100644 --- a/src/Gdpr/Attribute/Request/GdprRequestBody.php +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -20,7 +20,7 @@ /** * @internal -*/ + */ #[Attribute(Attribute::TARGET_METHOD)] final class GdprRequestBody extends RequestBody { diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index 5d49fee31..b3bdc25f6 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -14,22 +14,21 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Controller; use OpenApi\Attributes\Get; -use OpenApi\Attributes\Parameter; +use OpenApi\Attributes\Parameter; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; +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\HttpResponseCodes; -use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType; -use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Header\ContentDisposition; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\HttpFoundation\JsonResponse; /** * @internal @@ -63,7 +62,7 @@ public function __construct( required: true, description: 'The key of the single provider to export', example: 'pimcore_user' - ) + ), ] )] #[SuccessResponse( @@ -78,7 +77,7 @@ public function __construct( ])] public function startExport( int $id, - #[MapQueryParameter] string $providerKey + #[MapQueryParameter] string $providerKey ): StreamedResponse { return $this->gdprManagerService->getExportDataAsJson($id, $providerKey); } diff --git a/src/Gdpr/Controller/SearchDataProviderController.php b/src/Gdpr/Controller/SearchDataProviderController.php index f71af1be3..637be88a8 100644 --- a/src/Gdpr/Controller/SearchDataProviderController.php +++ b/src/Gdpr/Controller/SearchDataProviderController.php @@ -17,20 +17,20 @@ 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\Gdpr\Schema\GdprSearchResultProperty; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; +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\OpenApi\Attribute\Response\Content\CollectionJson; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultProperty; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; -use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; /** * @internal @@ -46,12 +46,12 @@ public function __construct( /** * Handles GDPR data search requests across different providers. - * + * * @throws NotFoundException - */ + */ #[Route( - '/gdpr/search', - name: 'pimcore_studio_api_gdpr_search', + '/gdpr/search', + name: 'pimcore_studio_api_gdpr_search', methods: ['POST'])] #[IsGranted(UserPermissions::GDPR->value)] #[POST( @@ -65,8 +65,8 @@ public function __construct( #[SuccessResponse( description: 'gdpr_search_data_success_response', content: new CollectionJson( - collection: new GdprSearchResultProperty() - ) + collection: new GdprSearchResultProperty() + ) )] #[DefaultResponses([ diff --git a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php index 96f2eef20..bf2848041 100644 --- a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php +++ b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter; @@ -36,4 +36,4 @@ public function __construct( public ?SearchTerms $searchTerms = null ) { } -} \ No newline at end of file +} diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index 3ff829983..9d7a158d1 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -69,7 +69,9 @@ public function getRequiredPermission(): UserPermissions; /** * Fetches a single item's data for export. * The returned data will be serialized as JSON. + * * @param int $id + * * @return array|object * * @throws NotFoundException diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index b0b4c26a2..5512d52a8 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -1,14 +1,13 @@ + * * @throws NotFoundException */ public function getSingleItemForDownload(int $id): array @@ -151,4 +151,4 @@ public function getAvailableColumns(): array new GdprDataColumn('language', 'Language'), ]; } -} \ No newline at end of file +} diff --git a/src/Gdpr/Schema/GdprSearchResultProperty.php b/src/Gdpr/Schema/GdprSearchResultProperty.php index 3dd922bcf..e0ec41269 100644 --- a/src/Gdpr/Schema/GdprSearchResultProperty.php +++ b/src/Gdpr/Schema/GdprSearchResultProperty.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; @@ -46,4 +46,4 @@ public function __construct() ) ); } -} \ No newline at end of file +} diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 30de8fbea..0c5496d18 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -23,13 +23,14 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseHeaders; -use Pimcore\Bundle\StudioBackendBundle\Util\Trait\StreamedResponseTrait; +use Pimcore\Bundle\StudioBackendBundle\Util\Trait\StreamedResponseTrait; use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use function count; use function sprintf; +use function strlen; /** * @internal @@ -90,7 +91,7 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo * @throws ForbiddenException * @throws NotFoundException */ - public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse + public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse { $currentUser = $this->securityService->getCurrentUser(); @@ -101,7 +102,7 @@ public function getExportDataAsJson(int $id, string $providerKey): StreamedRespo throw new ForbiddenException("Not allowed for provider: {$provider->getKey()}"); } - $data = $provider->getSingleItemForDownload($id);//id is a single item of a particular provider + $data = $provider->getSingleItemForDownload($id); //id is a single item of a particular provider $jsonData = json_encode($data, JSON_PRETTY_PRINT); @@ -121,7 +122,7 @@ function () use ($jsonData) { echo $jsonData; }, HttpResponseCodes::SUCCESS->value, - $headers + $headers ); return $response; diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index 023d58335..ad58e322d 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -20,6 +20,7 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; use Pimcore\Bundle\StudioBackendBundle\Response\Collection; use Symfony\Component\HttpFoundation\StreamedResponse; + /** * @internal */ From 51322c1e0686174f64df94ffa1a77f3d86b25c2e Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Tue, 18 Nov 2025 09:22:15 +0000 Subject: [PATCH 14/34] Additional changes and improvements --- doc/05_Additional_Custom_Attributes.md | 2 + doc/10_Extending_Studio/11_Gdpr.md | 156 +++++------------- src/Gdpr/Controller/ExportController.php | 11 +- .../SearchDataProviderController.php | 1 + .../Event/PreResponse/GdprExportDataEvent.php | 39 +++++ .../PreResponse/GdprSearchResultEvent.php | 40 +++++ src/Gdpr/Provider/DataProviderInterface.php | 43 +---- src/Gdpr/Provider/PimcoreUserProvider.php | 67 ++++---- src/Gdpr/Schema/GdprDataRow.php | 43 +++++ src/Gdpr/Service/GdprManagerService.php | 131 +++++++++++---- .../Service/GdprManagerServiceInterface.php | 2 - translations/studio_api_docs.en.yaml | 7 + 12 files changed, 308 insertions(+), 234 deletions(-) create mode 100644 src/Gdpr/Event/PreResponse/GdprExportDataEvent.php create mode 100644 src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php create mode 100644 src/Gdpr/Schema/GdprDataRow.php diff --git a/doc/05_Additional_Custom_Attributes.md b/doc/05_Additional_Custom_Attributes.md index 364fc1276..9910d0819 100644 --- a/doc/05_Additional_Custom_Attributes.md +++ b/doc/05_Additional_Custom_Attributes.md @@ -159,5 +159,7 @@ final class AssetEvent extends AbstractPreResponseEvent - `pre_response.notification_recipient` - `pre_response.php_code_transformer` - `pre_response.data_provider` +- `pre_response.gdpr_search_result` +- `pre_response.gdpr_export_data` - `pre_response.element.usage.item` - `pre_response.element.usage` diff --git a/doc/10_Extending_Studio/11_Gdpr.md b/doc/10_Extending_Studio/11_Gdpr.md index 4e16b16b6..4397a9bc8 100644 --- a/doc/10_Extending_Studio/11_Gdpr.md +++ b/doc/10_Extending_Studio/11_Gdpr.md @@ -2,170 +2,90 @@ The GDPR Data Provider system provides a centralized interface to find and export personal data from any part of your Pimcore application. You can add new data sources (like Data Objects, Assets, Users, or any custom entity) by creating your own provider. -New providers are created by implementing the `Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface` and tagging your class as a service with `pimcore.studio_backend.gdpr_data_provider` in gdpr.yaml. - -If you're using the default service configuration, simply placing your class in the `src/Gdpr/Provider/` directory is all you need for it to be registered. +New providers can be created by implementing the `Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface` and tagging your class as a service with `pimcore.studio_backend.gdpr_data_provider`. ## How does it work -The `GdprManagerService` acts as the central coordinator for all registered providers. It automatically discovers your tagged service. +As a developer, you only need to register it with the `pimcore.studio_backend.gdpr_data_provider` tag and implement the `DataProviderInterface`. The Pimcore system will automatically find your provider and use in Searching and Exporting. + +### For Searching + +This flow happens when a user opens the GDPR Data Extractor page and clicks "Search". + +1. **To build the page:** The system first calls these methods on your provider to build the UI: -### 🔎 For Searching + - `getName()`: To get the human-friendly name for the provider list. + - `getKey()`: To get the unique ID. + - `getSortPriority()`: To decide where to place your provider in the list. + - `getAvailableColumns()`: To build the columns for the search results grid. + - add more -1. The manager loads all tagged providers to build the search interface. It calls your provider's `getName()`, `getKey()`, `getSortPriority()`, and `getAvailableColumns()` methods. -2. When a user performs a search, the manager first checks `getRequiredPermission()` to see if the current user is allowed to use your provider. -3. If permitted, the manager calls your provider's `findData()` method, passing the user's search terms. The results are then displayed in the grid. +2. **When the user clicks "Search":** + - The system first calls your `getRequiredPermissions()` method to check if the current user is allowed to use your provider. + - If permission is granted, the system calls your `findData()` method, passing the user's search terms. + - The result you return from `findData()` is then displayed directly in the results grid. ### For Exporting (Direct Download) -The export process is a "direct download" flow. +This flow happens when a user has already searched and clicks the "Export" button on a single item in your results grid. -1. **Request:** The user makes a `GET` request to the export endpoint, specifying the item `id` in the URL and the `providerKey` as a query parameter. - `GET /pimcore-studio/api/gdpr/export-data/1?providerKey=pimcore_users` -2. **Logic:** The `GdprManagerService` resolves the one provider specified (`pimcore_users`). -3. **Permission Check:** It calls your provider's `getRequiredPermission()` to check if the user is allowed. -4. **Data Retrieval:** If permitted, the manager calls your provider's `getSingleItemForDownload(1)` method. -5. **Response:** Your provider returns the raw data (like a DataObject or an array). The `GdprManagerService` automatically serializes this data into a downloadable JSON file, a "Save As..." dialog in the user's browser. +1. **When the user clicks "Export" on an item:** + - The system again checks your `getRequiredPermission()` method. + - If permission is granted, the system calls your `getSingleItemForDownload(int $id)` method, passing the ID of the item the user wants to export. + - The `array` or `object` you return from `getSingleItemForDownload()` is then automatically converted by the system into a **downloadable file** for the user. --- ## Example Data Provider -Here is an example of a provider that supports both searching and direct exporting for **Customer** data objects. +Example below shows some of the important functions with their implementations ```php -value } - /** - * Defines the columns for the search result grid. - * The 'key' must match the key in the array returned by findData(). - * - * @return GdprDataColumn[] - */ public function getAvailableColumns(): array { return [ - new GdprDataColumn('id', 'ID'), - new GdprDataColumn('email', 'Email Address'), - new GdprDataColumn('path', 'Full Path'), + new GdprDataColumn('column1', 'Column 1 Value'), + new GdprDataColumn('column2', 'Column 2 Value'), ]; } - /** - * The core search logic. - * - * @return array> - */ public function findData(?SearchTerms $terms): array { - // Note: $terms can be null - if ($terms === null || empty($terms->value)) { - return []; - } - - $listing = new DataObject\Customer\Listing(); - $listing->setCondition('email LIKE ?', ['%' . $terms->value . '%']); - $listing->load(); - - $results = []; - foreach ($listing as $customer) { - // The keys here MUST match the keys in getAvailableColumns() - $results[] = [ - 'id' => $customer->getId(), - 'email' => $customer->getEmail(), - 'path' => $customer->getFullPath(), - ]; - } - - return $results; + //Find user data using input $terms + + //return $results; } - /** - * Fetches a single item's data for export. - * The returned data (array or object) will be serialized by the manager. - * - * @param int $id The ID of the item to fetch - * @return array|object The data to be serialized - * - * @throws NotFoundException - * @throws ForbiddenException - */ public function getSingleItemForDownload(int $id): array|object { - // 1. Find the item - $customer = Customer::getById($id); - - if ($customer === null) { - throw new NotFoundException('Customer', $id); - } - - // 2. (Optional) Check for specific permissions - // if ($this->securityService->isAllowedToSee($customer) === false) { - // throw new ForbiddenException('You are not allowed to export this item.'); - // } - - // 3. Return the data. - // The GdprManagerService will receive this and must serialize it. - return $customer; - + // return single Item of a Data Provider } } diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index b3bdc25f6..7dde9e5ad 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -15,6 +15,7 @@ use OpenApi\Attributes\Get; use OpenApi\Attributes\Parameter; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Query\TextFieldParameter; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType; @@ -54,15 +55,14 @@ public function __construct( operationId: 'gdpr_export', summary: 'gdpr_export_summary', description: 'gdpr_export_description', - tags: [Tags::Export->name], + tags: [Tags::Export->value], parameters: [ - new Parameter( + new TextFieldParameter( name: 'providerKey', - in: 'query', - required: true, description: 'The key of the single provider to export', + required: true, example: 'pimcore_user' - ), + ) ] )] #[SuccessResponse( @@ -74,6 +74,7 @@ public function __construct( HttpResponseCodes::UNAUTHORIZED, HttpResponseCodes::FORBIDDEN, HttpResponseCodes::BAD_REQUEST, + HttpResponseCodes::NOT_FOUND, ])] public function startExport( int $id, diff --git a/src/Gdpr/Controller/SearchDataProviderController.php b/src/Gdpr/Controller/SearchDataProviderController.php index 637be88a8..7330014da 100644 --- a/src/Gdpr/Controller/SearchDataProviderController.php +++ b/src/Gdpr/Controller/SearchDataProviderController.php @@ -74,6 +74,7 @@ public function __construct( HttpResponseCodes::FORBIDDEN, HttpResponseCodes::NOT_FOUND, HttpResponseCodes::BAD_REQUEST, + HttpResponseCodes::UNPROCESSABLE_CONTENT, ])] public function searchData( #[MapRequestPayload] GdprStructuredSearchRequest $request diff --git a/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php b/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php new file mode 100644 index 000000000..09ee63967 --- /dev/null +++ b/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php @@ -0,0 +1,39 @@ +data; + } + + public function setData(array|object $data): void + { + $this->data = $data; + } +} \ No newline at end of file diff --git a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php new file mode 100644 index 000000000..a05a4dac5 --- /dev/null +++ b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php @@ -0,0 +1,40 @@ +collection; + } + + public function setCollection(GdprSearchResultCollection $collection): void + { + $this->collection = $collection; + } +} \ No newline at end of file diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index 9d7a158d1..1f6f400a2 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -17,63 +17,32 @@ use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; -use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; interface DataProviderInterface { /** - * Searches for personal data within this provider's domain. - * - * @param SearchTerms|null $terms The search values (can be null if none provided) - * - - * @return array> + * @return GdprDataRow[] */ public function findData(?SearchTerms $terms): array; - /** - * Returns the human-readable name for this provider. - * - * @return string - */ public function getName(): string; - /** - * Returns the unique identifying key for this provider. - * - * @return string - */ public function getKey(): string; - /** - * A higher number means a higher priority (appears first). - * - * @return int - */ public function getSortPriority(): int; /** - * Returns the list of available columns for the result data. - * * @return GdprDataColumn[] - */ + */ public function getAvailableColumns(): array; - /** - * Returns the general UserPermission required to run this provider. - * - * @return UserPermissions + /** + * @return string[] (e.g., ['users', 'objects']) */ - public function getRequiredPermission(): UserPermissions; + public function getRequiredPermissions(): array; /** - * Fetches a single item's data for export. - * The returned data will be serialized as JSON. - * - * @param int $id - * - * @return array|object - * * @throws NotFoundException * @throws ForbiddenException */ diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index 5512d52a8..f25f1fa42 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -16,17 +16,18 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; -use Pimcore\Model\User; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; use Pimcore\Model\User\Listing; use function count; /** - * Searches for Pimcore backend users. - * * @internal */ final readonly class PimcoreUserProvider implements DataProviderInterface { + /** + * {@inheritdoc} + */ public function findData(?SearchTerms $terms): array { $listing = new Listing(); @@ -55,8 +56,8 @@ public function findData(?SearchTerms $terms): array } // If we have conditions, apply them. - if (count($conditionParts) > 1) { // We have more than just the base "type = 'user'" - $listing->setCondition(implode(' OR ', $conditionParts), $params); + if (count($conditionParts) > 1) { + $listing->setCondition(implode(' AND ', $conditionParts), $params); } else { // Only the "type = 'user'" condition exists $listing->setCondition($conditionParts[0]); @@ -64,29 +65,25 @@ public function findData(?SearchTerms $terms): array $users = $listing->getUsers(); - $results = []; - foreach ($users as $user) { - $results[] = [ - 'id' => $user->getId(), - 'name' => $user->getName(), - 'firstname' => $user->getFirstname(), - 'lastname' => $user->getLastname(), - 'email' => $user->getEmail(), - ]; - } - - return $results; + $columns = $this->getAvailableColumns(); + + return array_map( + fn ($user) => new GdprDataRow( + [ + 'id' => $user->getId(), + 'name' => $user->getName(), + 'firstname' => $user->getFirstname(), + 'lastname' => $user->getLastname(), + 'email' => $user->getEmail(), + ], + $columns + ), + $users + ); } /** - * Get all relevant GDPR data for a single user. - * Required by the manager service for single-item export. - * - * @param int $id - * - * @return array - * - * @throws NotFoundException + * {@inheritdoc} */ public function getSingleItemForDownload(int $id): array { @@ -104,15 +101,10 @@ public function getSingleItemForDownload(int $id): array return [ 'id' => $user->getId(), - 'parentId' => $user->getParentId(), - 'type' => $user->getType(), 'name' => $user->getName(), 'firstname' => $user->getFirstname(), 'lastname' => $user->getLastname(), 'email' => $user->getEmail(), - 'active' => $user->getActive(), - 'admin' => $user->isAdmin(), - 'language' => $user->getLanguage(), ]; } @@ -131,24 +123,25 @@ public function getSortPriority(): int return 5; } - public function getRequiredPermission(): UserPermissions + /** + * {@inheritdoc} + */ + public function getRequiredPermissions(): array { - return UserPermissions::PIMCORE_USER; + return [UserPermissions::PIMCORE_USER->value]; } + /** + * {@inheritdoc} + */ public function getAvailableColumns(): array { return [ new GdprDataColumn('id', 'ID'), - new GdprDataColumn('parentId', 'Parent ID'), - new GdprDataColumn('type', 'Type'), new GdprDataColumn('name', 'Username'), new GdprDataColumn('firstname', 'First Name'), new GdprDataColumn('lastname', 'Last Name'), new GdprDataColumn('email', 'Email'), - new GdprDataColumn('active', 'Active'), - new GdprDataColumn('admin', 'Admin'), - new GdprDataColumn('language', 'Language'), ]; } } diff --git a/src/Gdpr/Schema/GdprDataRow.php b/src/Gdpr/Schema/GdprDataRow.php new file mode 100644 index 000000000..1280c2a5a --- /dev/null +++ b/src/Gdpr/Schema/GdprDataRow.php @@ -0,0 +1,43 @@ + $data + * @param GdprDataColumn[] $availableColumns + */ + public function __construct( + private array $data, + private array $availableColumns + ) { + foreach ($availableColumns as $column) { + $columnName = $column->getKey(); + if (!array_key_exists($columnName, $data)) { + throw new InvalidArgumentException(sprintf( + 'Missing required column "%s"', + $columnName + )); + } + } + } + + public function getData(): array + { + return $this->data; + } +} \ No newline at end of file diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 0c5496d18..049398879 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -28,6 +28,8 @@ use Pimcore\Bundle\StudioBackendBundle\Util\Trait\StreamedResponseTrait; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprSearchResultEvent; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprExportDataEvent; use function count; use function sprintf; use function strlen; @@ -46,6 +48,9 @@ public function __construct( ) { } + /** + * {@inheritdoc} + */ public function getAvailableProviders(): Collection { $providers = $this->sortProviders($this->loader->getDataProviders()); @@ -53,6 +58,9 @@ public function getAvailableProviders(): Collection return $this->getDataProviderCollection($providers); } + /** + * {@inheritdoc} + */ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection { $allResults = []; @@ -61,15 +69,27 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo foreach ($request->providers as $providerKey) { $provider = $this->loader->resolve($providerKey); - $permission = $provider->getRequiredPermission(); + $permissions = $provider->getRequiredPermissions(); + $isGranted = false; + + if (empty($permissions)) { + $isGranted = true; // No permissions required + } else { + foreach ($permissions as $permission) { + if ($currentUser->isAllowed($permission)) { + $isGranted = true; + break; + } + } + } - // Check if the current user has the required permission to access the provider - if (!$currentUser->isAllowed($permission->value)) { + // Check if the current user has the required permission + if (!$isGranted) { throw new ForbiddenException( sprintf( - 'Not allowed to access the targeted provider "%s". Required permission: "%s"', + 'Not allowed to access the targeted provider "%s". Required permission(s): "%s"', $providerKey, - $permission->value + implode(', ', $permissions) ) ); } @@ -83,13 +103,12 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo ); } } + return $this->getSearchResultCollection($allResults); - return new GdprSearchResultCollection($allResults); } /** - * @throws ForbiddenException - * @throws NotFoundException + * {@inheritdoc} */ public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse { @@ -97,35 +116,33 @@ public function getExportDataAsJson(int $id, string $providerKey): StreamedRespo $provider = $this->loader->resolve($providerKey); - $permission = $provider->getRequiredPermission(); - if (!$currentUser->isAllowed($permission->value)) { - throw new ForbiddenException("Not allowed for provider: {$provider->getKey()}"); + $permissions = $provider->getRequiredPermissions(); + $isGranted = false; + + if (empty($permissions)) { + $isGranted = true; // No permissions required + } else { + foreach ($permissions as $permission) { + if ($currentUser->isAllowed($permission)) { + $isGranted = true; + break; + } + } } - $data = $provider->getSingleItemForDownload($id); //id is a single item of a particular provider - - $jsonData = json_encode($data, JSON_PRETTY_PRINT); - - $filename = sprintf('gdpr-export-%s-%d.json', $providerKey, $id); - $fileSize = strlen($jsonData); - - $headers = $this->getResponseHeaders( - mimeType: 'application/json', - fileSize: $fileSize, - filename: $filename, - contentDisposition: HttpResponseHeaders::ATTACHMENT_TYPE->value, // 'attachment' - additionalHeaders: [] - ); + if (!$isGranted) { + throw new ForbiddenException( + sprintf( + 'Not allowed for provider: %s. Required permission(s): %s', + $provider->getKey(), + implode(', ', $permissions) + ) + ); + } - $response = new StreamedResponse( - function () use ($jsonData) { - echo $jsonData; - }, - HttpResponseCodes::SUCCESS->value, - $headers - ); + $data = $provider->getSingleItemForDownload($id); //id is a single item of a particular provider - return $response; + return $this->createExportResponse($data, $providerKey, $id); } /** @@ -156,8 +173,52 @@ private function getDataProviderCollection(array $providers): Collection } /** - * Sorts the providers by priority. - * + * @param array $results + */ + private function getSearchResultCollection(array $results): GdprSearchResultCollection + { + $collection = new GdprSearchResultCollection($results); + + $this->eventDispatcher->dispatch( + new GdprSearchResultEvent($collection), + GdprSearchResultEvent::EVENT_NAME + ); + + return $collection; + } + + /** + * Helper to create the export response, dispatch event, and stream data. + */ + private function createExportResponse(mixed $data, string $providerKey, int $id): StreamedResponse + { + $event = new GdprExportDataEvent($data); + $this->eventDispatcher->dispatch($event, GdprExportDataEvent::EVENT_NAME); + $finalData = $event->getData(); + + $jsonData = json_encode($finalData, JSON_THROW_ON_ERROR); + + $filename = sprintf('gdpr-export-%s-%d.json', $providerKey, $id); + $fileSize = strlen($jsonData); + + $headers = $this->getResponseHeaders( + mimeType: 'application/json', + fileSize: $fileSize, + filename: $filename, + contentDisposition: HttpResponseHeaders::ATTACHMENT_TYPE->value, + additionalHeaders: [] + ); + + return new StreamedResponse( + function () use ($jsonData) { + echo $jsonData; + }, + HttpResponseCodes::SUCCESS->value, + $headers + ); + } + + /** * @param array $providers * * @return array diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php index ad58e322d..c8531e6d7 100644 --- a/src/Gdpr/Service/GdprManagerServiceInterface.php +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -27,8 +27,6 @@ interface GdprManagerServiceInterface { /** - * Returns a sorted collection of all available GDPR data providers. - * * @return Collection */ public function getAvailableProviders(): Collection; diff --git a/translations/studio_api_docs.en.yaml b/translations/studio_api_docs.en.yaml index 994207235..e461768df 100644 --- a/translations/studio_api_docs.en.yaml +++ b/translations/studio_api_docs.en.yaml @@ -1416,6 +1416,13 @@ data_object_get_phpcode_transformers_success_response: List of available PHPCode gdpr_list_providers_summary: List of available GDPR providers gdpr_list_providers_description: Returns a list of all configured GDPR providers that can be used for data compliance operations. gdpr_list_providers_success_response: Successfully retrieved the list of GDPR providers +tag_gdpr_description: All operations related to GDPR data search and export. +gdpr_export_summary: Export a single GDPR data provider item +gdpr_export_description: Fetches the data for a single data provider item (by ID) and returns it as a downloadable JSON file. +gdpr_export_success_response: Successfully retrieved the data export as a downloadable JSON file. +gdpr_search_data_summary: Search for GDPR data from all the available data providers +gdpr_search_data_description: Searches for GDPR data using the provided search terms and returns a list of results grouped by each provider name. +gdpr_search_data_success_response: Successfully retrieved the list of matching data from all searched providers. pimcore_studio_api_elements_get_usage: Get usage of element element_get_usage: Get usage of element element_get_usage_description: Get usage of element. Use {elementType} and {id} to specify the element. From db0a05c0b682b5ef77f78e5884a1f8bc9b839942 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Tue, 18 Nov 2025 09:51:29 +0000 Subject: [PATCH 15/34] changes --- doc/10_Extending_Studio/11_Gdpr.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/10_Extending_Studio/11_Gdpr.md b/doc/10_Extending_Studio/11_Gdpr.md index 4397a9bc8..d64577589 100644 --- a/doc/10_Extending_Studio/11_Gdpr.md +++ b/doc/10_Extending_Studio/11_Gdpr.md @@ -12,13 +12,14 @@ As a developer, you only need to register it with the `pimcore.studio_backend.gd This flow happens when a user opens the GDPR Data Extractor page and clicks "Search". -1. **To build the page:** The system first calls these methods on your provider to build the UI: +1. **To build the page:** The user needs to create these methods on their newly created provider: - `getName()`: To get the human-friendly name for the provider list. - `getKey()`: To get the unique ID. - `getSortPriority()`: To decide where to place your provider in the list. - `getAvailableColumns()`: To build the columns for the search results grid. - - add more + - `getRequiredPermissions()`: One or more permissions required by user to access the data provider information + - `findData()`: Find the data in the particular provider using the searched terms 2. **When the user clicks "Search":** - The system first calls your `getRequiredPermissions()` method to check if the current user is allowed to use your provider. From 4b8c6ced29d177b17d6b12cc27a03e6fb0b325d5 Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:56:43 +0000 Subject: [PATCH 16/34] Apply php-cs-fixer changes --- src/Gdpr/Controller/ExportController.php | 5 ++--- src/Gdpr/Event/PreResponse/GdprExportDataEvent.php | 8 ++++---- src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php | 8 ++++---- src/Gdpr/Provider/DataProviderInterface.php | 4 ++-- src/Gdpr/Provider/PimcoreUserProvider.php | 4 ++-- src/Gdpr/Schema/GdprDataRow.php | 6 ++++-- src/Gdpr/Service/GdprManagerService.php | 10 ++++++---- 7 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index 7dde9e5ad..c3cdea8c7 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -14,10 +14,9 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Controller; use OpenApi\Attributes\Get; -use OpenApi\Attributes\Parameter; -use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Query\TextFieldParameter; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Query\TextFieldParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Header\ContentDisposition; @@ -62,7 +61,7 @@ public function __construct( description: 'The key of the single provider to export', required: true, example: 'pimcore_user' - ) + ), ] )] #[SuccessResponse( diff --git a/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php b/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php index 09ee63967..d1b1b64e7 100644 --- a/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php +++ b/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php @@ -7,8 +7,8 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse; @@ -17,7 +17,7 @@ /** * @internal -*/ + */ final class GdprExportDataEvent extends Event { public const string EVENT_NAME = 'pre_response.gdpr_export_data'; @@ -36,4 +36,4 @@ public function setData(array|object $data): void { $this->data = $data; } -} \ No newline at end of file +} diff --git a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php index a05a4dac5..7bd99d186 100644 --- a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php +++ b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php @@ -7,14 +7,14 @@ * Full copyright and license information is available in * LICENSE.md which is distributed with this source code. * - * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) - * @license Pimcore Open Core License (POCL) + * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) + * @license Pimcore Open Core License (POCL) */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse; -use Symfony\Contracts\EventDispatcher\Event; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; +use Symfony\Contracts\EventDispatcher\Event; /** * @internal @@ -37,4 +37,4 @@ public function setCollection(GdprSearchResultCollection $collection): void { $this->collection = $collection; } -} \ No newline at end of file +} diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index 1f6f400a2..37fa8811c 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -34,10 +34,10 @@ public function getSortPriority(): int; /** * @return GdprDataColumn[] - */ + */ public function getAvailableColumns(): array; - /** + /** * @return string[] (e.g., ['users', 'objects']) */ public function getRequiredPermissions(): array; diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index f25f1fa42..adb0c8fb7 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -15,8 +15,8 @@ use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; -use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; +use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Model\User\Listing; use function count; @@ -133,7 +133,7 @@ public function getRequiredPermissions(): array /** * {@inheritdoc} - */ + */ public function getAvailableColumns(): array { return [ diff --git a/src/Gdpr/Schema/GdprDataRow.php b/src/Gdpr/Schema/GdprDataRow.php index 1280c2a5a..3dc8192ff 100644 --- a/src/Gdpr/Schema/GdprDataRow.php +++ b/src/Gdpr/Schema/GdprDataRow.php @@ -12,8 +12,10 @@ */ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException; +use function array_key_exists; +use function sprintf; final readonly class GdprDataRow { @@ -40,4 +42,4 @@ public function getData(): array { return $this->data; } -} \ No newline at end of file +} diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 049398879..54a2d3e4b 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -14,8 +14,9 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Service; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprDataProviderEvent; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprExportDataEvent; +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; @@ -28,8 +29,6 @@ use Pimcore\Bundle\StudioBackendBundle\Util\Trait\StreamedResponseTrait; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprSearchResultEvent; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprExportDataEvent; use function count; use function sprintf; use function strlen; @@ -78,6 +77,7 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo foreach ($permissions as $permission) { if ($currentUser->isAllowed($permission)) { $isGranted = true; + break; } } @@ -103,6 +103,7 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo ); } } + return $this->getSearchResultCollection($allResults); } @@ -121,10 +122,11 @@ public function getExportDataAsJson(int $id, string $providerKey): StreamedRespo if (empty($permissions)) { $isGranted = true; // No permissions required - } else { + } else { foreach ($permissions as $permission) { if ($currentUser->isAllowed($permission)) { $isGranted = true; + break; } } From 7cf7a931da441e746758d3c7cc7b78e2a018a5cc Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Tue, 18 Nov 2025 10:04:36 +0000 Subject: [PATCH 17/34] sonar fixes --- src/Gdpr/Schema/GdprDataRow.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gdpr/Schema/GdprDataRow.php b/src/Gdpr/Schema/GdprDataRow.php index 3dc8192ff..989465e54 100644 --- a/src/Gdpr/Schema/GdprDataRow.php +++ b/src/Gdpr/Schema/GdprDataRow.php @@ -25,7 +25,7 @@ */ public function __construct( private array $data, - private array $availableColumns + array $availableColumns ) { foreach ($availableColumns as $column) { $columnName = $column->getKey(); From 107468ae72efe9ff6ef32c03a89b8f0b321948af Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Tue, 18 Nov 2025 10:14:59 +0000 Subject: [PATCH 18/34] type fix docs --- src/Gdpr/Schema/GdprSearchResult.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Gdpr/Schema/GdprSearchResult.php b/src/Gdpr/Schema/GdprSearchResult.php index dbee9f692..a08a40c35 100644 --- a/src/Gdpr/Schema/GdprSearchResult.php +++ b/src/Gdpr/Schema/GdprSearchResult.php @@ -16,6 +16,7 @@ use OpenApi\Attributes\Items; use OpenApi\Attributes\Property; use OpenApi\Attributes\Schema; +use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; /** * @internal @@ -29,7 +30,7 @@ final class GdprSearchResult { /** - * @param array> $results + * @param GdprDataRow[] $results */ public function __construct( #[Property( @@ -37,14 +38,14 @@ public function __construct( type: 'string', example: 'data_objects' )] - private string $providerKey, + private readonly string $providerKey, #[Property( description: 'The list of results found by this provider', type: 'array', - items: new Items(type: 'object', example: '{"id": 1, "path": "/data/customer/1"}') + items: new Items(ref: GdprDataRow::class) )] - private array $results, + private readonly array $results, ) { } @@ -54,7 +55,7 @@ public function getProviderKey(): string } /** - * @return array> + * @return GdprDataRow[] */ public function getResults(): array { From 20287125161ca318af239212bb67b2ef75292acf Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:17:05 +0000 Subject: [PATCH 19/34] Apply php-cs-fixer changes --- src/Gdpr/Schema/GdprSearchResult.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Gdpr/Schema/GdprSearchResult.php b/src/Gdpr/Schema/GdprSearchResult.php index a08a40c35..db4b213d1 100644 --- a/src/Gdpr/Schema/GdprSearchResult.php +++ b/src/Gdpr/Schema/GdprSearchResult.php @@ -16,7 +16,6 @@ use OpenApi\Attributes\Items; use OpenApi\Attributes\Property; use OpenApi\Attributes\Schema; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; /** * @internal @@ -55,7 +54,7 @@ public function getProviderKey(): string } /** - * @return GdprDataRow[] + * @return GdprDataRow[] */ public function getResults(): array { From 68712e511ba502ad2562386da4921e917f99e757 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 19 Nov 2025 08:49:35 +0000 Subject: [PATCH 20/34] Re update Json event, Handle json error, Parameter, schema, event handling, rephase, non nullable search,Text Parameter fixes --- .../Attribute/Request/GdprRequestBody.php | 22 +++- src/Gdpr/Controller/ExportController.php | 16 ++- .../Event/PreResponse/GdprExportDataEvent.php | 39 ------- .../PreResponse/GdprSearchResultEvent.php | 13 +-- .../GdprStructuredSearchRequest.php | 4 +- src/Gdpr/Provider/DataProviderInterface.php | 2 +- src/Gdpr/Provider/PimcoreUserProvider.php | 2 +- .../Schema/GdprSearchResultCollection.php | 9 +- src/Gdpr/Service/GdprManagerService.php | 110 ++++++++---------- 9 files changed, 90 insertions(+), 127 deletions(-) delete mode 100644 src/Gdpr/Event/PreResponse/GdprExportDataEvent.php diff --git a/src/Gdpr/Attribute/Request/GdprRequestBody.php b/src/Gdpr/Attribute/Request/GdprRequestBody.php index 779459a68..245c41e79 100644 --- a/src/Gdpr/Attribute/Request/GdprRequestBody.php +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -14,6 +14,7 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request; use Attribute; +use OpenApi\Attributes\Items; use OpenApi\Attributes\JsonContent; use OpenApi\Attributes\Property; use OpenApi\Attributes\RequestBody; @@ -29,13 +30,24 @@ public function __construct() parent::__construct( required: true, content: new JsonContent( - required: ['providerName'], + + required: ['providers', 'searchTerms'], properties: [ new Property( - property: 'providerName', - description: 'The key of the single provider to search (e.g., pimcore_user)', - type: 'string', - example: 'pimcore_user' + property: 'providers', + description: 'A list of provider keys to search', + type: 'array', + items: new Items( + type: 'string', + example: 'pimcore_users' + ) + ), + + new Property( + property: 'searchTerms', + description: 'The object containing the search values.', + ref: SearchTerms::class, + type: 'object' ), ], type: 'object', diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index c3cdea8c7..10e677459 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -54,15 +54,13 @@ public function __construct( operationId: 'gdpr_export', summary: 'gdpr_export_summary', description: 'gdpr_export_description', - tags: [Tags::Export->value], - parameters: [ - new TextFieldParameter( - name: 'providerKey', - description: 'The key of the single provider to export', - required: true, - example: 'pimcore_user' - ), - ] + tags: [Tags::Export->value] + )] + #[TextFieldParameter( + name: 'providerKey', + description: 'The key of the single provider to export', + required: true, + example: 'pimcore_user' )] #[SuccessResponse( description: 'gdpr_export_success_response', diff --git a/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php b/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php deleted file mode 100644 index d1b1b64e7..000000000 --- a/src/Gdpr/Event/PreResponse/GdprExportDataEvent.php +++ /dev/null @@ -1,39 +0,0 @@ -data; - } - - public function setData(array|object $data): void - { - $this->data = $data; - } -} diff --git a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php index 7bd99d186..8fb5558b9 100644 --- a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php +++ b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php @@ -13,28 +13,23 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse; +use Pimcore\Bundle\StudioBackendBundle\Event\AbstractPreResponseEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; -use Symfony\Contracts\EventDispatcher\Event; /** * @internal */ -final class GdprSearchResultEvent extends Event +final class GdprSearchResultEvent extends AbstractPreResponseEvent { public const string EVENT_NAME = 'pre_response.gdpr_search_result'; - public function __construct(private GdprSearchResultCollection $collection) + public function __construct(private readonly GdprSearchResultCollection $collection) { - + parent::__construct($this->collection); } public function getCollection(): GdprSearchResultCollection { return $this->collection; } - - public function setCollection(GdprSearchResultCollection $collection): void - { - $this->collection = $collection; - } } diff --git a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php index bf2848041..e8cf5a3e6 100644 --- a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php +++ b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php @@ -18,6 +18,7 @@ use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Type; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Constraints\NotNull; /** * @internal @@ -33,7 +34,8 @@ public function __construct( public array $providers, #[Valid] - public ?SearchTerms $searchTerms = null + #[NotNull] + public SearchTerms $searchTerms ) { } } diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index 37fa8811c..4c12a556c 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -24,7 +24,7 @@ interface DataProviderInterface /** * @return GdprDataRow[] */ - public function findData(?SearchTerms $terms): array; + public function findData(SearchTerms $terms): array; public function getName(): string; diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index adb0c8fb7..8697ec354 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -28,7 +28,7 @@ /** * {@inheritdoc} */ - public function findData(?SearchTerms $terms): array + public function findData(SearchTerms $terms): array { $listing = new Listing(); $conditionParts = []; diff --git a/src/Gdpr/Schema/GdprSearchResultCollection.php b/src/Gdpr/Schema/GdprSearchResultCollection.php index 30561d166..1e8f2b4cf 100644 --- a/src/Gdpr/Schema/GdprSearchResultCollection.php +++ b/src/Gdpr/Schema/GdprSearchResultCollection.php @@ -13,6 +13,8 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; +use Pimcore\Bundle\StudioBackendBundle\Util\Schema\AdditionalAttributesInterface; +use Pimcore\Bundle\StudioBackendBundle\Util\Trait\AdditionalAttributesTrait; use OpenApi\Attributes\Items; use OpenApi\Attributes\Property; use OpenApi\Attributes\Schema; @@ -27,8 +29,10 @@ required: ['items'] )] -final class GdprSearchResultCollection +final class GdprSearchResultCollection implements AdditionalAttributesInterface { + use AdditionalAttributesTrait; + /** * @param array $items */ @@ -38,7 +42,8 @@ public function __construct( type: 'array', items: new Items(ref: GdprSearchResult::class) )] - private array $items, + + private readonly array $items, ) { } diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 54a2d3e4b..9e7a07d77 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -15,7 +15,6 @@ use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprDataProviderEvent; -use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprExportDataEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Event\PreResponse\GdprSearchResultEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\MappedParameter\GdprStructuredSearchRequest; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface; @@ -27,8 +26,10 @@ use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseHeaders; use Pimcore\Bundle\StudioBackendBundle\Util\Trait\StreamedResponseTrait; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use JsonException; use function count; use function sprintf; use function strlen; @@ -63,36 +64,11 @@ public function getAvailableProviders(): Collection public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection { $allResults = []; - $currentUser = $this->securityService->getCurrentUser(); foreach ($request->providers as $providerKey) { $provider = $this->loader->resolve($providerKey); - - $permissions = $provider->getRequiredPermissions(); - $isGranted = false; - - if (empty($permissions)) { - $isGranted = true; // No permissions required - } else { - foreach ($permissions as $permission) { - if ($currentUser->isAllowed($permission)) { - $isGranted = true; - - break; - } - } - } - - // Check if the current user has the required permission - if (!$isGranted) { - throw new ForbiddenException( - sprintf( - 'Not allowed to access the targeted provider "%s". Required permission(s): "%s"', - $providerKey, - implode(', ', $permissions) - ) - ); - } + + $this->checkProviderPermission($provider); $results = $provider->findData($request->searchTerms); @@ -113,34 +89,9 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo */ public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse { - $currentUser = $this->securityService->getCurrentUser(); - $provider = $this->loader->resolve($providerKey); - - $permissions = $provider->getRequiredPermissions(); - $isGranted = false; - - if (empty($permissions)) { - $isGranted = true; // No permissions required - } else { - foreach ($permissions as $permission) { - if ($currentUser->isAllowed($permission)) { - $isGranted = true; - - break; - } - } - } - - if (!$isGranted) { - throw new ForbiddenException( - sprintf( - 'Not allowed for provider: %s. Required permission(s): %s', - $provider->getKey(), - implode(', ', $permissions) - ) - ); - } + + $this->checkProviderPermission($provider); $data = $provider->getSingleItemForDownload($id); //id is a single item of a particular provider @@ -194,11 +145,19 @@ private function getSearchResultCollection(array $results): GdprSearchResultColl */ private function createExportResponse(mixed $data, string $providerKey, int $id): StreamedResponse { - $event = new GdprExportDataEvent($data); - $this->eventDispatcher->dispatch($event, GdprExportDataEvent::EVENT_NAME); - $finalData = $event->getData(); - - $jsonData = json_encode($finalData, JSON_THROW_ON_ERROR); + try { + $jsonData = json_encode($data, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new InvalidArgumentException( + sprintf( + 'JSON encode failed for "%s" (ID: %d): %s', + $providerKey, + $id, + $e->getMessage() + ), + previous: $e + ); + } $filename = sprintf('gdpr-export-%s-%d.json', $providerKey, $id); $fileSize = strlen($jsonData); @@ -234,4 +193,35 @@ private function sortProviders(array $providers): array return $providers; } + + /** + * @throws ForbiddenException + */ + private function checkProviderPermission(DataProviderInterface $provider): void + { + $currentUser = $this->securityService->getCurrentUser(); + $permissions = $provider->getRequiredPermissions(); + + // Check if user has at least one of the required permissions in order to access the provider + $isGranted = false; + + if ($currentUser !== null) { + foreach ($permissions as $permission) { + if ($currentUser->isAllowed($permission)) { + $isGranted = true; + break; + } + } + } + + if (!$isGranted) { + throw new ForbiddenException( + sprintf( + 'Not allowed for provider: %s. Required permission(s): %s', + $provider->getKey(), + implode(', ', $permissions) + ) + ); + } + } } From 26d9554dc2207866a351b080c2abaef9795c4c17 Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Wed, 19 Nov 2025 08:56:29 +0000 Subject: [PATCH 21/34] Apply php-cs-fixer changes --- src/Gdpr/Attribute/Request/GdprRequestBody.php | 2 +- src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php | 2 +- src/Gdpr/Schema/GdprSearchResultCollection.php | 4 ++-- src/Gdpr/Service/GdprManagerService.php | 9 +++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Gdpr/Attribute/Request/GdprRequestBody.php b/src/Gdpr/Attribute/Request/GdprRequestBody.php index 245c41e79..f5d4554d4 100644 --- a/src/Gdpr/Attribute/Request/GdprRequestBody.php +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -31,7 +31,7 @@ public function __construct() required: true, content: new JsonContent( - required: ['providers', 'searchTerms'], + required: ['providers', 'searchTerms'], properties: [ new Property( property: 'providers', diff --git a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php index e8cf5a3e6..32d92fb40 100644 --- a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php +++ b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php @@ -16,9 +16,9 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Symfony\Component\Validator\Constraints\All; use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Component\Validator\Constraints\NotNull; use Symfony\Component\Validator\Constraints\Type; use Symfony\Component\Validator\Constraints\Valid; -use Symfony\Component\Validator\Constraints\NotNull; /** * @internal diff --git a/src/Gdpr/Schema/GdprSearchResultCollection.php b/src/Gdpr/Schema/GdprSearchResultCollection.php index 1e8f2b4cf..cafb31e1a 100644 --- a/src/Gdpr/Schema/GdprSearchResultCollection.php +++ b/src/Gdpr/Schema/GdprSearchResultCollection.php @@ -13,11 +13,11 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema; -use Pimcore\Bundle\StudioBackendBundle\Util\Schema\AdditionalAttributesInterface; -use Pimcore\Bundle\StudioBackendBundle\Util\Trait\AdditionalAttributesTrait; use OpenApi\Attributes\Items; use OpenApi\Attributes\Property; use OpenApi\Attributes\Schema; +use Pimcore\Bundle\StudioBackendBundle\Util\Schema\AdditionalAttributesInterface; +use Pimcore\Bundle\StudioBackendBundle\Util\Trait\AdditionalAttributesTrait; /** * @internal diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 9e7a07d77..876f0ed08 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -13,7 +13,9 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Service; +use JsonException; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; +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; @@ -26,10 +28,8 @@ use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseCodes; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\HttpResponseHeaders; use Pimcore\Bundle\StudioBackendBundle\Util\Trait\StreamedResponseTrait; -use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -use JsonException; use function count; use function sprintf; use function strlen; @@ -67,7 +67,7 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo foreach ($request->providers as $providerKey) { $provider = $this->loader->resolve($providerKey); - + $this->checkProviderPermission($provider); $results = $provider->findData($request->searchTerms); @@ -90,7 +90,7 @@ public function search(GdprStructuredSearchRequest $request): GdprSearchResultCo public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse { $provider = $this->loader->resolve($providerKey); - + $this->checkProviderPermission($provider); $data = $provider->getSingleItemForDownload($id); //id is a single item of a particular provider @@ -209,6 +209,7 @@ private function checkProviderPermission(DataProviderInterface $provider): void foreach ($permissions as $permission) { if ($currentUser->isAllowed($permission)) { $isGranted = true; + break; } } From 5e5176e7a9c562ee58c05310477ec4b94e41e5d9 Mon Sep 17 00:00:00 2001 From: Martin Eiber Date: Wed, 19 Nov 2025 12:04:25 +0100 Subject: [PATCH 22/34] Minor Changes. --- .../Attribute/Request/GdprRequestBody.php | 2 +- src/Gdpr/Attribute/Request/SearchTerms.php | 3 +- src/Gdpr/Controller/ExportController.php | 8 ++--- .../Controller/GetDataProviderController.php | 2 +- .../SearchDataProviderController.php | 2 +- .../PreResponse/GdprDataProviderEvent.php | 4 +-- .../PreResponse/GdprSearchResultEvent.php | 4 +-- src/Gdpr/Provider/PimcoreUserProvider.php | 34 +++++++++---------- src/Gdpr/Schema/GdprDataColumn.php | 6 ++-- src/Gdpr/Schema/GdprSearchResult.php | 2 +- .../Schema/GdprSearchResultCollection.php | 4 +-- .../Service/DataProviderLoaderInterface.php | 2 +- src/Gdpr/Service/GdprManagerService.php | 13 +++---- 13 files changed, 39 insertions(+), 47 deletions(-) diff --git a/src/Gdpr/Attribute/Request/GdprRequestBody.php b/src/Gdpr/Attribute/Request/GdprRequestBody.php index f5d4554d4..52585055f 100644 --- a/src/Gdpr/Attribute/Request/GdprRequestBody.php +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -45,8 +45,8 @@ public function __construct() new Property( property: 'searchTerms', - description: 'The object containing the search values.', ref: SearchTerms::class, + description: 'The object containing the search values.', type: 'object' ), ], diff --git a/src/Gdpr/Attribute/Request/SearchTerms.php b/src/Gdpr/Attribute/Request/SearchTerms.php index ec9fafc22..4c57ba974 100644 --- a/src/Gdpr/Attribute/Request/SearchTerms.php +++ b/src/Gdpr/Attribute/Request/SearchTerms.php @@ -42,7 +42,8 @@ public function __construct( public ?string $lastname = null, #[Property(description: 'The email address to search for.', type: 'string', nullable: true)] - #[Type('string')]//why is #[Email] constraint causing issues + #[Type('string')] + #[Email] public ?string $email = null, ) { } diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index 10e677459..0f497def9 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -45,16 +45,16 @@ public function __construct( #[Route( '/gdpr/export-data/{id}', name: 'pimcore_studio_api_gdpr_export_start', - methods: ['GET'], - requirements: ['id' => '\d+'] + requirements: ['id' => '\d+'], + methods: ['GET'] )] #[IsGranted(UserPermissions::GDPR->value)] #[Get( path: self::PREFIX . '/gdpr/export-data/{id}', operationId: 'gdpr_export', - summary: 'gdpr_export_summary', description: 'gdpr_export_description', - tags: [Tags::Export->value] + summary: 'gdpr_export_summary', + tags: [Tags::GDPR->value] )] #[TextFieldParameter( name: 'providerKey', diff --git a/src/Gdpr/Controller/GetDataProviderController.php b/src/Gdpr/Controller/GetDataProviderController.php index a04715d7a..35b334916 100644 --- a/src/Gdpr/Controller/GetDataProviderController.php +++ b/src/Gdpr/Controller/GetDataProviderController.php @@ -58,8 +58,8 @@ public function __construct( #[GET( path: self::PREFIX . '/gdpr/providers', operationId: 'gdpr_list_providers', - summary: 'gdpr_list_providers_summary', description: 'gdpr_list_providers_description', + summary: 'gdpr_list_providers_summary', tags: [Tags::GDPR->value] )] #[SuccessResponse( diff --git a/src/Gdpr/Controller/SearchDataProviderController.php b/src/Gdpr/Controller/SearchDataProviderController.php index 7330014da..a64744f32 100644 --- a/src/Gdpr/Controller/SearchDataProviderController.php +++ b/src/Gdpr/Controller/SearchDataProviderController.php @@ -57,8 +57,8 @@ public function __construct( #[POST( path: self::PREFIX . '/gdpr/search', operationId: 'gdpr_search_data', - summary: 'gdpr_search_data_summary', description: 'gdpr_search_data_description', + summary: 'gdpr_search_data_summary', tags: [Tags::GDPR->value] )] #[GdprRequestBody] diff --git a/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php index 928e8520e..b2f2f4336 100644 --- a/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php +++ b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php @@ -16,9 +16,7 @@ use Pimcore\Bundle\StudioBackendBundle\Event\AbstractPreResponseEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataProvider; -/** - * @internal - */ + final class GdprDataProviderEvent extends AbstractPreResponseEvent { public const string EVENT_NAME = 'pre_response.data_provider'; diff --git a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php index 8fb5558b9..9882997c5 100644 --- a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php +++ b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php @@ -16,9 +16,7 @@ use Pimcore\Bundle\StudioBackendBundle\Event\AbstractPreResponseEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; -/** - * @internal - */ + final class GdprSearchResultEvent extends AbstractPreResponseEvent { public const string EVENT_NAME = 'pre_response.gdpr_search_result'; diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index 8697ec354..9ed252405 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -35,24 +35,22 @@ public function findData(SearchTerms $terms): array $params = []; $conditionParts[] = "`type` = 'user'"; - // Only try to build if we actually have search terms - if ($terms !== null) { - if ($terms->id !== null) { - $conditionParts[] = 'id = ?'; - $params[] = $terms->id; - } - if ($terms->firstname !== null) { - $conditionParts[] = 'firstname LIKE ?'; - $params[] = '%' . $terms->firstname . '%'; - } - if ($terms->lastname !== null) { - $conditionParts[] = 'lastname LIKE ?'; - $params[] = '%' . $terms->lastname . '%'; - } - if ($terms->email !== null) { - $conditionParts[] = 'email LIKE ?'; - $params[] = '%' . $terms->email . '%'; - } + + if ($terms->id !== null) { + $conditionParts[] = 'id = ?'; + $params[] = $terms->id; + } + if ($terms->firstname !== null) { + $conditionParts[] = 'firstname LIKE ?'; + $params[] = '%' . $terms->firstname . '%'; + } + if ($terms->lastname !== null) { + $conditionParts[] = 'lastname LIKE ?'; + $params[] = '%' . $terms->lastname . '%'; + } + if ($terms->email !== null) { + $conditionParts[] = 'email LIKE ?'; + $params[] = '%' . $terms->email . '%'; } // If we have conditions, apply them. diff --git a/src/Gdpr/Schema/GdprDataColumn.php b/src/Gdpr/Schema/GdprDataColumn.php index c229d2682..cde950828 100644 --- a/src/Gdpr/Schema/GdprDataColumn.php +++ b/src/Gdpr/Schema/GdprDataColumn.php @@ -25,7 +25,7 @@ required: ['key', 'label'], type: 'object', )] -final class GdprDataColumn +final readonly class GdprDataColumn { public function __construct( #[Property( @@ -33,14 +33,14 @@ public function __construct( type: 'string', example: 'email' )] - private readonly string $key, + private string $key, #[Property( description: 'Translated label of the column (for the header)', type: 'string', example: 'Email Address' )] - private readonly string $label, + private string $label, ) { } diff --git a/src/Gdpr/Schema/GdprSearchResult.php b/src/Gdpr/Schema/GdprSearchResult.php index db4b213d1..97ce2411b 100644 --- a/src/Gdpr/Schema/GdprSearchResult.php +++ b/src/Gdpr/Schema/GdprSearchResult.php @@ -26,7 +26,7 @@ required: ['providerKey', 'results'], type: 'object', )] -final class GdprSearchResult +final readonly class GdprSearchResult { /** * @param GdprDataRow[] $results diff --git a/src/Gdpr/Schema/GdprSearchResultCollection.php b/src/Gdpr/Schema/GdprSearchResultCollection.php index cafb31e1a..7033a253e 100644 --- a/src/Gdpr/Schema/GdprSearchResultCollection.php +++ b/src/Gdpr/Schema/GdprSearchResultCollection.php @@ -25,8 +25,8 @@ #[Schema( title: 'GDPR Search Result Collection', description: 'A collection of search results from all providers.', - type: 'object', - required: ['items'] + required: ['items'], + type: 'object' )] final class GdprSearchResultCollection implements AdditionalAttributesInterface diff --git a/src/Gdpr/Service/DataProviderLoaderInterface.php b/src/Gdpr/Service/DataProviderLoaderInterface.php index 11e556087..ca3431dd6 100644 --- a/src/Gdpr/Service/DataProviderLoaderInterface.php +++ b/src/Gdpr/Service/DataProviderLoaderInterface.php @@ -21,7 +21,7 @@ */ interface DataProviderLoaderInterface { - public const DATA_PROVIDER_TAG = 'pimcore.studio_backend.gdpr_data_provider'; + public const string DATA_PROVIDER_TAG = 'pimcore.studio_backend.gdpr_data_provider'; /** * @return array diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 876f0ed08..7869714b0 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -166,8 +166,7 @@ private function createExportResponse(mixed $data, string $providerKey, int $id) mimeType: 'application/json', fileSize: $fileSize, filename: $filename, - contentDisposition: HttpResponseHeaders::ATTACHMENT_TYPE->value, - additionalHeaders: [] + contentDisposition: HttpResponseHeaders::ATTACHMENT_TYPE->value ); return new StreamedResponse( @@ -205,13 +204,11 @@ private function checkProviderPermission(DataProviderInterface $provider): void // Check if user has at least one of the required permissions in order to access the provider $isGranted = false; - if ($currentUser !== null) { - foreach ($permissions as $permission) { - if ($currentUser->isAllowed($permission)) { - $isGranted = true; + foreach ($permissions as $permission) { + if ($currentUser->isAllowed($permission)) { + $isGranted = true; - break; - } + break; } } From 4a0bb7e414a206c5ca39d4998936056bee7967e7 Mon Sep 17 00:00:00 2001 From: martineiber <11687066+martineiber@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:05:16 +0000 Subject: [PATCH 23/34] Apply php-cs-fixer changes --- src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php | 1 - src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php index b2f2f4336..1b2954a06 100644 --- a/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php +++ b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php @@ -16,7 +16,6 @@ use Pimcore\Bundle\StudioBackendBundle\Event\AbstractPreResponseEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataProvider; - final class GdprDataProviderEvent extends AbstractPreResponseEvent { public const string EVENT_NAME = 'pre_response.data_provider'; diff --git a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php index 9882997c5..ed368c643 100644 --- a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php +++ b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php @@ -16,7 +16,6 @@ use Pimcore\Bundle\StudioBackendBundle\Event\AbstractPreResponseEvent; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprSearchResultCollection; - final class GdprSearchResultEvent extends AbstractPreResponseEvent { public const string EVENT_NAME = 'pre_response.gdpr_search_result'; From 0bc6f6cd4ad705b19d58a3840297b43c4e78d5cf Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 19 Nov 2025 17:49:46 +0000 Subject: [PATCH 24/34] Additional imporovements and new features added in json download --- doc/05_Additional_Custom_Attributes.md | 1 - src/Gdpr/Attribute/Request/SearchTerms.php | 11 +++- src/Gdpr/Controller/ExportController.php | 7 ++- src/Gdpr/Provider/PimcoreUserProvider.php | 65 ++++++++++++++++++---- src/Gdpr/Service/GdprManagerService.php | 7 +-- 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/doc/05_Additional_Custom_Attributes.md b/doc/05_Additional_Custom_Attributes.md index 9910d0819..16d138bd6 100644 --- a/doc/05_Additional_Custom_Attributes.md +++ b/doc/05_Additional_Custom_Attributes.md @@ -160,6 +160,5 @@ final class AssetEvent extends AbstractPreResponseEvent - `pre_response.php_code_transformer` - `pre_response.data_provider` - `pre_response.gdpr_search_result` -- `pre_response.gdpr_export_data` - `pre_response.element.usage.item` - `pre_response.element.usage` diff --git a/src/Gdpr/Attribute/Request/SearchTerms.php b/src/Gdpr/Attribute/Request/SearchTerms.php index 4c57ba974..49246181f 100644 --- a/src/Gdpr/Attribute/Request/SearchTerms.php +++ b/src/Gdpr/Attribute/Request/SearchTerms.php @@ -15,6 +15,7 @@ use OpenApi\Attributes\Property; use OpenApi\Attributes\Schema; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException; use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Type; @@ -43,9 +44,15 @@ public function __construct( #[Property(description: 'The email address to search for.', type: 'string', nullable: true)] #[Type('string')] - #[Email] - public ?string $email = null, + public ?string $email = null, ) { + if ($this->id === null && + $this->firstname === null && + $this->lastname === null && + $this->email === null + ) { + throw new InvalidArgumentException('You must provide at least one search term (id, firstname, lastname, or email).'); + } } public function getId(): ?string diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index 0f497def9..9bb7a50ca 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -17,6 +17,7 @@ use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Query\TextFieldParameter; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Path\IdParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Header\ContentDisposition; @@ -56,11 +57,15 @@ public function __construct( summary: 'gdpr_export_summary', tags: [Tags::GDPR->value] )] + #[IdParameter( + name: 'id', + required: true, + )] #[TextFieldParameter( name: 'providerKey', description: 'The key of the single provider to export', required: true, - example: 'pimcore_user' + example: 'pimcore_users' )] #[SuccessResponse( description: 'gdpr_export_success_response', diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index 9ed252405..1132ccfb4 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -12,13 +12,14 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; +use Pimcore\Db; +use Pimcore\Model\User; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Model\User\Listing; -use function count; /** * @internal @@ -34,8 +35,6 @@ public function findData(SearchTerms $terms): array $conditionParts = []; $params = []; - $conditionParts[] = "`type` = 'user'"; - if ($terms->id !== null) { $conditionParts[] = 'id = ?'; $params[] = $terms->id; @@ -53,13 +52,7 @@ public function findData(SearchTerms $terms): array $params[] = '%' . $terms->email . '%'; } - // If we have conditions, apply them. - if (count($conditionParts) > 1) { - $listing->setCondition(implode(' AND ', $conditionParts), $params); - } else { - // Only the "type = 'user'" condition exists - $listing->setCondition($conditionParts[0]); - } + $listing->setCondition(implode(' AND ', $conditionParts), $params); $users = $listing->getUsers(); @@ -86,7 +79,7 @@ public function findData(SearchTerms $terms): array public function getSingleItemForDownload(int $id): array { $listing = new Listing(); - $listing->setCondition('id = ? AND `type` = ?', [$id, 'user']); + $listing->setCondition('id = ?', [$id]); $listing->setLimit(1); $users = $listing->getUsers(); @@ -97,15 +90,63 @@ public function getSingleItemForDownload(int $id): array $user = $users[0]; - return [ + $userData = [ 'id' => $user->getId(), 'name' => $user->getName(), 'firstname' => $user->getFirstname(), 'lastname' => $user->getLastname(), 'email' => $user->getEmail(), + 'versions' => $this->getVersionDataForUser($user), + 'usageLog' => $this->getUsageLogDataForUser($user), ]; + + return $userData; + } + + protected function getVersionDataForUser(User\AbstractUser $user): array + { + $db = Db::get(); + $versions = $db->fetchAllAssociative("SELECT ctype, cid, note, FROM_UNIXTIME(`date`) AS 'date' FROM versions WHERE userId = ?", [$user->getId()]); + + return $versions; } + protected function getUsageLogDataForUser(User\AbstractUser $user): array + { + $logsDir = PIMCORE_PROJECT_ROOT . '/var/log'; + + $pattern = ' [' . $user->getId() . ','; + $matches = []; + + $handle = @fopen($logsDir . '/usage.log', 'r'); + if ($handle) { + while (!feof($handle)) { + $buffer = fgets($handle); + if ($buffer && strpos($buffer, $pattern) !== false) { + $matches[] = $buffer; + } + } + fclose($handle); + } + + $archiveFiles = glob($logsDir . '/usage-archive-*.log.gz'); + foreach ($archiveFiles as $archiveFile) { + $handle = @gzopen($archiveFile, 'r'); + if ($handle) { + while (!feof($handle)) { + $buffer = fgets($handle); + if (strpos($buffer, $pattern) !== false) { + $matches[] = $buffer; + } + } + fclose($handle); + } + } + + return $matches; + } + + public function getName(): string { return 'Pimcore Users'; diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 7869714b0..72a74f04d 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -93,7 +93,7 @@ public function getExportDataAsJson(int $id, string $providerKey): StreamedRespo $this->checkProviderPermission($provider); - $data = $provider->getSingleItemForDownload($id); //id is a single item of a particular provider + $data = $provider->getSingleItemForDownload($id); return $this->createExportResponse($data, $providerKey, $id); } @@ -140,9 +140,6 @@ private function getSearchResultCollection(array $results): GdprSearchResultColl return $collection; } - /** - * Helper to create the export response, dispatch event, and stream data. - */ private function createExportResponse(mixed $data, string $providerKey, int $id): StreamedResponse { try { @@ -185,7 +182,6 @@ function () use ($jsonData) { */ private function sortProviders(array $providers): array { - // Higher number = Higher priority. uasort($providers, static fn (DataProviderInterface $a, DataProviderInterface $b): int => $b->getSortPriority() <=> $a->getSortPriority() ); @@ -201,7 +197,6 @@ private function checkProviderPermission(DataProviderInterface $provider): void $currentUser = $this->securityService->getCurrentUser(); $permissions = $provider->getRequiredPermissions(); - // Check if user has at least one of the required permissions in order to access the provider $isGranted = false; foreach ($permissions as $permission) { From a7ddb1965223cfb087d564655bbedd819bf9df72 Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:54:45 +0000 Subject: [PATCH 25/34] Apply php-cs-fixer changes --- src/Gdpr/Attribute/Request/SearchTerms.php | 3 +-- src/Gdpr/Controller/ExportController.php | 2 +- src/Gdpr/Provider/PimcoreUserProvider.php | 5 ++--- src/Gdpr/Service/GdprManagerService.php | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Gdpr/Attribute/Request/SearchTerms.php b/src/Gdpr/Attribute/Request/SearchTerms.php index 49246181f..30de69c97 100644 --- a/src/Gdpr/Attribute/Request/SearchTerms.php +++ b/src/Gdpr/Attribute/Request/SearchTerms.php @@ -16,7 +16,6 @@ use OpenApi\Attributes\Property; use OpenApi\Attributes\Schema; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException; -use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Type; /** @@ -44,7 +43,7 @@ public function __construct( #[Property(description: 'The email address to search for.', type: 'string', nullable: true)] #[Type('string')] - public ?string $email = null, + public ?string $email = null, ) { if ($this->id === null && $this->firstname === null && diff --git a/src/Gdpr/Controller/ExportController.php b/src/Gdpr/Controller/ExportController.php index 9bb7a50ca..7022230a2 100644 --- a/src/Gdpr/Controller/ExportController.php +++ b/src/Gdpr/Controller/ExportController.php @@ -16,8 +16,8 @@ use OpenApi\Attributes\Get; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\GdprManagerServiceInterface; -use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Query\TextFieldParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Path\IdParameter; +use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Query\TextFieldParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\MediaType; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\DefaultResponses; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Header\ContentDisposition; diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index 1132ccfb4..fcbcec065 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -12,13 +12,13 @@ namespace Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider; -use Pimcore\Db; -use Pimcore\Model\User; use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotFoundException; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; +use Pimcore\Db; +use Pimcore\Model\User; use Pimcore\Model\User\Listing; /** @@ -146,7 +146,6 @@ protected function getUsageLogDataForUser(User\AbstractUser $user): array return $matches; } - public function getName(): string { return 'Pimcore Users'; diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 72a74f04d..4a57c70d7 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -93,7 +93,7 @@ public function getExportDataAsJson(int $id, string $providerKey): StreamedRespo $this->checkProviderPermission($provider); - $data = $provider->getSingleItemForDownload($id); + $data = $provider->getSingleItemForDownload($id); return $this->createExportResponse($data, $providerKey, $id); } From 7b118c72979963d7ccf1680563f3f586d688752d Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 19 Nov 2025 18:16:28 +0000 Subject: [PATCH 26/34] fixed sonar cloud issue --- src/Gdpr/Attribute/Request/SearchTerms.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gdpr/Attribute/Request/SearchTerms.php b/src/Gdpr/Attribute/Request/SearchTerms.php index 30de69c97..b9de9525e 100644 --- a/src/Gdpr/Attribute/Request/SearchTerms.php +++ b/src/Gdpr/Attribute/Request/SearchTerms.php @@ -50,7 +50,7 @@ public function __construct( $this->lastname === null && $this->email === null ) { - throw new InvalidArgumentException('You must provide at least one search term (id, firstname, lastname, or email).'); + throw new InvalidArgumentException('Provide at least one search term.'); } } From 3aff281cdecad442f43af4302b77c166f894ad80 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Wed, 19 Nov 2025 18:23:12 +0000 Subject: [PATCH 27/34] fix sonar cloud issue --- src/Gdpr/Provider/PimcoreUserProvider.php | 42 +++++++++++++++-------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index fcbcec065..aee84f639 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -106,7 +106,11 @@ public function getSingleItemForDownload(int $id): array protected function getVersionDataForUser(User\AbstractUser $user): array { $db = Db::get(); - $versions = $db->fetchAllAssociative("SELECT ctype, cid, note, FROM_UNIXTIME(`date`) AS 'date' FROM versions WHERE userId = ?", [$user->getId()]); + $versions = $db->fetchAllAssociative( + "SELECT ctype, cid, note, FROM_UNIXTIME(`date`) AS 'date' + FROM versions + WHERE userId = ?", [$user->getId()] + ); return $versions; } @@ -118,7 +122,19 @@ protected function getUsageLogDataForUser(User\AbstractUser $user): array $pattern = ' [' . $user->getId() . ','; $matches = []; - $handle = @fopen($logsDir . '/usage.log', 'r'); + $this->readPlainFile($logsDir . '/usage.log', $pattern, $matches); + + $archiveFiles = glob($logsDir . '/usage-archive-*.log.gz'); + foreach ($archiveFiles as $archiveFile) { + $this->readGzFile($archiveFile, $pattern, $matches); + } + + return $matches; + } + + private function readPlainFile(string $file, string $pattern, array &$matches): void + { + $handle = @fopen($file, 'r'); if ($handle) { while (!feof($handle)) { $buffer = fgets($handle); @@ -128,22 +144,20 @@ protected function getUsageLogDataForUser(User\AbstractUser $user): array } fclose($handle); } + } - $archiveFiles = glob($logsDir . '/usage-archive-*.log.gz'); - foreach ($archiveFiles as $archiveFile) { - $handle = @gzopen($archiveFile, 'r'); - if ($handle) { - while (!feof($handle)) { - $buffer = fgets($handle); - if (strpos($buffer, $pattern) !== false) { - $matches[] = $buffer; - } + private function readGzFile(string $file, string $pattern, array &$matches): void + { + $handle = @gzopen($file, 'r'); + if ($handle) { + while (!feof($handle)) { + $buffer = fgets($handle); + if ($buffer && strpos($buffer, $pattern) !== false) { + $matches[] = $buffer; } - fclose($handle); } + fclose($handle); } - - return $matches; } public function getName(): string From e23d98ff16c01cf5011f67563ad38392e8710299 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Thu, 20 Nov 2025 09:06:06 +0000 Subject: [PATCH 28/34] changes and ,sonar fix --- config/gdpr.yaml | 3 ++ src/Gdpr/Provider/PimcoreUserProvider.php | 48 +++++++++++++---------- src/Gdpr/Service/GdprManagerService.php | 6 ++- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/config/gdpr.yaml b/config/gdpr.yaml index 73f760b63..be17c5704 100644 --- a/config/gdpr.yaml +++ b/config/gdpr.yaml @@ -20,5 +20,8 @@ services: class: Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\Loader\TaggedIteratorDataProviderLoader # --- GDPR Providers --- + Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\PimcoreUserProvider: tags: ["pimcore.studio_backend.gdpr_data_provider"] + arguments: + $logsDir: "%kernel.logs_dir%" diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index aee84f639..1eb3984c1 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -29,6 +29,13 @@ /** * {@inheritdoc} */ + private string $logsDir; + + public function __construct(string $logsDir) + { + $this->logsDir = $logsDir; + } + public function findData(SearchTerms $terms): array { $listing = new Listing(); @@ -66,6 +73,8 @@ public function findData(SearchTerms $terms): array 'firstname' => $user->getFirstname(), 'lastname' => $user->getLastname(), 'email' => $user->getEmail(), + 'versions' => $this->getVersionDataForUser($user), + 'usageLog' => $this->getUsageLogDataForUser($user), ], $columns ), @@ -90,41 +99,36 @@ public function getSingleItemForDownload(int $id): array $user = $users[0]; - $userData = [ - 'id' => $user->getId(), - 'name' => $user->getName(), - 'firstname' => $user->getFirstname(), - 'lastname' => $user->getLastname(), - 'email' => $user->getEmail(), - 'versions' => $this->getVersionDataForUser($user), - 'usageLog' => $this->getUsageLogDataForUser($user), - ]; - return $userData; + return [ + 'id' => $user->getId(), + 'name' => $user->getName(), + 'firstname' => $user->getFirstname(), + 'lastname' => $user->getLastname(), + 'email' => $user->getEmail(), + 'versions' => $this->getVersionDataForUser($user), + 'usageLog' => $this->getUsageLogDataForUser($user), + ]; } protected function getVersionDataForUser(User\AbstractUser $user): array { - $db = Db::get(); - $versions = $db->fetchAllAssociative( - "SELECT ctype, cid, note, FROM_UNIXTIME(`date`) AS 'date' - FROM versions - WHERE userId = ?", [$user->getId()] + return Db::get()->fetchAllAssociative( + "SELECT ctype, cid, note, FROM_UNIXTIME(`date`) AS 'date' + FROM versions + WHERE userId = ?", + [$user->getId()] ); - - return $versions; } protected function getUsageLogDataForUser(User\AbstractUser $user): array { - $logsDir = PIMCORE_PROJECT_ROOT . '/var/log'; - $pattern = ' [' . $user->getId() . ','; $matches = []; - $this->readPlainFile($logsDir . '/usage.log', $pattern, $matches); + $this->readPlainFile($this->logsDir . '/usage.log', $pattern, $matches); - $archiveFiles = glob($logsDir . '/usage-archive-*.log.gz'); + $archiveFiles = glob($this->logsDir . '/usage-archive-*.log.gz'); foreach ($archiveFiles as $archiveFile) { $this->readGzFile($archiveFile, $pattern, $matches); } @@ -194,6 +198,8 @@ public function getAvailableColumns(): array new GdprDataColumn('firstname', 'First Name'), new GdprDataColumn('lastname', 'Last Name'), new GdprDataColumn('email', 'Email'), + new GdprDataColumn('versions', 'Versions'), + new GdprDataColumn('usageLog', 'Usage Log'), ]; } } diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 4a57c70d7..1742f6a04 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -182,8 +182,10 @@ function () use ($jsonData) { */ private function sortProviders(array $providers): array { - uasort($providers, static fn (DataProviderInterface $a, DataProviderInterface $b): int - => $b->getSortPriority() <=> $a->getSortPriority() + uasort( + $providers, + static fn (DataProviderInterface $a, DataProviderInterface $b): int => + $b->getSortPriority() <=> $a->getSortPriority() ); return $providers; From 56486c4b8cb1028d905012ff6d734e0c909842f1 Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:09:34 +0000 Subject: [PATCH 29/34] Apply php-cs-fixer changes --- src/Gdpr/Provider/PimcoreUserProvider.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index 1eb3984c1..938d5b411 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -29,7 +29,7 @@ /** * {@inheritdoc} */ - private string $logsDir; + private string $logsDir; public function __construct(string $logsDir) { @@ -99,7 +99,6 @@ public function getSingleItemForDownload(int $id): array $user = $users[0]; - return [ 'id' => $user->getId(), 'name' => $user->getName(), From 0f3a3947db6c235ccfbc7057fb3e0b39ebe9d419 Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Thu, 20 Nov 2025 14:59:27 +0000 Subject: [PATCH 30/34] set the user that is non deletable --- src/Gdpr/Provider/DataProviderInterface.php | 2 + src/Gdpr/Provider/PimcoreUserProvider.php | 59 ++++++++++++--------- src/Gdpr/Schema/GdprDataProvider.php | 12 +++++ src/Gdpr/Service/GdprManagerService.php | 1 + 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/Gdpr/Provider/DataProviderInterface.php b/src/Gdpr/Provider/DataProviderInterface.php index 4c12a556c..d546101ad 100644 --- a/src/Gdpr/Provider/DataProviderInterface.php +++ b/src/Gdpr/Provider/DataProviderInterface.php @@ -26,6 +26,8 @@ interface DataProviderInterface */ public function findData(SearchTerms $terms): array; + public function getDeleteSwaggerOperationId(): string; + public function getName(): string; public function getKey(): string; diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index 938d5b411..d7058abfd 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -17,6 +17,7 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; +use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; use Pimcore\Db; use Pimcore\Model\User; use Pimcore\Model\User\Listing; @@ -26,43 +27,50 @@ */ final readonly class PimcoreUserProvider implements DataProviderInterface { + public function __construct( + private readonly string $logsDir, + private readonly SecurityServiceInterface $securityService + ) { + + } + /** * {@inheritdoc} */ - private string $logsDir; - - public function __construct(string $logsDir) - { - $this->logsDir = $logsDir; - } - public function findData(SearchTerms $terms): array { $listing = new Listing(); - $conditionParts = []; - $params = []; if ($terms->id !== null) { - $conditionParts[] = 'id = ?'; - $params[] = $terms->id; + $listing->addConditionParam( + 'id = :id', + ['id' => $terms->id] + ); } + if ($terms->firstname !== null) { - $conditionParts[] = 'firstname LIKE ?'; - $params[] = '%' . $terms->firstname . '%'; + $listing->addConditionParam( + 'firstname LIKE :firstname', + ['firstname' => '%' . $terms->firstname . '%'] + ); } + if ($terms->lastname !== null) { - $conditionParts[] = 'lastname LIKE ?'; - $params[] = '%' . $terms->lastname . '%'; + $listing->addConditionParam( + 'lastname LIKE :lastname', + ['lastname' => '%' . $terms->lastname . '%'] + ); } + if ($terms->email !== null) { - $conditionParts[] = 'email LIKE ?'; - $params[] = '%' . $terms->email . '%'; + $listing->addConditionParam( + 'email LIKE :email', + ['email' => '%' . $terms->email . '%'] + ); } - $listing->setCondition(implode(' AND ', $conditionParts), $params); - $users = $listing->getUsers(); - + $columns = $this->getAvailableColumns(); return array_map( @@ -73,8 +81,7 @@ public function findData(SearchTerms $terms): array 'firstname' => $user->getFirstname(), 'lastname' => $user->getLastname(), 'email' => $user->getEmail(), - 'versions' => $this->getVersionDataForUser($user), - 'usageLog' => $this->getUsageLogDataForUser($user), + '__gdprIsDeletable' => $user->getId() != $this->securityService->getCurrentUser()->getId(), ], $columns ), @@ -82,6 +89,11 @@ public function findData(SearchTerms $terms): array ); } + public function getDeleteSwaggerOperationId(): string + { + return 'user_delete_by_id'; + } + /** * {@inheritdoc} */ @@ -197,8 +209,7 @@ public function getAvailableColumns(): array new GdprDataColumn('firstname', 'First Name'), new GdprDataColumn('lastname', 'Last Name'), new GdprDataColumn('email', 'Email'), - new GdprDataColumn('versions', 'Versions'), - new GdprDataColumn('usageLog', 'Usage Log'), + new GdprDataColumn('__gdprIsDeletable', 'Is Deletable') ]; } } diff --git a/src/Gdpr/Schema/GdprDataProvider.php b/src/Gdpr/Schema/GdprDataProvider.php index 9dbd256b3..65994f25f 100644 --- a/src/Gdpr/Schema/GdprDataProvider.php +++ b/src/Gdpr/Schema/GdprDataProvider.php @@ -50,6 +50,13 @@ public function __construct( )] private readonly string $label, + #[Property( + description: 'The Operation ID to call when deleting an item.', + type: 'string', + example: 'user_delete_by_id' + )] + private readonly string $deleteOperationId, + #[Property( description: 'List of column definitions for the result grid', type: 'array', @@ -69,6 +76,11 @@ public function getLabel(): string return $this->label; } + public function getDeleteOperationId(): string + { + return $this->deleteOperationId; + } + /** * @return GdprDataColumn[] */ diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 1742f6a04..0f8df73f0 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -111,6 +111,7 @@ private function getDataProviderCollection(array $providers): Collection $item = new GdprDataProvider( key: $key, label: $provider->getName(), + deleteOperationId: $provider->getDeleteSwaggerOperationId(), columns: $provider->getAvailableColumns(), ); From 8e65beddc0f8e9c97f6dee82357f12ae9021077d Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Thu, 20 Nov 2025 15:02:22 +0000 Subject: [PATCH 31/34] update docs --- doc/10_Extending_Studio/11_Gdpr.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/10_Extending_Studio/11_Gdpr.md b/doc/10_Extending_Studio/11_Gdpr.md index d64577589..e0425a7dd 100644 --- a/doc/10_Extending_Studio/11_Gdpr.md +++ b/doc/10_Extending_Studio/11_Gdpr.md @@ -20,6 +20,7 @@ This flow happens when a user opens the GDPR Data Extractor page and clicks "Sea - `getAvailableColumns()`: To build the columns for the search results grid. - `getRequiredPermissions()`: One or more permissions required by user to access the data provider information - `findData()`: Find the data in the particular provider using the searched terms + - `getDeleteSwaggerOperationId()`: The Operation ID to call to delete item 2. **When the user clicks "Search":** - The system first calls your `getRequiredPermissions()` method to check if the current user is allowed to use your provider. From 95008c029788360553aaa1a4d2ee2a8098bac39a Mon Sep 17 00:00:00 2001 From: stunnerparas <49896041+stunnerparas@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:03:29 +0000 Subject: [PATCH 32/34] Apply php-cs-fixer changes --- src/Gdpr/Provider/PimcoreUserProvider.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index d7058abfd..429a9145d 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -16,8 +16,8 @@ use Pimcore\Bundle\StudioBackendBundle\Gdpr\Attribute\Request\SearchTerms; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn; use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataRow; -use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions; use Pimcore\Db; use Pimcore\Model\User; use Pimcore\Model\User\Listing; @@ -32,7 +32,7 @@ public function __construct( private readonly SecurityServiceInterface $securityService ) { - } + } /** * {@inheritdoc} @@ -52,7 +52,7 @@ public function findData(SearchTerms $terms): array $listing->addConditionParam( 'firstname LIKE :firstname', ['firstname' => '%' . $terms->firstname . '%'] - ); + ); } if ($terms->lastname !== null) { @@ -63,14 +63,14 @@ public function findData(SearchTerms $terms): array } if ($terms->email !== null) { - $listing->addConditionParam( - 'email LIKE :email', - ['email' => '%' . $terms->email . '%'] - ); + $listing->addConditionParam( + 'email LIKE :email', + ['email' => '%' . $terms->email . '%'] + ); } $users = $listing->getUsers(); - + $columns = $this->getAvailableColumns(); return array_map( @@ -209,7 +209,7 @@ public function getAvailableColumns(): array new GdprDataColumn('firstname', 'First Name'), new GdprDataColumn('lastname', 'Last Name'), new GdprDataColumn('email', 'Email'), - new GdprDataColumn('__gdprIsDeletable', 'Is Deletable') + new GdprDataColumn('__gdprIsDeletable', 'Is Deletable'), ]; } } From 2e5e175b1ddb3ef2a2d34f9a7cf7d432e6ecb85d Mon Sep 17 00:00:00 2001 From: Martin Eiber Date: Fri, 21 Nov 2025 07:24:59 +0100 Subject: [PATCH 33/34] Minor Changes. --- src/Gdpr/Provider/PimcoreUserProvider.php | 4 ++-- src/Gdpr/Service/GdprManagerService.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Gdpr/Provider/PimcoreUserProvider.php b/src/Gdpr/Provider/PimcoreUserProvider.php index 429a9145d..d5acfd810 100644 --- a/src/Gdpr/Provider/PimcoreUserProvider.php +++ b/src/Gdpr/Provider/PimcoreUserProvider.php @@ -28,8 +28,8 @@ final readonly class PimcoreUserProvider implements DataProviderInterface { public function __construct( - private readonly string $logsDir, - private readonly SecurityServiceInterface $securityService + private string $logsDir, + private SecurityServiceInterface $securityService ) { } diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php index 0f8df73f0..73125fcd9 100644 --- a/src/Gdpr/Service/GdprManagerService.php +++ b/src/Gdpr/Service/GdprManagerService.php @@ -144,7 +144,7 @@ private function getSearchResultCollection(array $results): GdprSearchResultColl private function createExportResponse(mixed $data, string $providerKey, int $id): StreamedResponse { try { - $jsonData = json_encode($data, JSON_THROW_ON_ERROR); + $jsonData = json_encode($data, JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT); } catch (JsonException $e) { throw new InvalidArgumentException( sprintf( From 9842536718f91804da23cb285488fcb37ed7e87c Mon Sep 17 00:00:00 2001 From: stunnerparas Date: Fri, 21 Nov 2025 10:00:45 +0000 Subject: [PATCH 34/34] update docs --- doc/10_Extending_Studio/11_Gdpr.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/10_Extending_Studio/11_Gdpr.md b/doc/10_Extending_Studio/11_Gdpr.md index e0425a7dd..9d047993a 100644 --- a/doc/10_Extending_Studio/11_Gdpr.md +++ b/doc/10_Extending_Studio/11_Gdpr.md @@ -38,6 +38,16 @@ This flow happens when a user has already searched and clicks the "Export" butto --- +### For Deleting an Item + +This flow happens when a user clicks "Delete" on a result row. + +Instead of handling deletion logic inside the provider, you simply **point** to the correct API endpoint. + +1. You implement `getDeleteSwaggerOperationId()`. +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. + ## Example Data Provider Example below shows some of the important functions with their implementations @@ -84,6 +94,10 @@ final class UserCreatedDataProvider implements DataProviderInterface //return $results; } + public function getDeleteSwaggerOperationId(): string + { + return 'data_provider_delete_by_operation_id'; + } public function getSingleItemForDownload(int $id): array|object {