diff --git a/src/main/java/org/htmlunit/csp/Directive.java b/src/main/java/org/htmlunit/csp/Directive.java
index 8768e0c..8a6d3d4 100644
--- a/src/main/java/org/htmlunit/csp/Directive.java
+++ b/src/main/java/org/htmlunit/csp/Directive.java
@@ -65,6 +65,10 @@ protected void removeValueIgnoreCase(final String value) {
values_ = copy;
}
+ protected void removeValueExact(final String value) {
+ values_.remove(value);
+ }
+
@FunctionalInterface
public interface DirectiveErrorConsumer {
/** ignored. */
@@ -74,4 +78,21 @@ void add(Policy.Severity severity, String message,
int valueIndex); // index = -1 for errors not pertaining to a value
}
+
+ /** ManipulationErrorConsumer. */
+ @FunctionalInterface
+ public interface ManipulationErrorConsumer {
+ /** ignored. */
+ ManipulationErrorConsumer ignored = (severity, message) -> { };
+
+ void add(Severity severity, String message);
+
+ /** Severity. */
+ enum Severity {
+ /** Info. */
+ Info,
+ /** Warning. */
+ Warning
+ }
+ }
}
diff --git a/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java b/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
index 8ffdbe1..245b036 100644
--- a/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
+++ b/src/main/java/org/htmlunit/csp/directive/TrustedTypesDirective.java
@@ -188,4 +188,32 @@ public void setStar_(final boolean star) {
public List getPolicyNames_() {
return Collections.unmodifiableList(policyNames_);
}
+
+ /**
+ * Add a policy name.
+ * @param policyName the policy name to add
+ * @param errors the error consumer
+ */
+ public void addPolicyName(final String policyName, final ManipulationErrorConsumer errors) {
+ if (!TT_POLICY_NAME_PATTERN.matcher(policyName).matches()) {
+ throw new IllegalArgumentException("Invalid policy name: " + policyName);
+ }
+ // Policy names are case-sensitive per browser behavior
+ if (policyNames_.contains(policyName)) {
+ errors.add(ManipulationErrorConsumer.Severity.Warning, "Duplicate policy name " + policyName);
+ return;
+ }
+ policyNames_.add(policyName);
+ addValue(policyName);
+ }
+
+ /**
+ * Remove a policy name.
+ * @param policyName the policy name to remove
+ */
+ public void removePolicyName(final String policyName) {
+ // Policy names are case-sensitive per browser behavior
+ policyNames_.remove(policyName);
+ removeValueExact(policyName);
+ }
}
diff --git a/src/test/java/org/htmlunit/csp/TrustedTypesTest.java b/src/test/java/org/htmlunit/csp/TrustedTypesTest.java
index 6a0d833..4d8c990 100644
--- a/src/test/java/org/htmlunit/csp/TrustedTypesTest.java
+++ b/src/test/java/org/htmlunit/csp/TrustedTypesTest.java
@@ -14,6 +14,7 @@
*/
package org.htmlunit.csp;
+import org.htmlunit.csp.Directive;
import org.htmlunit.csp.directive.RequireTrustedTypesForDirective;
import org.htmlunit.csp.directive.TrustedTypesDirective;
import org.junit.jupiter.api.Test;
@@ -22,6 +23,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@@ -424,6 +426,161 @@ public void testTrustedTypesPolicyNamesCaseSensitive() {
assertTrue(tt.getPolicyNames_().contains("MyPolicy"));
}
+ @Test
+ public void testTrustedTypesManipulation() {
+ Policy p = Policy.parseSerializedCSP("trusted-types one", ThrowIfPolicyError);
+ TrustedTypesDirective tt = p.trustedTypes().get();
+
+ // Add policy name
+ tt.addPolicyName("two", Directive.ManipulationErrorConsumer.ignored);
+ assertEquals(2, tt.getPolicyNames_().size());
+ assertTrue(tt.getPolicyNames_().contains("one"));
+ assertTrue(tt.getPolicyNames_().contains("two"));
+
+ // Adding same name with different case should work (case-sensitive)
+ tt.addPolicyName("ONE", Directive.ManipulationErrorConsumer.ignored);
+ assertEquals(3, tt.getPolicyNames_().size());
+ assertTrue(tt.getPolicyNames_().contains("ONE"));
+
+ // Remove policy name (case-sensitive)
+ tt.removePolicyName("one");
+ assertEquals(2, tt.getPolicyNames_().size());
+ assertFalse(tt.getPolicyNames_().contains("one"));
+ assertTrue(tt.getPolicyNames_().contains("ONE")); // ONE should still be there
+ assertTrue(tt.getPolicyNames_().contains("two"));
+
+ // Set allow-duplicates
+ assertFalse(tt.allowDuplicates());
+ tt.setAllowDuplicates_(true);
+ assertTrue(tt.allowDuplicates());
+ tt.setAllowDuplicates_(false);
+ assertFalse(tt.allowDuplicates());
+
+ // Set star
+ assertFalse(tt.star());
+ tt.setStar_(true);
+ assertTrue(tt.star());
+ tt.setStar_(false);
+ assertFalse(tt.star());
+ }
+
+ @Test
+ public void testAddPolicyNameInvalidThrows() {
+ Policy p = Policy.parseSerializedCSP("trusted-types one", ThrowIfPolicyError);
+ TrustedTypesDirective tt = p.trustedTypes().get();
+
+ assertThrows(IllegalArgumentException.class,
+ () -> tt.addPolicyName("policy!name", Directive.ManipulationErrorConsumer.ignored));
+ assertThrows(IllegalArgumentException.class,
+ () -> tt.addPolicyName("policy name", Directive.ManipulationErrorConsumer.ignored));
+ assertThrows(IllegalArgumentException.class,
+ () -> tt.addPolicyName("policy(name)", Directive.ManipulationErrorConsumer.ignored));
+ assertThrows(IllegalArgumentException.class,
+ () -> tt.addPolicyName("", Directive.ManipulationErrorConsumer.ignored));
+
+ // Original policy name unchanged
+ assertEquals(1, tt.getPolicyNames_().size());
+ assertTrue(tt.getPolicyNames_().contains("one"));
+ }
+
+ @Test
+ public void testAddPolicyNameDuplicateWarning() {
+ Policy p = Policy.parseSerializedCSP("trusted-types one", ThrowIfPolicyError);
+ TrustedTypesDirective tt = p.trustedTypes().get();
+
+ ArrayList warnings = new ArrayList<>();
+ Directive.ManipulationErrorConsumer consumer = (severity, message) -> {
+ warnings.add(message);
+ };
+
+ // Adding duplicate should trigger warning, not add
+ tt.addPolicyName("one", consumer);
+ assertEquals(1, warnings.size());
+ assertTrue(warnings.get(0).contains("Duplicate policy name one"));
+ assertEquals(1, tt.getPolicyNames_().size());
+ }
+
+ @Test
+ public void testAddPolicyNameSpecialCharacters() {
+ Policy p = Policy.parseSerializedCSP("trusted-types base", ThrowIfPolicyError);
+ TrustedTypesDirective tt = p.trustedTypes().get();
+
+ tt.addPolicyName("my-policy", Directive.ManipulationErrorConsumer.ignored);
+ tt.addPolicyName("policy#1", Directive.ManipulationErrorConsumer.ignored);
+ tt.addPolicyName("path/to/policy", Directive.ManipulationErrorConsumer.ignored);
+ tt.addPolicyName("policy@domain", Directive.ManipulationErrorConsumer.ignored);
+ tt.addPolicyName("policy.v2", Directive.ManipulationErrorConsumer.ignored);
+ tt.addPolicyName("policy%20", Directive.ManipulationErrorConsumer.ignored);
+ tt.addPolicyName("policy=val", Directive.ManipulationErrorConsumer.ignored);
+ tt.addPolicyName("policy_name", Directive.ManipulationErrorConsumer.ignored);
+
+ assertEquals(9, tt.getPolicyNames_().size());
+ }
+
+ @Test
+ public void testRemovePolicyNameNonExistent() {
+ Policy p = Policy.parseSerializedCSP("trusted-types one two", ThrowIfPolicyError);
+ TrustedTypesDirective tt = p.trustedTypes().get();
+
+ // Removing a name that doesn't exist should be a no-op
+ tt.removePolicyName("three");
+ assertEquals(2, tt.getPolicyNames_().size());
+ assertTrue(tt.getPolicyNames_().contains("one"));
+ assertTrue(tt.getPolicyNames_().contains("two"));
+ }
+
+ @Test
+ public void testRemovePolicyNameCaseSensitive() {
+ Policy p = Policy.parseSerializedCSP("trusted-types myPolicy", ThrowIfPolicyError);
+ TrustedTypesDirective tt = p.trustedTypes().get();
+
+ // Removing with wrong case should not remove
+ tt.removePolicyName("MYPOLICY");
+ assertEquals(1, tt.getPolicyNames_().size());
+ assertTrue(tt.getPolicyNames_().contains("myPolicy"));
+
+ tt.removePolicyName("mypolicy");
+ assertEquals(1, tt.getPolicyNames_().size());
+
+ // Exact case should remove
+ tt.removePolicyName("myPolicy");
+ assertEquals(0, tt.getPolicyNames_().size());
+ }
+
+ @Test
+ public void testManipulationRoundTrip() {
+ Policy p = Policy.parseSerializedCSP("trusted-types one two", ThrowIfPolicyError);
+ TrustedTypesDirective tt = p.trustedTypes().get();
+
+ tt.addPolicyName("three", Directive.ManipulationErrorConsumer.ignored);
+ assertEquals("trusted-types one two three", p.toString());
+
+ tt.removePolicyName("two");
+ assertEquals("trusted-types one three", p.toString());
+
+ tt.setAllowDuplicates_(true);
+ assertEquals("trusted-types one three 'allow-duplicates'", p.toString());
+
+ tt.removePolicyName("one");
+ tt.removePolicyName("three");
+ assertEquals("trusted-types 'allow-duplicates'", p.toString());
+ }
+
+ @Test
+ public void testAddAfterRemove() {
+ Policy p = Policy.parseSerializedCSP("trusted-types one", ThrowIfPolicyError);
+ TrustedTypesDirective tt = p.trustedTypes().get();
+
+ tt.removePolicyName("one");
+ assertEquals(0, tt.getPolicyNames_().size());
+
+ // Re-adding the same name should work
+ tt.addPolicyName("one", Directive.ManipulationErrorConsumer.ignored);
+ assertEquals(1, tt.getPolicyNames_().size());
+ assertTrue(tt.getPolicyNames_().contains("one"));
+ assertEquals("trusted-types one", p.toString());
+ }
+
@Test
public void testRequireTrustedTypesForManipulation() {
Policy p = Policy.parseSerializedCSP("require-trusted-types-for 'script'", ThrowIfPolicyError);