Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
24 changes: 24 additions & 0 deletions config/gdpr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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"]
3 changes: 2 additions & 1 deletion doc/05_Additional_Custom_Attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,5 +158,6 @@ final class AssetEvent extends AbstractPreResponseEvent
- `pre_response.workflow_details`
- `pre_response.notification_recipient`
- `pre_response.php_code_transformer`
- `pre_response.data_provider`
- `pre_response.element.usage.item`
- `pre_response.element.usage`
- `pre_response.element.usage`
172 changes: 172 additions & 0 deletions doc/10_Extending_Studio/11_Gdpr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# 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` 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

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 (Direct Download)

The export process is a "direct download" flow.

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.

---

## Example Data Provider

Here is an example of a provider that supports both searching and direct exporting for **Customer** data objects.

```php
<?php
declare(strict_types=1);

namespace App\Gdpr\Provider;

// 1. Import all required classes
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\Provider\DataProviderInterface;
use Pimcore\Bundle\StudioBackendBundle\Gdpr\Schema\GdprDataColumn;
use Pimcore\Bundle\StudioBackendBundle\Util\Constant\UserPermissions;
use Pimcore\Model\DataObject;
// You need to import the class for your DataObject
use Pimcore\Model\DataObject\Customer;

// 2. Add the AutoconfigureTag to register the provider
final class CustomerObjectProvider implements DataProviderInterface
{
/**
* You can inject any services you need.
*/
public function __construct(
// e.g. private readonly SecurityService $securityService
) {
}

/**
* 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 GdprDataColumn('path', 'Full Path'),
];
}

/**
* The core search logic.
*
* @return array<array<string, mixed>>
*/
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;
}

/**
* 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;

}
}

```
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);
}
}
}
45 changes: 45 additions & 0 deletions src/Gdpr/Attribute/Request/GdprRequestBody.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?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\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: ['providerName'],
properties: [
new Property(
property: 'providerName',
description: 'The key of the single provider to search (e.g., pimcore_user)',
type: 'string',
example: 'pimcore_user'
),
],
type: 'object',
),
);
}
}
69 changes: 69 additions & 0 deletions src/Gdpr/Attribute/Request/SearchTerms.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?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 Symfony\Component\Validator\Constraints\Email;
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')]//why is #[Email] constraint causing issues
public ?string $email = null,
) {
}

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