Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
10aca52
Progressing GDPR
stunnerparas Nov 10, 2025
d1e2af3
Apply php-cs-fixer changes
stunnerparas Nov 10, 2025
28fc4da
Addition of new manager function to run job and fixes
stunnerparas Nov 12, 2025
afe0939
Apply php-cs-fixer changes
stunnerparas Nov 12, 2025
86cf5e3
Static code analysis fixes
stunnerparas Nov 12, 2025
bfdbf3e
Apply php-cs-fixer changes
stunnerparas Nov 12, 2025
f481324
Addition fixes
stunnerparas Nov 12, 2025
8ab6810
Apply php-cs-fixer changes
stunnerparas Nov 12, 2025
c4bd9ea
Addition fixes
stunnerparas Nov 12, 2025
73f9b14
Apply php-cs-fixer changes
stunnerparas Nov 12, 2025
8654501
Addition fixes
stunnerparas Nov 12, 2025
8eefb6a
pimcore user and finalize manager service
stunnerparas Nov 13, 2025
0e2edf3
Apply php-cs-fixer changes
stunnerparas Nov 13, 2025
dd3a387
Merge branch '1.x' into 840_gdpr
stunnerparas Nov 13, 2025
51322c1
Additional changes and improvements
stunnerparas Nov 18, 2025
0343841
Merge branch '1.x' into 840_gdpr
stunnerparas Nov 18, 2025
db0a05c
changes
stunnerparas Nov 18, 2025
4b8c6ce
Apply php-cs-fixer changes
stunnerparas Nov 18, 2025
7cf7a93
sonar fixes
stunnerparas Nov 18, 2025
107468a
type fix docs
stunnerparas Nov 18, 2025
2028712
Apply php-cs-fixer changes
stunnerparas Nov 18, 2025
68712e5
Re update Json event, Handle json error, Parameter, schema, event han…
stunnerparas Nov 19, 2025
26d9554
Apply php-cs-fixer changes
stunnerparas Nov 19, 2025
5e5176e
Minor Changes.
martineiber Nov 19, 2025
4a0bb7e
Apply php-cs-fixer changes
martineiber Nov 19, 2025
0bc6f6c
Additional imporovements and new features added in json download
stunnerparas Nov 19, 2025
a7ddb19
Apply php-cs-fixer changes
stunnerparas Nov 19, 2025
7b118c7
fixed sonar cloud issue
stunnerparas Nov 19, 2025
3aff281
fix sonar cloud issue
stunnerparas Nov 19, 2025
e23d98f
changes and ,sonar fix
stunnerparas Nov 20, 2025
56486c4
Apply php-cs-fixer changes
stunnerparas Nov 20, 2025
0f3a394
set the user that is non deletable
stunnerparas Nov 20, 2025
8e65bed
update docs
stunnerparas Nov 20, 2025
95008c0
Apply php-cs-fixer changes
stunnerparas Nov 20, 2025
2e5e175
Minor Changes.
martineiber Nov 21, 2025
9842536
update docs
stunnerparas Nov 21, 2025
7cd6879
Merge branch '1.x' into 840_gdpr
stunnerparas Nov 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions config/gdpr.yaml
Original file line number Diff line number Diff line change
@@ -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%"
4 changes: 3 additions & 1 deletion doc/05_Additional_Custom_Attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
- `pre_response.element.usage`
108 changes: 108 additions & 0 deletions doc/10_Extending_Studio/11_Gdpr.md
Original file line number Diff line number Diff line change
@@ -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
}
}

```
46 changes: 46 additions & 0 deletions src/DependencyInjection/CompilerPass/DataProviderPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
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\DependencyInjection\CompilerPass;

use Pimcore\Bundle\StudioBackendBundle\Exception\MustImplementInterfaceException;
use Pimcore\Bundle\StudioBackendBundle\Gdpr\Provider\DataProviderInterface;
use Pimcore\Bundle\StudioBackendBundle\Gdpr\Service\Loader\TaggedIteratorDataProviderLoader;
use Pimcore\Bundle\StudioBackendBundle\Util\Trait\MustImplementInterfaceTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* @internal
*/
final readonly class DataProviderPass implements CompilerPassInterface
{
use MustImplementInterfaceTrait;

/**
* @throws MustImplementInterfaceException
*/
public function process(ContainerBuilder $container): void
{
$taggedServices = array_keys(
[
... $container->findTaggedServiceIds(TaggedIteratorDataProviderLoader::DATA_PROVIDER_TAG),

]
);

foreach ($taggedServices as $environmentType) {
$this->checkInterface($environmentType, DataProviderInterface::class);
}
}
}
57 changes: 57 additions & 0 deletions src/Gdpr/Attribute/Request/GdprRequestBody.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php
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\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
{
public function __construct()
{
parent::__construct(
required: true,
content: new JsonContent(

required: ['providers', 'searchTerms'],
properties: [
new Property(
property: 'providers',
description: 'A list of provider keys to search',
type: 'array',
items: new Items(
type: 'string',
example: 'pimcore_users'
)
),

new Property(
property: 'searchTerms',
ref: SearchTerms::class,
description: 'The object containing the search values.',
type: 'object'
),
],
type: 'object',
),
);
}
}
76 changes: 76 additions & 0 deletions src/Gdpr/Attribute/Request/SearchTerms.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
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\Attribute\Request;

use OpenApi\Attributes\Property;
use OpenApi\Attributes\Schema;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException;
use Symfony\Component\Validator\Constraints\Type;

/**
* @internal
*/
#[Schema(
title: 'GDPR Search Terms',
description: 'Object containing the values to search for. All fields are optional.',
type: 'object'
)]
final readonly class SearchTerms
{
public function __construct(
#[Property(description: 'The ID to search for.', type: 'string', nullable: true)]
#[Type('string')]
public ?string $id = null,

#[Property(description: 'The first name to search for.', type: 'string', nullable: true)]
#[Type('string')]
public ?string $firstname = null,

#[Property(description: 'The last name to search for.', type: 'string', nullable: true)]
#[Type('string')]
public ?string $lastname = null,

#[Property(description: 'The email address to search for.', type: 'string', nullable: true)]
#[Type('string')]
public ?string $email = null,
) {
if ($this->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;
}
}
Loading