Skip to content
This repository was archived by the owner on Feb 6, 2025. It is now read-only.

Commit 93cabc5

Browse files
committed
Add Permissions-Policy header to respect user privacy
1 parent 63d3411 commit 93cabc5

8 files changed

Lines changed: 185 additions & 29 deletions

File tree

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ return [
3838
### Block cookies
3939

4040
By default all cookies are kept, also the cookie consent was not set.
41-
To block all (symfony) cookies, you can set the following config.
41+
To block all domain cookies, you can set the following config.
4242

4343
```yaml
4444
# config/packages/nucleos_gdpr.yaml
@@ -59,6 +59,20 @@ nucleos_gdpr:
5959
- ADMIN_.*
6060
```
6161
62+
63+
### Google FLoC (Federated Learning of Cohorts)
64+
65+
By default a `Permissions-Policy` header is added to every response to respect user privacy. You can enable Google FLoC tracking via the following configuration:
66+
67+
```yaml
68+
# config/packages/nucleos_gdpr.yaml
69+
70+
nucleos_gdpr:
71+
privacy:
72+
google_floc: true
73+
```
74+
75+
6276
### Assets
6377

6478
It is recommended to use [webpack](https://webpack.js.org/) / [webpack-encore](https://github.com/symfony/webpack-encore)

src/DependencyInjection/Configuration.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919

2020
final class Configuration implements ConfigurationInterface
2121
{
22-
public function getConfigTreeBuilder()
22+
public function getConfigTreeBuilder(): TreeBuilder
2323
{
2424
$treeBuilder = new TreeBuilder('nucleos_gdpr');
2525

2626
$rootNode = $treeBuilder->getRootNode();
2727
$rootNode->append($this->getBlockCookiesNode());
28+
$rootNode->append($this->getPrivacyNode());
2829

2930
return $treeBuilder;
3031
}
@@ -44,4 +45,18 @@ private function getBlockCookiesNode(): NodeDefinition
4445

4546
return $node;
4647
}
48+
49+
private function getPrivacyNode(): NodeDefinition
50+
{
51+
$node = (new TreeBuilder('privacy'))->getRootNode();
52+
53+
$node
54+
->addDefaultsIfNotSet()
55+
->children()
56+
->booleanNode('google_floc')->defaultFalse()->end()
57+
->end()
58+
;
59+
60+
return $node;
61+
}
4762
}

src/DependencyInjection/NucleosGDPRExtension.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,11 @@ public function load(array $configs, ContainerBuilder $container): void
3131

3232
$loader = new Loader\PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
3333
$loader->load('block.php');
34+
$loader->load('listener.php');
3435

35-
if (isset($config['block_cookies'])) {
36-
$loader->load('listener.php');
37-
38-
$container->getDefinition(KernelEventSubscriber::class)
39-
->replaceArgument(0, $config['block_cookies']['keep'])
40-
;
41-
}
36+
$container->getDefinition(KernelEventSubscriber::class)
37+
->replaceArgument(0, isset($config['block_cookies']) ? $config['block_cookies']['keep'] : null)
38+
->replaceArgument(1, $config['privacy']['google_floc'])
39+
;
4240
}
4341
}

src/EventListener/KernelEventSubscriber.php

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,43 @@
2020
final class KernelEventSubscriber implements EventSubscriberInterface
2121
{
2222
/**
23-
* @var string[]
23+
* @var string[]|null
2424
*/
2525
private $whitelist;
2626

27+
/**
28+
* @var bool
29+
*/
30+
private $googleFLOC;
31+
2732
/**
2833
* @param string[] $whitelist
2934
*/
30-
public function __construct(array $whitelist = [])
35+
public function __construct(?array $whitelist = [], bool $googleFLOC = false)
3136
{
32-
$this->whitelist = $whitelist;
37+
$this->whitelist = $whitelist;
38+
$this->googleFLOC = $googleFLOC;
3339
}
3440

3541
public static function getSubscribedEvents(): array
3642
{
3743
return [
38-
KernelEvents::RESPONSE => 'cleanCookies',
44+
KernelEvents::RESPONSE => [
45+
['cleanCookies', 0],
46+
['addFLoCPolicy', 0],
47+
],
3948
];
4049
}
4150

51+
/**
52+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
53+
*/
4254
public function cleanCookies(ResponseEvent $event): void
4355
{
56+
if (null === $this->whitelist) {
57+
return;
58+
}
59+
4460
$headers = $event->getResponse()->headers;
4561
$cookies = $headers->getCookies();
4662

@@ -57,6 +73,16 @@ public function cleanCookies(ResponseEvent $event): void
5773
}
5874
}
5975

