From 75cf2e6ab303ae32d5711eabb1f0fda030bfc7cb Mon Sep 17 00:00:00 2001 From: Timon Heuser Date: Mon, 19 Jan 2026 12:44:34 +0100 Subject: [PATCH 1/3] chore: fix pipeline after merge --- Classes/Helpers/DirectivesNormalizer.php | 75 ++++++++++--------- Classes/Helpers/TagHelper.php | 40 +++++----- Tests/Unit/Factory/PolicyFactoryTest.php | 16 +++- .../Unit/Helpers/DirectivesNormalizerTest.php | 2 + Tests/Unit/Model/PolicyTest.php | 3 - 5 files changed, 76 insertions(+), 60 deletions(-) diff --git a/Classes/Helpers/DirectivesNormalizer.php b/Classes/Helpers/DirectivesNormalizer.php index ae2a603..61ebbcb 100644 --- a/Classes/Helpers/DirectivesNormalizer.php +++ b/Classes/Helpers/DirectivesNormalizer.php @@ -14,10 +14,10 @@ * * We also cleanup of empty directives and entries here before further processing. */ -class DirectivesNormalizer +final class DirectivesNormalizer { /** - * @param array> $directives + * @param array> $directives * @return string[][] * @throws DirectivesNormalizerException */ @@ -26,43 +26,50 @@ public static function normalize(array $directives, LoggerInterface $logger): ar $result = []; // directives e.g. script-src: foreach ($directives as $directive => $values) { - if(is_array($values) && sizeof($values) > 0) { - $normalizedValues = []; - $firstKeyType = null; - // values e.g. 'self', 'unsafe-inline' OR key-value pairs e.g. example.com: true - foreach ($values as $key => $value) { - $keyType = gettype($key); - $valueType = gettype($value); - if ($firstKeyType === null) { - $firstKeyType = $keyType; - } else { - if ($keyType !== $firstKeyType) { - // we do not allow mixed key types -> this should be marked as an error in the IDE as well - // as Flow should throw an exception here. But just to be sure, we add this check. - throw new DirectivesNormalizerException('Directives must be defined as a list OR an object.'); - } + if (!is_array($values) || count($values) === 0) { + continue; + } + + $normalizedValues = []; + $firstKeyType = null; + // values e.g. 'self', 'unsafe-inline' OR key-value pairs e.g. example.com: true + foreach ($values as $key => $value) { + if ($firstKeyType === null) { + $firstKeyType = gettype($key); + } else { + if (gettype($key) !== $firstKeyType) { + // we do not allow mixed key types -> this should be marked as an error in the IDE as well + // as Flow should throw an exception here. But just to be sure, we add this check. + throw new DirectivesNormalizerException( + 'Directives must be defined as a list OR an object.' + ); } + } - if($keyType === 'integer' && $valueType === 'string' && !empty($value)) { - // old configuration format using list - $normalizedValues[] = $value; - $logger->warning('Using list format for CSP directives is deprecated and will be removed in future versions. Please use key-value pairs with boolean values instead.'); - } elseif($keyType === 'string') { - // new configuration format using key-value pairs - if($valueType === 'boolean') { - if($value === true && !empty($key)) { - $normalizedValues[] = $key; - } - } else { - // We chose a format similar to NodeType constraints yaml configuration. - throw new DirectivesNormalizerException('When using keys in your yaml, the values must be boolean.'); + if (is_int($key) && is_string($value) && trim($value) !== '') { + // old configuration format using list + $normalizedValues[] = $value; + $logger->warning( + 'Using list format for CSP directives is deprecated and will be removed in future versions. Please use key-value pairs with boolean values instead.' + ); + } elseif (is_string($key)) { + // new configuration format using key-value pairs + if (is_bool($value)) { + if ($value === true && trim($key) !== '') { + $normalizedValues[] = $key; } + continue; } + + // We chose a format similar to NodeType constraints yaml configuration. + throw new DirectivesNormalizerException( + 'When using keys in your yaml, the values must be boolean.' + ); } - if(!empty($normalizedValues)) { - // we also clean up empty directives here - $result[$directive] = $normalizedValues; - } + } + if ($normalizedValues !== []) { + // we also clean up empty directives here + $result[$directive] = $normalizedValues; } } diff --git a/Classes/Helpers/TagHelper.php b/Classes/Helpers/TagHelper.php index 7f0ba69..94f9c1a 100644 --- a/Classes/Helpers/TagHelper.php +++ b/Classes/Helpers/TagHelper.php @@ -9,7 +9,7 @@ class TagHelper public static function tagHasAttribute( string $tag, string $name, - string $value = null + ?string $value = null ): bool { $value = (string)$value; if ($value === '') { @@ -36,10 +36,10 @@ public static function tagChangeAttributeValue( return preg_replace_callback( self::buildMatchAttributeNameWithAnyValueReqex($name), function ($hits) use ($newValue) { - return $hits["pre"]. - $hits["name"]. - $hits["glue"]. - $newValue. + return $hits["pre"] . + $hits["name"] . + $hits["glue"] . + $newValue . $hits["post"]; }, $tag @@ -49,22 +49,22 @@ function ($hits) use ($newValue) { public static function tagAddAttribute( string $tag, string $name, - string $value = null + ?string $value = null ): string { return preg_replace_callback( self::buildMatchEndOfOpeningTagReqex(), function ($hits) use ($name, $value) { if ((string)$value !== '') { - return $hits["start"]. - ' '. - $name. - '="'. - $value. - '"'. + return $hits["start"] . + ' ' . + $name . + '="' . + $value . + '"' . $hits["end"]; } - return $hits["start"].' '.$name.$hits["end"]; + return $hits["start"] . ' ' . $name . $hits["end"]; }, $tag ); @@ -85,8 +85,8 @@ private static function buildMatchAttributeNameWithAnyValueReqex(string $name): { $nameQuoted = self::escapeReqexCharsInString($name); - return '/(?
<.*? )(?'.
-            $nameQuoted.
+        return '/(?
<.*? )(?' .
+            $nameQuoted .
             ')(?=")(?.*?)(?".*?>)/';
     }
 
