Skip to content

Commit 94f6fd3

Browse files
WIP
Signed-off-by: Misha M.-Kupriyanov <[email protected]>
1 parent df47f4a commit 94f6fd3

File tree

11 files changed

+329
-2
lines changed

11 files changed

+329
-2
lines changed

lib/AppInfo/Application.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010

1111
use OC\Security\CSP\ContentSecurityPolicyManager;
1212
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
13+
use OCA\NCGoogleAnalytics\Service\Consent\IConsentService;
14+
use OCA\NCGoogleAnalytics\Service\Consent\IonosConsentService;
1315
use OCP\AppFramework\App;
1416
use OCP\AppFramework\Bootstrap\IBootContext;
1517
use OCP\AppFramework\Bootstrap\IBootstrap;
1618
use OCP\AppFramework\Bootstrap\IRegistrationContext;
1719
use OCP\AppFramework\Http\ContentSecurityPolicy;
1820
use OCP\IURLGenerator;
1921
use OCP\Util;
22+
use Psr\Container\ContainerInterface;
2023

2124
class Application extends App implements IBootstrap {
2225
public const APP_ID = 'googleanalytics';
@@ -26,6 +29,9 @@ public function __construct() {
2629
}
2730

2831
public function register(IRegistrationContext $context): void {
32+
$context->registerService(IConsentService::class, function (ContainerInterface $c): IonosConsentService {
33+
return new IonosConsentService();
34+
});
2935
}
3036

3137
public function boot(IBootContext $context): void {

lib/Controller/JavaScriptController.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
namespace OCA\NCGoogleAnalytics\Controller;
88

99
use OCA\NCGoogleAnalytics\Config;
10+
use OCA\NCGoogleAnalytics\Service\Consent\IConsentService;
1011
use OCP\AppFramework\Controller;
1112
use OCP\AppFramework\Http\DataDownloadResponse;
1213
use OCP\AppFramework\Http\TextPlainResponse;
@@ -19,11 +20,13 @@ class JavaScriptController extends Controller {
1920
* @param string $appName
2021
* @param IRequest $request
2122
* @param Config $config
23+
* @param IConsentService $consentService
2224
*/
2325
public function __construct(
2426
$appName,
2527
IRequest $request,
2628
protected Config $config,
29+
protected IConsentService $consentService,
2730
) {
2831
parent::__construct($appName, $request);
2932
}
@@ -44,6 +47,17 @@ public function tracking(): TextPlainResponse|DataDownloadResponse {
4447
return $response;
4548
}
4649

50+
try {
51+
$consent = $this->consentService->getConsent($_COOKIE);
52+
if (!$consent->hasStatisticsConsent()) {
53+
throw new \Exception('no statistics consent');
54+
}
55+
} catch (\Exception $e) {
56+
$response = new TextPlainResponse('// tracking disabled: no statistics consent');
57+
$response->addHeader('Content-Type', 'text/javascript');
58+
return $response;
59+
}
60+
4761
$script = file_get_contents(__DIR__ . '/../../js/track.js');
4862
$script = str_replace('%GTM_ID%', $gtmId, $script);
4963

lib/Service/Consent/Consent.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2024 2024 STRATO AG
4+
//
5+
// SPDX-License-Identifier: AGPL-3.0-or-later
6+
7+
declare(strict_types=1);
8+
9+
namespace OCA\NCGoogleAnalytics\Service\Consent;
10+
11+
class Consent implements IConsent {
12+
public function __construct(
13+
private readonly bool $technical = false,
14+
private readonly bool $statistics = false,
15+
private readonly bool $marketing = false,
16+
private readonly bool $partnerships = false,
17+
) {
18+
}
19+
20+
public function hasTechnicalConsent(): bool {
21+
return $this->technical;
22+
}
23+
24+
public function hasStatisticsConsent(): bool {
25+
return $this->statistics;
26+
}
27+
28+
public function hasMarketingConsent(): bool {
29+
return $this->marketing;
30+
}
31+
32+
public function hasPartnershipsConsent(): bool {
33+
return $this->partnerships;
34+
}
35+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2024 2024 STRATO AG
4+
//
5+
// SPDX-License-Identifier: AGPL-3.0-or-later
6+
7+
declare(strict_types=1);
8+
9+
namespace OCA\NCGoogleAnalytics\Service\Consent;
10+
11+
enum ConsentCategory: string {
12+
case TECHNICAL = 'technical';
13+
case STATISTICS = 'statistics';
14+
case MARKETING = 'marketing';
15+
case PARTNERSHIPS = 'partnerships';
16+
}

lib/Service/Consent/IConsent.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2024 2024 STRATO AG
4+
//
5+
// SPDX-License-Identifier: AGPL-3.0-or-later
6+
7+
declare(strict_types=1);
8+
9+
namespace OCA\NCGoogleAnalytics\Service\Consent;
10+
11+
interface IConsent {
12+
public function hasTechnicalConsent(): bool;
13+
14+
public function hasStatisticsConsent(): bool;
15+
16+
public function hasMarketingConsent(): bool;
17+
18+
public function hasPartnershipsConsent(): bool;
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2024 2024 STRATO AG
4+
//
5+
// SPDX-License-Identifier: AGPL-3.0-or-later
6+
7+
declare(strict_types=1);
8+
9+
namespace OCA\NCGoogleAnalytics\Service\Consent;
10+
11+
interface IConsentService {
12+
public function getConsent(array $cookies): IConsent;
13+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2024 2024 STRATO AG
4+
//
5+
// SPDX-License-Identifier: AGPL-3.0-or-later
6+
7+
declare(strict_types=1);
8+
9+
namespace OCA\NCGoogleAnalytics\Service\Consent;
10+
11+
class IonosConsentService implements IConsentService {
12+
private const COOKIE_NAME = 'PRIVACY_CONSENT';
13+
14+
public function getConsent(array $cookies): IConsent {
15+
$encodedValue = $cookies[self::COOKIE_NAME] ?? '';
16+
17+
if (empty($encodedValue)) {
18+
throw new \InvalidArgumentException('No consent cookie found');
19+
}
20+
21+
$decoded = base64_decode($encodedValue, true);
22+
if ($decoded === false) {
23+
throw new \InvalidArgumentException('Invalid base64 string');
24+
}
25+
26+
$data = json_decode($decoded, true);
27+
if (json_last_error() !== JSON_ERROR_NONE) {
28+
throw new \InvalidArgumentException('Invalid JSON string');
29+
}
30+
31+
return new Consent(
32+
$data[ConsentCategory::TECHNICAL->value] ?? false,
33+
$data[ConsentCategory::STATISTICS->value] ?? false,
34+
$data[ConsentCategory::MARKETING->value] ?? false,
35+
$data[ConsentCategory::PARTNERSHIPS->value] ?? false
36+
);
37+
}
38+
}

tests/Integration/GoogleAnalyticsTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
use Psr\Container\ContainerExceptionInterface;
1818
use Psr\Container\NotFoundExceptionInterface;
1919

20+
/**
21+
* @backupGlobals enabled
22+
*/
2023
class GoogleAnalyticsTest extends TestCase {
2124
private JavaScriptController $controller;
2225

@@ -37,14 +40,38 @@ public function setUp(): void {
3740
$this->controller = $container->get(JavaScriptController::class);
3841
}
3942

43+
private function mockContentServiceState(bool $statistics = false): void {
44+
$cookieValue = base64_encode(json_encode([
45+
'technical' => false,
46+
'statistics' => $statistics,
47+
'marketing' => false,
48+
'partnerships' => false,
49+
]));
50+
51+
$_COOKIE = ['PRIVACY_CONSENT' => $cookieValue];
52+
}
53+
4054
public function tearDown(): void {
4155
$this->config->setSystemValue('googleanalytics_tracking_key', null);
56+
unset($_COOKIE['PRIVACY_CONSENT']);
4257
}
4358

4459
/**
4560
* @throws Exception
4661
*/
4762
public function testTrackingReturnsDisabledResponseWhenNoKey(): void {
63+
$this->mockContentServiceState(true);
64+
$response = $this->controller->tracking();
65+
66+
$this->assertInstanceOf(TextPlainResponse::class, $response);
67+
$this->assertEquals('// tracking disabled', $response->render());
68+
}
69+
70+
/**
71+
* @throws Exception
72+
*/
73+
public function testTrackingReturnsDisabledResponseWhenNoConsent(): void {
74+
$this->mockContentServiceState(true);
4875
$response = $this->controller->tracking();
4976

5077
$this->assertInstanceOf(TextPlainResponse::class, $response);
@@ -55,6 +82,7 @@ public function testTrackingReturnsDisabledResponseWhenNoKey(): void {
5582
* @throws Exception
5683
*/
5784
public function testTrackingReturnsScriptResponseWhenKeyExists(): void {
85+
$this->mockContentServiceState(true);
5886
$this->config->setSystemValue('googleanalytics_tracking_key', 'UA-123456-1');
5987

6088
$response = $this->controller->tracking();

tests/Unit/AppInfo/ApplicationTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
use OC\Security\CSP\ContentSecurityPolicyManager;
1010
use OC\Security\CSP\ContentSecurityPolicyNonceManager;
1111
use OCA\NCGoogleAnalytics\AppInfo\Application;
12+
use OCA\NCGoogleAnalytics\Service\Consent\IConsentService;
13+
use OCA\NCGoogleAnalytics\Service\Consent\IonosConsentService;
1214
use OCP\AppFramework\Bootstrap\IBootContext;
15+
use OCP\AppFramework\Bootstrap\IRegistrationContext;
1316
use OCP\AppFramework\Http\ContentSecurityPolicy;
1417
use OCP\IURLGenerator;
1518
use PHPUnit\Framework\MockObject\MockObject;
@@ -21,12 +24,14 @@ class ApplicationTest extends TestCase {
2124
private ContentSecurityPolicyNonceManager|MockObject $nonceManager;
2225
private ContentSecurityPolicyManager|MockObject $contentSecurityPolicyManager;
2326
private IBootContext|MockObject $context;
27+
private $registrationContext;
2428

2529
protected function setUp(): void {
2630
$this->urlGenerator = $this->createMock(IURLGenerator::class);
2731
$this->nonceManager = $this->createMock(ContentSecurityPolicyNonceManager::class);
2832
$this->contentSecurityPolicyManager = $this->createMock(ContentSecurityPolicyManager::class);
2933
$this->context = $this->createMock(IBootContext::class);
34+
$this->registrationContext = $this->createMock(IRegistrationContext::class);
3035
$this->application = new Application();
3136
}
3237

@@ -40,6 +45,25 @@ public function testBoot(): void {
4045

4146
$this->application->boot($this->context);
4247
}
48+
49+
public function testRegister(): void {
50+
$this->registrationContext->expects($this->exactly(1))
51+
->method('registerService')
52+
->with(
53+
IConsentService::class,
54+
$this->isInstanceOf(\Closure::class),
55+
true
56+
);
57+
58+
$this->application->register($this->registrationContext);
59+
}
60+
61+
public function testConsentServiceRegistration(): void {
62+
$consentService = \OC::$server->getRegisteredAppContainer(Application::APP_ID)->get(IConsentService::class);
63+
64+
$this->assertInstanceOf(IonosConsentService::class, $consentService, 'FATAL: IonosConsentService is not registered!');
65+
}
66+
4367
public function testTrackingScriptAddition(): void {
4468
$this->urlGenerator->method('linkToRoute')->willReturn('someUrl');
4569
$this->nonceManager->method('getNonce')->willReturn('someNonce');

0 commit comments

Comments
 (0)