diff --git a/config/gdpr.yaml b/config/gdpr.yaml new file mode 100644 index 000000000..be17c5704 --- /dev/null +++ b/config/gdpr.yaml @@ -0,0 +1,27 @@ +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\PimcoreUserProvider: + tags: ["pimcore.studio_backend.gdpr_data_provider"] + arguments: + $logsDir: "%kernel.logs_dir%" diff --git a/doc/05_Additional_Custom_Attributes.md b/doc/05_Additional_Custom_Attributes.md index 31cc5fb0f..16d138bd6 100644 --- a/doc/05_Additional_Custom_Attributes.md +++ b/doc/05_Additional_Custom_Attributes.md @@ -158,5 +158,7 @@ final class AssetEvent extends AbstractPreResponseEvent - `pre_response.workflow_details` - `pre_response.notification_recipient` - `pre_response.php_code_transformer` +- `pre_response.data_provider` +- `pre_response.gdpr_search_result` - `pre_response.element.usage.item` -- `pre_response.element.usage` \ No newline at end of file +- `pre_response.element.usage` diff --git a/doc/10_Extending_Studio/11_Gdpr.md b/doc/10_Extending_Studio/11_Gdpr.md new file mode 100644 index 000000000..9d047993a --- /dev/null +++ b/doc/10_Extending_Studio/11_Gdpr.md @@ -0,0 +1,108 @@ +# 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 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 + +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 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. + - `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. + - 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) + +This flow happens when a user has already searched and clicks the "Export" button on a single item in your results grid. + +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. + +--- + +### 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 + +```php + +final class UserCreatedDataProvider implements DataProviderInterface +{ + public function getKey(): string + { + return 'key_value'; + } + + public function getName(): string + { + return 'Data Provider Name'; + } + + public function getSortPriority(): int + { + return 10;//set the priority of provider + } + + /** + * @return string[] + */ + public function getRequiredPermissions(): array + { + // Return an array of permission strings + return ['permission 1', 'permission 2'];//example : UserPermissions::USERS->value + } + + public function getAvailableColumns(): array + { + return [ + new GdprDataColumn('column1', 'Column 1 Value'), + new GdprDataColumn('column2', 'Column 2 Value'), + ]; + } + + public function findData(?SearchTerms $terms): array + { + //Find user data using input $terms + + //return $results; + } + public function getDeleteSwaggerOperationId(): string + { + return 'data_provider_delete_by_operation_id'; + } + + public function getSingleItemForDownload(int $id): array|object + { + // return single Item of a Data Provider + } +} + +``` 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..52585055f --- /dev/null +++ b/src/Gdpr/Attribute/Request/GdprRequestBody.php @@ -0,0 +1,57 @@ +id === null && + $this->firstname === null && + $this->lastname === null && + $this->email === null + ) { + throw new InvalidArgumentException('Provide at least one search term.'); + } + } + + public function getId(): ?string + { + return $this->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/ExportController.php b/src/Gdpr/Controller/ExportController.php new file mode 100644 index 000000000..7022230a2 --- /dev/null +++ b/src/Gdpr/Controller/ExportController.php @@ -0,0 +1,87 @@ + '\d+'], + methods: ['GET'] + )] + #[IsGranted(UserPermissions::GDPR->value)] + #[Get( + path: self::PREFIX . '/gdpr/export-data/{id}', + operationId: 'gdpr_export', + description: 'gdpr_export_description', + 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_users' + )] + #[SuccessResponse( + description: 'gdpr_export_success_response', + content: new MediaType('application/json'), + headers: [new ContentDisposition('inline')] + )] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::FORBIDDEN, + HttpResponseCodes::BAD_REQUEST, + HttpResponseCodes::NOT_FOUND, + ])] + public function startExport( + int $id, + #[MapQueryParameter] string $providerKey + ): StreamedResponse { + return $this->gdprManagerService->getExportDataAsJson($id, $providerKey); + } +} diff --git a/src/Gdpr/Controller/GetDataProviderController.php b/src/Gdpr/Controller/GetDataProviderController.php new file mode 100644 index 000000000..35b334916 --- /dev/null +++ b/src/Gdpr/Controller/GetDataProviderController.php @@ -0,0 +1,85 @@ +value)] + #[GET( + path: self::PREFIX . '/gdpr/providers', + operationId: 'gdpr_list_providers', + description: 'gdpr_list_providers_description', + summary: 'gdpr_list_providers_summary', + 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..a64744f32 --- /dev/null +++ b/src/Gdpr/Controller/SearchDataProviderController.php @@ -0,0 +1,87 @@ +value)] + #[POST( + path: self::PREFIX . '/gdpr/search', + operationId: 'gdpr_search_data', + description: 'gdpr_search_data_description', + summary: 'gdpr_search_data_summary', + tags: [Tags::GDPR->value] + )] + #[GdprRequestBody] + #[SuccessResponse( + description: 'gdpr_search_data_success_response', + content: new CollectionJson( + collection: new GdprSearchResultProperty() + ) + )] + + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::FORBIDDEN, + HttpResponseCodes::NOT_FOUND, + HttpResponseCodes::BAD_REQUEST, + HttpResponseCodes::UNPROCESSABLE_CONTENT, + ])] + 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..1b2954a06 --- /dev/null +++ b/src/Gdpr/Event/PreResponse/GdprDataProviderEvent.php @@ -0,0 +1,32 @@ +provider); + } + + public function getProvider(): GdprDataProvider + { + return $this->provider; + } +} diff --git a/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php new file mode 100644 index 000000000..ed368c643 --- /dev/null +++ b/src/Gdpr/Event/PreResponse/GdprSearchResultEvent.php @@ -0,0 +1,32 @@ +collection); + } + + public function getCollection(): GdprSearchResultCollection + { + return $this->collection; + } +} diff --git a/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php new file mode 100644 index 000000000..32d92fb40 --- /dev/null +++ b/src/Gdpr/MappedParameter/GdprStructuredSearchRequest.php @@ -0,0 +1,41 @@ +id !== null) { + $listing->addConditionParam( + 'id = :id', + ['id' => $terms->id] + ); + } + + if ($terms->firstname !== null) { + $listing->addConditionParam( + 'firstname LIKE :firstname', + ['firstname' => '%' . $terms->firstname . '%'] + ); + } + + if ($terms->lastname !== null) { + $listing->addConditionParam( + 'lastname LIKE :lastname', + ['lastname' => '%' . $terms->lastname . '%'] + ); + } + + if ($terms->email !== null) { + $listing->addConditionParam( + 'email LIKE :email', + ['email' => '%' . $terms->email . '%'] + ); + } + + $users = $listing->getUsers(); + + $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(), + '__gdprIsDeletable' => $user->getId() != $this->securityService->getCurrentUser()->getId(), + ], + $columns + ), + $users + ); + } + + public function getDeleteSwaggerOperationId(): string + { + return 'user_delete_by_id'; + } + + /** + * {@inheritdoc} + */ + public function getSingleItemForDownload(int $id): array + { + $listing = new Listing(); + $listing->setCondition('id = ?', [$id]); + $listing->setLimit(1); + + $users = $listing->getUsers(); + + if (empty($users)) { + throw new NotFoundException('Pimcore User', $id); + } + + $user = $users[0]; + + 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 + { + return Db::get()->fetchAllAssociative( + "SELECT ctype, cid, note, FROM_UNIXTIME(`date`) AS 'date' + FROM versions + WHERE userId = ?", + [$user->getId()] + ); + } + + protected function getUsageLogDataForUser(User\AbstractUser $user): array + { + $pattern = ' [' . $user->getId() . ','; + $matches = []; + + $this->readPlainFile($this->logsDir . '/usage.log', $pattern, $matches); + + $archiveFiles = glob($this->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); + if ($buffer && strpos($buffer, $pattern) !== false) { + $matches[] = $buffer; + } + } + fclose($handle); + } + } + + 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); + } + } + + public function getName(): string + { + return 'Pimcore Users'; + } + + public function getKey(): string + { + return 'pimcore_users'; + } + + public function getSortPriority(): int + { + return 5; + } + + /** + * {@inheritdoc} + */ + public function getRequiredPermissions(): array + { + return [UserPermissions::PIMCORE_USER->value]; + } + + /** + * {@inheritdoc} + */ + 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'), + new GdprDataColumn('__gdprIsDeletable', 'Is Deletable'), + ]; + } +} diff --git a/src/Gdpr/Schema/GdprDataColumn.php b/src/Gdpr/Schema/GdprDataColumn.php new file mode 100644 index 000000000..cde950828 --- /dev/null +++ b/src/Gdpr/Schema/GdprDataColumn.php @@ -0,0 +1,56 @@ +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..65994f25f --- /dev/null +++ b/src/Gdpr/Schema/GdprDataProvider.php @@ -0,0 +1,91 @@ + $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: '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', + items: new Items(ref: GdprDataColumn::class) + )] + private readonly array $columns, + ) { + } + + public function getKey(): string + { + return $this->key; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getDeleteOperationId(): string + { + return $this->deleteOperationId; + } + + /** + * @return GdprDataColumn[] + */ + public function getColumns(): array + { + return $this->columns; + } +} diff --git a/src/Gdpr/Schema/GdprDataRow.php b/src/Gdpr/Schema/GdprDataRow.php new file mode 100644 index 000000000..989465e54 --- /dev/null +++ b/src/Gdpr/Schema/GdprDataRow.php @@ -0,0 +1,45 @@ + $data + * @param GdprDataColumn[] $availableColumns + */ + public function __construct( + private array $data, + 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; + } +} diff --git a/src/Gdpr/Schema/GdprSearchResult.php b/src/Gdpr/Schema/GdprSearchResult.php new file mode 100644 index 000000000..97ce2411b --- /dev/null +++ b/src/Gdpr/Schema/GdprSearchResult.php @@ -0,0 +1,63 @@ +providerKey; + } + + /** + * @return GdprDataRow[] + */ + public function getResults(): array + { + return $this->results; + } +} diff --git a/src/Gdpr/Schema/GdprSearchResultCollection.php b/src/Gdpr/Schema/GdprSearchResultCollection.php new file mode 100644 index 000000000..7033a253e --- /dev/null +++ b/src/Gdpr/Schema/GdprSearchResultCollection.php @@ -0,0 +1,57 @@ + $items + */ + public function __construct( + #[Property( + description: 'List of search results, grouped by provider', + type: 'array', + items: new Items(ref: GdprSearchResult::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 new file mode 100644 index 000000000..e0ec41269 --- /dev/null +++ b/src/Gdpr/Schema/GdprSearchResultProperty.php @@ -0,0 +1,49 @@ + + */ + public function getDataProviders(): array; + + /** + * @throws NotFoundException + */ + public function resolve(string $key): DataProviderInterface; +} diff --git a/src/Gdpr/Service/GdprManagerService.php b/src/Gdpr/Service/GdprManagerService.php new file mode 100644 index 000000000..73125fcd9 --- /dev/null +++ b/src/Gdpr/Service/GdprManagerService.php @@ -0,0 +1,223 @@ +sortProviders($this->loader->getDataProviders()); + + return $this->getDataProviderCollection($providers); + } + + /** + * {@inheritdoc} + */ + public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection + { + $allResults = []; + + foreach ($request->providers as $providerKey) { + $provider = $this->loader->resolve($providerKey); + + $this->checkProviderPermission($provider); + + $results = $provider->findData($request->searchTerms); + + if (!empty($results)) { + $allResults[] = new GdprSearchResult( + providerKey: $providerKey, + results: $results + ); + } + } + + return $this->getSearchResultCollection($allResults); + + } + + /** + * {@inheritdoc} + */ + public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse + { + $provider = $this->loader->resolve($providerKey); + + $this->checkProviderPermission($provider); + + $data = $provider->getSingleItemForDownload($id); + + return $this->createExportResponse($data, $providerKey, $id); + } + + /** + * @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(), + deleteOperationId: $provider->getDeleteSwaggerOperationId(), + columns: $provider->getAvailableColumns(), + ); + + $this->eventDispatcher->dispatch( + new GdprDataProviderEvent($item), + GdprDataProviderEvent::EVENT_NAME + ); + + $items[] = $item; + } + + return new Collection(count($items), $items); + } + + /** + * @param array $results + */ + private function getSearchResultCollection(array $results): GdprSearchResultCollection + { + $collection = new GdprSearchResultCollection($results); + + $this->eventDispatcher->dispatch( + new GdprSearchResultEvent($collection), + GdprSearchResultEvent::EVENT_NAME + ); + + return $collection; + } + + private function createExportResponse(mixed $data, string $providerKey, int $id): StreamedResponse + { + try { + $jsonData = json_encode($data, JSON_THROW_ON_ERROR|JSON_PRETTY_PRINT); + } 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); + + $headers = $this->getResponseHeaders( + mimeType: 'application/json', + fileSize: $fileSize, + filename: $filename, + contentDisposition: HttpResponseHeaders::ATTACHMENT_TYPE->value + ); + + return new StreamedResponse( + function () use ($jsonData) { + echo $jsonData; + }, + HttpResponseCodes::SUCCESS->value, + $headers + ); + } + + /** + * @param array $providers + * + * @return array + */ + private function sortProviders(array $providers): array + { + uasort( + $providers, + static fn (DataProviderInterface $a, DataProviderInterface $b): int => + $b->getSortPriority() <=> $a->getSortPriority() + ); + + return $providers; + } + + /** + * @throws ForbiddenException + */ + private function checkProviderPermission(DataProviderInterface $provider): void + { + $currentUser = $this->securityService->getCurrentUser(); + $permissions = $provider->getRequiredPermissions(); + + $isGranted = false; + + 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) + ) + ); + } + } +} diff --git a/src/Gdpr/Service/GdprManagerServiceInterface.php b/src/Gdpr/Service/GdprManagerServiceInterface.php new file mode 100644 index 000000000..c8531e6d7 --- /dev/null +++ b/src/Gdpr/Service/GdprManagerServiceInterface.php @@ -0,0 +1,46 @@ + + */ + public function getAvailableProviders(): Collection; + + /** + * Searches for data in the specified providers. + * + * @throws ForbiddenException + */ + public function search(GdprStructuredSearchRequest $request): GdprSearchResultCollection; + + /** + * @throws ForbiddenException + * @throws NotFoundException + */ + public function getExportDataAsJson(int $id, string $providerKey): StreamedResponse; +} diff --git a/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php b/src/Gdpr/Service/Loader/TaggedIteratorDataProviderLoader.php new file mode 100644 index 000000000..d210c39ec --- /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..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; @@ -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 a914afa3a..141cc04ac 100644 --- a/translations/studio_api_docs.en.yaml +++ b/translations/studio_api_docs.en.yaml @@ -1421,6 +1421,16 @@ 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 +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.