@@ -94,7 +94,7 @@ private static function buildMatchAttributeNameReqex(string $name): string
     {
         $nameQuoted = self::escapeReqexCharsInString($name);
 
-        return '/(?
<.*? )(?'.$nameQuoted.')(?.*?>)/';
+        return '/(?
<.*? )(?' . $nameQuoted . ')(?.*?>)/';
     }
 
     private static function buildMatchAttributeNameWithSpecificValueReqex(
@@ -104,10 +104,10 @@ private static function buildMatchAttributeNameWithSpecificValueReqex(
         $nameQuoted = self::escapeReqexCharsInString($name);
         $valueQuoted = self::escapeReqexCharsInString($value);
 
-        return '/(?
<.*? )(?'.
-            $nameQuoted.
-            ')(?=")(?'.
-            $valueQuoted.
+        return '/(?
<.*? )(?' .
+            $nameQuoted .
+            ')(?=")(?' .
+            $valueQuoted .
             ')(?".*?>)/';
     }
 }
diff --git a/Tests/Unit/Factory/PolicyFactoryTest.php b/Tests/Unit/Factory/PolicyFactoryTest.php
index 9e877c7..fab1acb 100644
--- a/Tests/Unit/Factory/PolicyFactoryTest.php
+++ b/Tests/Unit/Factory/PolicyFactoryTest.php
@@ -24,6 +24,7 @@ class PolicyFactoryTest extends TestCase
 {
     private readonly LoggerInterface&MockObject $loggerMock;
     private readonly PolicyFactory $policyFactory;
+    private readonly ReflectionClass $policyFactoryReflection;
 
     protected function setUp(): void
     {
@@ -35,7 +36,10 @@ protected function setUp(): void
 
         $this->policyFactoryReflection = new ReflectionClass($this->policyFactory);
         $this->policyFactoryReflection->getProperty('logger')->setValue($this->policyFactory, $this->loggerMock);
-        $this->policyFactoryReflection->getProperty('throwInvalidDirectiveException')->setValue($this->policyFactory, true);
+        $this->policyFactoryReflection->getProperty('throwInvalidDirectiveException')->setValue(
+            $this->policyFactory,
+            true
+        );
     }
 
     public function testCreateShouldReturnPolicyAndMergeCustomWithDefaultDirective(): void
@@ -116,7 +120,10 @@ public function testCreateShouldFailWithInvalidDirective(): void
     public function testCreateShouldLogInvalidDirectiveInProduction(): void
     {
         $nonceMock = $this->createMock(Nonce::class);
-        $this->policyFactoryReflection->getProperty('throwInvalidDirectiveException')->setValue($this->policyFactory, false);
+        $this->policyFactoryReflection->getProperty('throwInvalidDirectiveException')->setValue(
+            $this->policyFactory,
+            false
+        );
 
         $defaultDirective = [
             'invalid' => [
@@ -131,7 +138,10 @@ public function testCreateShouldLogInvalidDirectiveInProduction(): void
         $this->loggerMock->expects($this->once())->method('critical');
         $this->policyFactory->create($nonceMock, $defaultDirective, $customDirective);
 
-        $this->policyFactoryReflection->getProperty('throwInvalidDirectiveException')->setValue($this->policyFactory, true);
+        $this->policyFactoryReflection->getProperty('throwInvalidDirectiveException')->setValue(
+            $this->policyFactory,
+            true
+        );
     }
 
     public function testCreateShouldReturnPolicyWithUniqueValues(): void
diff --git a/Tests/Unit/Helpers/DirectivesNormalizerTest.php b/Tests/Unit/Helpers/DirectivesNormalizerTest.php
index 8dbbc59..86c0eea 100644
--- a/Tests/Unit/Helpers/DirectivesNormalizerTest.php
+++ b/Tests/Unit/Helpers/DirectivesNormalizerTest.php
@@ -15,6 +15,7 @@
 class DirectivesNormalizerTest extends TestCase
 {
     private readonly LoggerInterface&MockObject $loggerMock;
+
     protected function setUp(): void
     {
         parent::setUp();
@@ -167,6 +168,7 @@ public function testConfiguration(): void
 
         self::assertSame($expected, $actual);
 
+        // @phpstan-ignore argument.type
         $actual = DirectivesNormalizer::normalize([
             'base-uri',
             'script-src' => [],
diff --git a/Tests/Unit/Model/PolicyTest.php b/Tests/Unit/Model/PolicyTest.php
index 860669e..ba92a90 100644
--- a/Tests/Unit/Model/PolicyTest.php
+++ b/Tests/Unit/Model/PolicyTest.php
@@ -11,7 +11,6 @@
 use PHPUnit\Framework\Attributes\CoversClass;
 use PHPUnit\Framework\Attributes\UsesClass;
 use PHPUnit\Framework\TestCase;
-use Psr\Log\LoggerInterface;
 use ReflectionClass;
 
 #[CoversClass(Policy::class)]
@@ -26,8 +25,6 @@ protected function setUp(): void
     {
         parent::setUp();
 
-        $this->loggerMock = $this->createMock(LoggerInterface::class);
-
         $this->policy = new Policy();
 
         $this->policyReflection = new ReflectionClass($this->policy);

From 935cbec4e869cb60893652fe47bed48812599dc0 Mon Sep 17 00:00:00 2001
From: Timon Heuser 
Date: Mon, 19 Jan 2026 12:45:39 +0100
Subject: [PATCH 2/3] chore: add pipelines to PRs

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7312a27..6345fc0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,6 +1,6 @@
 name: CI
 
-on: [ push ]
+on: [ push, pull_request ]
 
 jobs:
   phpunit:

From 5568da260f881bf3563ffd15feb776afce5f7251 Mon Sep 17 00:00:00 2001
From: Timon Heuser 
Date: Mon, 19 Jan 2026 12:54:27 +0100
Subject: [PATCH 3/3] chore: fix phpunit use class annotation

---
 Tests/Unit/Factory/PolicyFactoryTest.php        | 2 ++
 Tests/Unit/Helpers/DirectivesNormalizerTest.php | 1 +
 2 files changed, 3 insertions(+)

diff --git a/Tests/Unit/Factory/PolicyFactoryTest.php b/Tests/Unit/Factory/PolicyFactoryTest.php
index fab1acb..093b899 100644
--- a/Tests/Unit/Factory/PolicyFactoryTest.php
+++ b/Tests/Unit/Factory/PolicyFactoryTest.php
@@ -6,6 +6,7 @@
 
 use Flowpack\ContentSecurityPolicy\Exceptions\InvalidDirectiveException;
 use Flowpack\ContentSecurityPolicy\Factory\PolicyFactory;
+use Flowpack\ContentSecurityPolicy\Helpers\DirectivesNormalizer;
 use Flowpack\ContentSecurityPolicy\Model\Directive;
 use Flowpack\ContentSecurityPolicy\Model\Nonce;
 use Flowpack\ContentSecurityPolicy\Model\Policy;
@@ -20,6 +21,7 @@
 #[UsesClass(Policy::class)]
 #[UsesClass(Directive::class)]
 #[UsesClass(InvalidDirectiveException::class)]
+#[UsesClass(DirectivesNormalizer::class)]
 class PolicyFactoryTest extends TestCase
 {
     private readonly LoggerInterface&MockObject $loggerMock;
diff --git a/Tests/Unit/Helpers/DirectivesNormalizerTest.php b/Tests/Unit/Helpers/DirectivesNormalizerTest.php
index 86c0eea..7e30e4f 100644
--- a/Tests/Unit/Helpers/DirectivesNormalizerTest.php
+++ b/Tests/Unit/Helpers/DirectivesNormalizerTest.php
@@ -12,6 +12,7 @@
 use Psr\Log\LoggerInterface;
 
 #[CoversClass(DirectivesNormalizer::class)]
+#[CoversClass(DirectivesNormalizerException::class)]
 class DirectivesNormalizerTest extends TestCase
 {
     private readonly LoggerInterface&MockObject $loggerMock;