76+
public function addFLoCPolicy(ResponseEvent $event): void
77+
{
78+
if (true === $this->googleFLOC) {
79+
return;
80+
}
81+
82+
$response = $event->getResponse();
83+
$response->headers->set('Permissions-Policy', 'interest-cohort=()');
84+
}
85+
6086
/**
6187
* @param Cookie[] $cookies
6288
*/
@@ -71,8 +97,15 @@ private function hasCookieConsent(array $cookies): bool
7197
return false;
7298
}
7399

100+
/**
101+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
102+
*/
74103
private function isWhitelisted(Cookie $cookie): bool
75104
{
105+
if (null === $this->whitelist) {
106+
return true;
107+
}
108+
76109
foreach ($this->whitelist as $name) {
77110
if ($cookie->getName() === $name || 1 === preg_match('#'.$name.'#', $cookie->getName())) {
78111
return true;

src/Resources/config/listener.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
->set(KernelEventSubscriber::class)
2020
->tag('kernel.event_subscriber')
2121
->args([
22-
[],
22+
null,
23+
false,
2324
])
2425

2526
;

tests/DependencyInjection/ConfigurationTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,28 @@ public function testDefaultOptions(): void
2424
$config = $processor->processConfiguration(new Configuration(), []);
2525

2626
$expected = [
27+
'privacy' => [
28+
'google_floc' => false,
29+
],
30+
];
31+
32+
static::assertSame($expected, $config);
33+
}
34+
35+
public function testEnabledGoogleFLoC(): void
36+
{
37+
$processor = new Processor();
38+
39+
$config = $processor->processConfiguration(new Configuration(), [[
40+
'privacy' => [
41+
'google_floc' => true,
42+
],
43+
]]);
44+
45+
$expected = [
46+
'privacy' => [
47+
'google_floc' => true,
48+
],
2749
];
2850

2951
static::assertSame($expected, $config);
@@ -41,6 +63,9 @@ public function testBlockedCookieEnabled(): void
4163
'block_cookies' => [
4264
'keep' => ['PHPSESSID'],
4365
],
66+
'privacy' => [
67+
'google_floc' => false,
68+
],
4469
];
4570

4671
static::assertSame($expected, $config);
@@ -60,6 +85,9 @@ public function testBlockedCookieOptions(): void
6085
'block_cookies' => [
6186
'keep' => ['SOMEKEY', 'OTHERKEY'],
6287
],
88+
'privacy' => [
89+
'google_floc' => false,
90+
],
6391
];
6492

6593
static::assertSame($expected, $config);

tests/DependencyInjection/NucleosGDPRExtensionTest.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ public function testLoadDefault(): void
2424
$this->load();
2525

2626
$this->assertContainerBuilderHasService('nucleos_gdpr.block.information');
27-
$this->assertContainerBuilderNotHasService(KernelEventSubscriber::class);
27+
$this->assertContainerBuilderHasService(KernelEventSubscriber::class);
28+
29+
$this->assertContainerBuilderHasServiceDefinitionWithArgument(KernelEventSubscriber::class, 0, null);
30+
$this->assertContainerBuilderHasServiceDefinitionWithArgument(KernelEventSubscriber::class, 1, false);
2831
}
2932

3033
public function testLoadWithCookieBlock(): void
@@ -33,13 +36,22 @@ public function testLoadWithCookieBlock(): void
3336
'block_cookies' => null,
3437
]);
3538

36-
$this->assertContainerBuilderHasService(KernelEventSubscriber::class);
37-
3839
$this->assertContainerBuilderHasServiceDefinitionWithArgument(KernelEventSubscriber::class, 0, [
3940
'PHPSESSID',
4041
]);
4142
}
4243

44+
public function testLoadWithGoogleFLoC(): void
45+
{
46+
$this->load([
47+
'privacy' => [
48+
'google_floc' => true,
49+
],
50+
]);
51+
52+
$this->assertContainerBuilderHasServiceDefinitionWithArgument(KernelEventSubscriber::class, 1, true);
53+
}
54+
4355
protected function getContainerExtensions(): array
4456
{
4557
return [

tests/EventListener/KernelEventSubscriberTest.php

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,29 @@ final class KernelEventSubscriberTest extends TestCase
2727
private const KEEP_REGEX = 'ADMIN_.*';
2828
private const KEEP_REGED_EXAMPLE = 'ADMIN_TEST';
2929

30-
/**
31-
* @var KernelEventSubscriber
32-
*/
33-
private $subscriber;
34-
35-
protected function setUp(): void
30+
public function testCleanCookiesWithDisabledOption(): void
3631
{
37-
$this->subscriber = new KernelEventSubscriber([
38-
self::KEEP_COOKIE_NAME,
39-
self::KEEP_REGEX,
40-
]);
32+
$response = new Response();
33+
$response->headers->setCookie(Cookie::create(self::SOME_COOKIE_NAME));
34+
$response->headers->setCookie(Cookie::create(self::KEEP_COOKIE_NAME));
35+
$response->headers->setCookie(Cookie::create(self::KEEP_REGED_EXAMPLE));
36+
$response->headers->setCookie(Cookie::create(GDPRInformationBlockService::COOKIE_NAME));
37+
38+
$event = new ResponseEvent(
39+
$this->createStub(HttpKernelInterface::class),
40+
$this->createStub(Request::class),
41+
0,
42+
$response
43+
);
44+
45+
$subscriber = new KernelEventSubscriber(null);
46+
$subscriber->cleanCookies($event);
47+
48+
static::assertCount(4, $response->headers->getCookies());
49+
$this->assertHasCookie(self::SOME_COOKIE_NAME, $response);
50+
$this->assertHasCookie(self::KEEP_COOKIE_NAME, $response);
51+
$this->assertHasCookie(self::KEEP_REGED_EXAMPLE, $response);
52+
$this->assertHasCookie(GDPRInformationBlockService::COOKIE_NAME, $response);
4153
}
4254

4355
public function testCleanCookiesWithConsent(): void
@@ -55,7 +67,11 @@ public function testCleanCookiesWithConsent(): void
5567
$response
5668
);
5769

58-
$this->subscriber->cleanCookies($event);
70+
$subscriber = new KernelEventSubscriber([
71+
self::KEEP_COOKIE_NAME,
72+
self::KEEP_REGEX,
73+
]);
74+
$subscriber->cleanCookies($event);
5975

6076
static::assertCount(4, $response->headers->getCookies());
6177
$this->assertHasCookie(self::SOME_COOKIE_NAME, $response);
@@ -78,13 +94,52 @@ public function testCleanCookiesWithNoConsent(): void
7894
$response
7995
);
8096

81-
$this->subscriber->cleanCookies($event);
97+
$subscriber = new KernelEventSubscriber([
98+
self::KEEP_COOKIE_NAME,
99+
self::KEEP_REGEX,
100+
]);
101+
$subscriber->cleanCookies($event);
82102

83103
static::assertCount(2, $response->headers->getCookies());
84104
$this->assertHasCookie(self::KEEP_COOKIE_NAME, $response);
85105
$this->assertHasCookie(self::KEEP_REGED_EXAMPLE, $response);
86106
}
87107

108+
public function testAddFLoCPolicy(): void
109+
{
110+
$response = new Response();
111+
112+
$event = new ResponseEvent(
113+
$this->createStub(HttpKernelInterface::class),
114+
$this->createStub(Request::class),
115+
0,
116+
$response
117+
);
118+
119+
$subscriber = new KernelEventSubscriber(null, false);
120+
$subscriber->addFLoCPolicy($event);
121+
122+
static::assertTrue($response->headers->has('Permissions-Policy'));
123+
static::assertSame('interest-cohort=()', $response->headers->get('Permissions-Policy'));
124+
}
125+
126+
public function testAddFLoCPolicyWithDisabledOption(): void
127+
{
128+
$response = new Response();
129+
130+
$event = new ResponseEvent(
131+
$this->createStub(HttpKernelInterface::class),
132+
$this->createStub(Request::class),
133+
0,
134+
$response
135+
);
136+
137+
$subscriber = new KernelEventSubscriber(null, true);
138+
$subscriber->addFLoCPolicy($event);
139+
140+
static::assertFalse($response->headers->has('Permissions-Policy'));
141+
}
142+
88143
private function assertHasCookie(string $cookieName, Response $response): void
89144
{
90145
static::assertCount(1, array_filter($response->headers->getCookies(), static function (Cookie $cookie) use ($cookieName): bool {

0 commit comments

Comments
 (0)