commit 8a038d2e7f57d358eec4f38bc3c72c2b9213475a
parent 9a0627d238cd984b8e57bd997f81e6996a007a05
Author: Tom Schuster <tschuster@mozilla.com>
Date: Thu, 16 Oct 2025 21:25:28 +0000
Bug 1989215 - Sanitizer: Sort the result of the get() method. r=smaug
Differential Revision: https://phabricator.services.mozilla.com/D267211
Diffstat:
7 files changed, 369 insertions(+), 202 deletions(-)
diff --git a/dom/security/sanitizer/Sanitizer.cpp b/dom/security/sanitizer/Sanitizer.cpp
@@ -189,23 +189,6 @@ void Sanitizer::SetDefaultConfig() {
ClearOnShutdown(&sDefaultAttributes);
}
-auto& GetAsSanitizerElementNamespace(
- const StringOrSanitizerElementNamespace& aElement) {
- return aElement.GetAsSanitizerElementNamespace();
-}
-auto& GetAsSanitizerElementNamespace(
- const OwningStringOrSanitizerElementNamespace& aElement) {
- return aElement.GetAsSanitizerElementNamespace();
-}
-auto& GetAsSanitizerElementNamespace(
- const StringOrSanitizerElementNamespaceWithAttributes& aElement) {
- return aElement.GetAsSanitizerElementNamespaceWithAttributes();
-}
-auto& GetAsSanitizerElementNamespace(
- const OwningStringOrSanitizerElementNamespaceWithAttributes& aElement) {
- return aElement.GetAsSanitizerElementNamespaceWithAttributes();
-}
-
// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-element
template <typename SanitizerElement>
static CanonicalName CanonicalizeElement(const SanitizerElement& aElement) {
@@ -224,7 +207,7 @@ static CanonicalName CanonicalizeElement(const SanitizerElement& aElement) {
// Step 3. Assert: name is a dictionary and both name["name"] and
// name["namespace"] exist.
- const auto& elem = GetAsSanitizerElementNamespace(aElement);
+ const auto& elem = GetAsDictionary(aElement);
MOZ_ASSERT(!elem.mName.IsVoid());
// Step 4. If name["namespace"] is the empty string, then set it to null.
@@ -760,49 +743,89 @@ void Sanitizer::MaybeMaterializeDefaultConfig() {
mIsDefaultConfig = false;
}
+// https://wicg.github.io/sanitizer-api/#dom-sanitizer-get
void Sanitizer::Get(SanitizerConfig& aConfig) {
MaybeMaterializeDefaultConfig();
+ // Step 1. Let config be this’s configuration.
+ // Step 2. If config["elements"] exists:
if (mElements) {
nsTArray<OwningStringOrSanitizerElementNamespaceWithAttributes> elements;
+ // Step 2.1. For any element of config["elements"]:
for (const auto& entry : *mElements) {
- elements.AppendElement()->SetAsSanitizerElementNamespaceWithAttributes() =
+ // Step 2.1.1. If element["attributes"] exists:
+ // Step 2.1.2. If element["removeAttributes"] exists:
+ // ...
+ // (The attributes are sorted by the ToSanitizerAttributes call in
+ // ToSanitizerElementNamespaceWithAttributes)
+ OwningStringOrSanitizerElementNamespaceWithAttributes owning;
+ owning.SetAsSanitizerElementNamespaceWithAttributes() =
entry.GetKey().ToSanitizerElementNamespaceWithAttributes(
entry.GetData());
+
+ // Step 2.2. Set config["elements"] to the result of sort in ascending
+ // order config["elements"], with elementA being less than item elementB.
+ // (Instead of sorting at the end, we sort during insertion)
+ elements.InsertElementSorted(owning,
+ SanitizerComparator<decltype(owning)>());
}
aConfig.mElements.Construct(std::move(elements));
} else {
+ // Step 3. If config["removeElements"] exists:
+ // Step 3.1. Set config["removeElements"] to the result of sort in ascending
+ // order config["removeElements"], with elementA being less than item
+ // elementB.
nsTArray<OwningStringOrSanitizerElementNamespace> removeElements;
for (const CanonicalName& canonical : *mRemoveElements) {
- removeElements.AppendElement()->SetAsSanitizerElementNamespace() =
+ OwningStringOrSanitizerElementNamespace owning;
+ owning.SetAsSanitizerElementNamespace() =
canonical.ToSanitizerElementNamespace();
+ removeElements.InsertElementSorted(
+ owning, SanitizerComparator<decltype(owning)>());
}
aConfig.mRemoveElements.Construct(std::move(removeElements));
}
+ // Step 4. If config["replaceWithChildrenElements"] exists:
if (mReplaceWithChildrenElements) {
+ // Step 4.1. Set config["replaceWithChildrenElements"] to the result of sort
+ // in ascending order config["replaceWithChildrenElements"], with elementA
+ // being less than item elementB.
nsTArray<OwningStringOrSanitizerElementNamespace>
replaceWithChildrenElements;
for (const CanonicalName& canonical : *mReplaceWithChildrenElements) {
- replaceWithChildrenElements.AppendElement()
- ->SetAsSanitizerElementNamespace() =
+ OwningStringOrSanitizerElementNamespace owning;
+ owning.SetAsSanitizerElementNamespace() =
canonical.ToSanitizerElementNamespace();
+ replaceWithChildrenElements.InsertElementSorted(
+ owning, SanitizerComparator<decltype(owning)>());
}
aConfig.mReplaceWithChildrenElements.Construct(
std::move(replaceWithChildrenElements));
}
+ // Step 5. If config["attributes"] exists:
if (mAttributes) {
+ // Step 5.1. Set config["attributes"] to the result of sort in ascending
+ // order config["attributes"], with attrA being less than item attrB.
+ // (Sorting is done by ToSanitizerAttributes)
aConfig.mAttributes.Construct(ToSanitizerAttributes(*mAttributes));
} else {
+ // Step 6. If config["removeAttributes"] exists:
+ // Step 6.1. Set config["removeAttributes"] to the result of sort in
+ // ascending order config["removeAttributes"], with attrA being less than
+ // item attrB.
aConfig.mRemoveAttributes.Construct(
ToSanitizerAttributes(*mRemoveAttributes));
}
+ // (In the spec these already exist in the |config| and don't need sorting)
aConfig.mComments.Construct(mComments);
if (mDataAttributes) {
aConfig.mDataAttributes.Construct(*mDataAttributes);
}
+
+ // Step 7. Return config.
}
// https://wicg.github.io/sanitizer-api/#sanitizerconfig-allow-an-element
diff --git a/dom/security/sanitizer/SanitizerTypes.cpp b/dom/security/sanitizer/SanitizerTypes.cpp
@@ -86,11 +86,13 @@ bool CanonicalElementAttributes::Equals(
nsTArray<OwningStringOrSanitizerAttributeNamespace> ToSanitizerAttributes(
const CanonicalNameSet& aSet) {
- // XXX Sorting
nsTArray<OwningStringOrSanitizerAttributeNamespace> attributes;
for (const CanonicalName& canonical : aSet) {
- attributes.AppendElement()->SetAsSanitizerAttributeNamespace() =
+ OwningStringOrSanitizerAttributeNamespace owning;
+ owning.SetAsSanitizerAttributeNamespace() =
canonical.ToSanitizerAttributeNamespace();
+ attributes.InsertElementSorted(owning,
+ SanitizerComparator<decltype(owning)>());
}
return attributes;
}
diff --git a/dom/security/sanitizer/SanitizerTypes.h b/dom/security/sanitizer/SanitizerTypes.h
@@ -77,6 +77,81 @@ using CanonicalElementMap =
nsTArray<OwningStringOrSanitizerAttributeNamespace> ToSanitizerAttributes(
const CanonicalNameSet& aSet);
+inline const auto& GetAsDictionary(
+ const OwningStringOrSanitizerAttributeNamespace& aOwning) {
+ return aOwning.GetAsSanitizerAttributeNamespace();
+}
+
+inline const auto& GetAsDictionary(
+ const OwningStringOrSanitizerElementNamespace& aOwning) {
+ return aOwning.GetAsSanitizerElementNamespace();
+}
+
+inline const auto& GetAsDictionary(
+ const OwningStringOrSanitizerElementNamespaceWithAttributes& aOwning) {
+ return aOwning.GetAsSanitizerElementNamespaceWithAttributes();
+}
+
+inline const auto& GetAsDictionary(
+ const StringOrSanitizerElementNamespace& aElement) {
+ return aElement.GetAsSanitizerElementNamespace();
+}
+
+inline const auto& GetAsDictionary(
+ const StringOrSanitizerElementNamespaceWithAttributes& aElement) {
+ return aElement.GetAsSanitizerElementNamespaceWithAttributes();
+}
+
+template <typename SanitizerNameNamespace>
+class MOZ_STACK_CLASS SanitizerComparator final {
+ public:
+ bool Equals(const SanitizerNameNamespace& aItemA,
+ const SanitizerNameNamespace& aItemB) const {
+ const auto& itemA = GetAsDictionary(aItemA);
+ const auto& itemB = GetAsDictionary(aItemB);
+
+ return itemA.mNamespace.IsVoid() == itemB.mNamespace.IsVoid() &&
+ itemA.mNamespace == itemB.mNamespace && itemA.mName == itemB.mName;
+ }
+
+ // https://wicg.github.io/sanitizer-api/#sanitizerconfig-less-than-item
+ bool LessThan(const SanitizerNameNamespace& aItemA,
+ const SanitizerNameNamespace& aItemB) const {
+ const auto& itemA = GetAsDictionary(aItemA);
+ const auto& itemB = GetAsDictionary(aItemB);
+
+ // Step 1. If itemA["namespace"] is null:
+ if (itemA.mNamespace.IsVoid()) {
+ // Step 1.1. If itemB["namespace"] is not null, return true.
+ if (!itemB.mNamespace.IsVoid()) {
+ return true;
+ }
+ } else {
+ // Step 2. Otherwise:
+ // Step 2.1. If itemB["namespace"] is null, return false.
+ if (itemB.mNamespace.IsVoid()) {
+ return false;
+ }
+
+ int result = Compare(itemA.mNamespace, itemB.mNamespace);
+ // Step 2.2. If itemA["namespace"] is code unit less than
+ // itemB["namespace"], return true.
+ if (result < 0) {
+ return true;
+ }
+ // Step 2.3. If itemA["namespace"] is not equal itemB["namespace"], return
+ // false.
+ // XXX https://github.com/WICG/sanitizer-api/pull/341
+ if (result != 0) {
+ return false;
+ }
+ }
+
+ // Step 3. Return itemA["name"] is code unit less thanitemB["name"].
+ return itemA.mName < itemB.mName;
+ }
+};
+
} // namespace mozilla::dom::sanitizer
#endif
diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-get.tentative.html b/testing/web-platform/tests/sanitizer-api/sanitizer-get.tentative.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./support/util.js"></script>
+</head>
+<body>
+<script>
+
+const TEST_CASES = [
+ [
+ ["b", "a"],
+ ["a", "b"]
+ ],
+ [
+ ["c", "b", "a"],
+ ["a", "b", "c"]
+ ],
+ [
+ [{name: "b", namespace: null}, {name: "a", namespace: null}],
+ [{name: "a", namespace: null}, {name: "b", namespace: null}]
+ ],
+ [
+ [{name: "_", namespace: "a"}, {name: "_", namespace: null}],
+ [{name: "_", namespace: null}, {name: "_", namespace: "a"}]
+ ],
+ [
+ [{name: "_", namespace: "b"}, {name: "_", namespace: "a"}],
+ [{name: "_", namespace: "a"}, {name: "_", namespace: "b"}]
+ ],
+ [
+ [{name: "a", namespace: "b"}, {name: "z", namespace: "a"}, {name: "b", namespace: "b"}],
+ [{name: "z", namespace: "a"}, {name: "a", namespace: "b"}, {name: "b", namespace: "b"}],
+ ]
+];
+
+for (const key of ["attributes", "removeAttributes", "elements", "removeElements", "replaceWithChildrenElements"]) {
+ test(() => {
+ for (const [input, expected] of TEST_CASES) {
+ let s = new Sanitizer({
+ [key]: input
+ });
+ assert_config(s.get(), {
+ [key]: expected
+ });
+ }
+ }, `Sorting of ${key}`);
+}
+
+for (const key of ["attributes", "removeAttributes"]) {
+ test(() => {
+ for (const [input, expected] of TEST_CASES) {
+ let s = new Sanitizer({
+ elements: [{name: "_", [key]: input}]
+ });
+ assert_config(s.get(), {
+ elements: [{name: "_", [key]: expected}]
+ });
+ }
+ }, `Sorting of element's ${key}`);
+}
+
+</script>
+</body>
+</html>
+\ No newline at end of file
diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-modifiers.tentative.html b/testing/web-platform/tests/sanitizer-api/sanitizer-modifiers.tentative.html
@@ -3,172 +3,10 @@
<head>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
+ <script src="./support/util.js"></script>
</head>
<body>
<script>
-function is_same_sanitizer_name(a, b) {
- return a.name === b.name && a.namespace === b.namespace;
-}
-
-// https://pr-preview.s3.amazonaws.com/otherdaniel/purification/pull/296.html#sanitizerconfig-valid
-function assert_config_is_valid(config) {
- // The config has either an elements or a removeElements key, but not both.
- assert_false(
- "elements" in config && "removeElements" in config,
- "Either elements or a removeElements, but not both",
- );
- assert_true(
- "elements" in config || "removeElements" in config,
- "Either elements or a removeElements",
- );
-
- // The config has either an attributes or a removeAttributes key, but not both.
- assert_false(
- "attributes" in config && "removeAttributes" in config,
- "Either attributes or a removeAttributes, but not both",
- );
- assert_true(
- "attributes" in config || "removeAttributes" in config,
- "Either attributes or removeAttributes",
- );
-
- // If both config[elements] and config[replaceWithChildrenElements] exist, then the difference of config[elements] and config[replaceWithChildrenElements] is empty.
- if (config.elements && config.replaceWithChildrenElements) {
- for (let element of config.elements) {
- assert_false(
- config.replaceWithChildrenElements.some((replaceElement) =>
- is_same_sanitizer_name(element, replaceElement),
- ),
- `replaceWithChildrenElements should not contain ${element.name}`,
- );
- }
- }
-
- // If both config[removeElements] and config[replaceWithChildrenElements] exist, then the difference of config[removeElements] and config[replaceWithChildrenElements] is empty.
- if (config.removeElements && config.replaceWithChildrenElements) {
- for (let removeElement of config.removeElements) {
- assert_false(
- config.replaceWithChildrenElements.some((replaceElement) =>
- is_same_sanitizer_name(removeElement, replaceElement),
- ),
- `replaceWithChildrenElements should not contain ${removeElement.name}`,
- );
- }
- }
-
- // If config[attributes] exists:
- if (config.attributes) {
- } else {
- // config[dataAttributes] does not exist.
- assert_false("dataAttributes" in config, "dataAttributes does not exist");
- }
-}
-
-function assert_config(config, expected) {
- const PROPERTIES = [
- "attributes",
- "removeAttributes",
- "elements",
- "removeElements",
- "replaceWithChildrenElements",
- "comments",
- "dataAttributes",
- ];
-
- // Prevent some typos in the expected config.
- for (let key of Object.keys(expected)) {
- assert_in_array(key, PROPERTIES, "expected");
- }
- for (let key of Object.keys(config)) {
- assert_in_array(key, PROPERTIES, "config");
- }
-
- assert_config_is_valid(config);
-
- // XXX dataAttributes
- // XXX comments
- // XXX duplications
- // XXX other consistency checks
-
- function assert_attrs(key, config, expected, prefix = "config") {
- // XXX we allow specifying only a subset for expected.
- if (!(key in expected)) {
- return;
- }
-
- if (expected[key] === undefined) {
- assert_false(key in config, `Unexpected '${key}' in ${prefix}`);
- return;
- }
-
- assert_true(key in config, `Missing '${key}' from ${prefix}`);
- assert_equals(config[key]?.length, expected[key].length, `${prefix}.${key}.length`);
- for (let i = 0; i < expected[key].length; i++) {
- let attribute = expected[key][i];
- if (typeof attribute === "string") {
- assert_object_equals(
- config[key][i],
- { name: attribute, namespace: null },
- `${prefix}.${key}[${i}] should match`,
- );
- } else {
- assert_object_equals(
- config[key][i],
- attribute,
- `${prefix}.${key}[${i}] should match`,
- );
- }
- }
- }
-
- assert_attrs("attributes", config, expected);
- assert_attrs("removeAttributes", config, expected);
-
- function assert_elems(key) {
- if (!(key in expected)) {
- return;
- }
-
- if (expected[key] === undefined) {
- assert_false(key in config, `Unexpected '${key}' in config`);
- return;
- }
-
- assert_true(key in config, `Missing '${key}' from config`);
- assert_equals(config[key]?.length, expected[key].length, `${key}.length`);
-
- const XHTML_NS = "http://www.w3.org/1999/xhtml";
-
- for (let i = 0; i < expected[key].length; i++) {
- let element = expected[key][i];
- // To make writing tests a bit easier we also support the shorthand string syntax.
- if (typeof element === "string") {
- let extra = key === "elements" ? { removeAttributes: [] } : { };
- assert_object_equals(
- config[key][i],
- { name: element, namespace: XHTML_NS, ...extra },
- `${key}[${i}] should match`,
- );
- } else {
- if (key === "elements") {
- assert_equals(config[key][i].name, element.name, `${key}[${i}].name should match`);
- let ns = "namespace" in element ? element.namespace : XHTML_NS;
- assert_equals(config[key][i].namespace, ns, `${key}[${i}].namespace should match`);
-
- assert_attrs("attributes", config[key][i], element, `config.elements[${i}]`);
- assert_attrs("removeAttributes", config[key][i], element, `config.elements[${i}]`);
- } else {
- assert_object_equals(config[key][i], element, `${key}[${i}] should match`);
- }
- }
- }
- }
-
- assert_elems("elements");
- assert_elems("removeElements");
- assert_elems("replaceWithChildrenElements");
-}
-
const NS = "http://example.org/";
test(() => {
@@ -339,19 +177,19 @@ test(() => {
assert_true(s.removeAttribute("dir"));
assert_config(s.get(), {
- removeAttributes: ["title", "dir"],
+ removeAttributes: ["dir", "title"],
elements: [{ name: "div", attributes: ["class"], removeAttributes: ["id"] }],
});
assert_true(s.removeAttribute("id"));
assert_config(s.get(), {
- removeAttributes: ["title", "dir", "id"],
+ removeAttributes: ["dir", "id", "title"],
elements: [{ name: "div", attributes: ["class"], removeAttributes: [] }],
});
assert_true(s.removeAttribute("class"));
assert_config(s.get(), {
- removeAttributes: ["title", "dir", "id", "class"],
+ removeAttributes: ["class", "dir", "id", "title"],
elements: [{ name: "div", attributes: [], removeAttributes: [] }],
});
}, "sanitizer.removeAttribute() with global removeAttributes and elements");
@@ -364,13 +202,13 @@ test(() => {
assert_false(s.removeElement("span"));
assert_config(s.get(), {
- elements: ["p", { name: "p", namespace: NS }],
+ elements: [{ name: "p", namespace: NS }, "p"],
replaceWithChildrenElements: ["b"],
});
assert_true(s.removeElement("b"));
assert_config(s.get(), {
- elements: ["p", { name: "p", namespace: NS }],
+ elements: [{ name: "p", namespace: NS }, "p"],
replaceWithChildrenElements: [],
});
@@ -395,25 +233,25 @@ test(() => {
assert_false(s.removeElement("p"));
assert_config(s.get(), {
- removeElements: ["p", { name: "p", namespace: NS }],
+ removeElements: [{ name: "p", namespace: NS }, "p"],
replaceWithChildrenElements: ["b"],
});
assert_false(s.removeElement({ name: "p", namespace: NS }));
assert_config(s.get(), {
- removeElements: ["p", { name: "p", namespace: NS }],
+ removeElements: [{ name: "p", namespace: NS }, "p"],
replaceWithChildrenElements: ["b"],
});
assert_true(s.removeElement("span"));
assert_config(s.get(), {
- removeElements: ["p", { name: "p", namespace: NS }, "span"],
+ removeElements: [{ name: "p", namespace: NS }, "p", "span"],
replaceWithChildrenElements: ["b"],
});
assert_true(s.removeElement("b"));
assert_config(s.get(), {
- removeElements: ["p", { name: "p", namespace: NS }, "span", "b"],
+ removeElements: [{ name: "p", namespace: NS }, "b", "p", "span"],
replaceWithChildrenElements: [],
});
}, "sanitizer.removeElement() with global removeElements");
@@ -438,7 +276,7 @@ test(() => {
assert_true(s.replaceElementWithChildren("b"));
assert_config(s.get(), {
- replaceWithChildrenElements: ["a", "span", "b"],
+ replaceWithChildrenElements: ["a", "b", "span"],
elements: [],
});
}, "sanitizer.replaceElementWithChildren() with global elements");
@@ -463,7 +301,7 @@ test(() => {
assert_true(s.replaceElementWithChildren("b"));
assert_config(s.get(), {
- replaceWithChildrenElements: ["a", "span", "b"],
+ replaceWithChildrenElements: ["a", "b", "span"],
removeElements: [],
});
}, "sanitizer.replaceElementWithChildren() with global removeElements");
@@ -482,13 +320,13 @@ test(() => {
assert_true(s.allowElement({name: "a", namespace: NS}));
assert_config(s.get(), {
- elements: ["a", {name: "a", namespace: NS}],
+ elements: [{name: "a", namespace: NS}, "a"],
replaceWithChildrenElements: ["b"]
});
assert_true(s.allowElement("b"));
assert_config(s.get(), {
- elements: ["a", {name: "a", namespace: NS}, "b"],
+ elements: [{name: "a", namespace: NS}, "a", "b"],
replaceWithChildrenElements: []
});
}, "sanitizer.allowElement() with global elements");
diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-removeUnsafe.tentative.html b/testing/web-platform/tests/sanitizer-api/sanitizer-removeUnsafe.tentative.html
@@ -22,7 +22,7 @@ test(t => {
"removeElements": [
{
"namespace": "http://www.w3.org/1999/xhtml",
- "name": "script"
+ "name": "embed"
},
{
"namespace": "http://www.w3.org/1999/xhtml",
@@ -38,7 +38,7 @@ test(t => {
},
{
"namespace": "http://www.w3.org/1999/xhtml",
- "name": "embed"
+ "name": "script"
},
{
"namespace": "http://www.w3.org/2000/svg",
diff --git a/testing/web-platform/tests/sanitizer-api/support/util.js b/testing/web-platform/tests/sanitizer-api/support/util.js
@@ -0,0 +1,162 @@
+function is_same_sanitizer_name(a, b) {
+ return a.name === b.name && a.namespace === b.namespace;
+}
+
+// https://pr-preview.s3.amazonaws.com/otherdaniel/purification/pull/296.html#sanitizerconfig-valid
+function assert_config_is_valid(config) {
+ // The config has either an elements or a removeElements key, but not both.
+ assert_false(
+ "elements" in config && "removeElements" in config,
+ "Either elements or a removeElements, but not both",
+ );
+ assert_true(
+ "elements" in config || "removeElements" in config,
+ "Either elements or a removeElements",
+ );
+
+ // The config has either an attributes or a removeAttributes key, but not both.
+ assert_false(
+ "attributes" in config && "removeAttributes" in config,
+ "Either attributes or a removeAttributes, but not both",
+ );
+ assert_true(
+ "attributes" in config || "removeAttributes" in config,
+ "Either attributes or removeAttributes",
+ );
+
+ // If both config[elements] and config[replaceWithChildrenElements] exist, then the difference of config[elements] and config[replaceWithChildrenElements] is empty.
+ if (config.elements && config.replaceWithChildrenElements) {
+ for (let element of config.elements) {
+ assert_false(
+ config.replaceWithChildrenElements.some((replaceElement) =>
+ is_same_sanitizer_name(element, replaceElement),
+ ),
+ `replaceWithChildrenElements should not contain ${element.name}`,
+ );
+ }
+ }
+
+ // If both config[removeElements] and config[replaceWithChildrenElements] exist, then the difference of config[removeElements] and config[replaceWithChildrenElements] is empty.
+ if (config.removeElements && config.replaceWithChildrenElements) {
+ for (let removeElement of config.removeElements) {
+ assert_false(
+ config.replaceWithChildrenElements.some((replaceElement) =>
+ is_same_sanitizer_name(removeElement, replaceElement),
+ ),
+ `replaceWithChildrenElements should not contain ${removeElement.name}`,
+ );
+ }
+ }
+
+ // If config[attributes] exists:
+ if (config.attributes) {
+ } else {
+ // config[dataAttributes] does not exist.
+ assert_false("dataAttributes" in config, "dataAttributes does not exist");
+ }
+}
+
+function assert_config(config, expected) {
+ const PROPERTIES = [
+ "attributes",
+ "removeAttributes",
+ "elements",
+ "removeElements",
+ "replaceWithChildrenElements",
+ "comments",
+ "dataAttributes",
+ ];
+
+ // Prevent some typos in the expected config.
+ for (let key of Object.keys(expected)) {
+ assert_in_array(key, PROPERTIES, "expected");
+ }
+ for (let key of Object.keys(config)) {
+ assert_in_array(key, PROPERTIES, "config");
+ }
+
+ assert_config_is_valid(config);
+
+ // XXX dataAttributes
+ // XXX comments
+ // XXX duplications
+ // XXX other consistency checks
+
+ function assert_attrs(key, config, expected, prefix = "config") {
+ // XXX we allow specifying only a subset for expected.
+ if (!(key in expected)) {
+ return;
+ }
+
+ if (expected[key] === undefined) {
+ assert_false(key in config, `Unexpected '${key}' in ${prefix}`);
+ return;
+ }
+
+ assert_true(key in config, `Missing '${key}' from ${prefix}`);
+ assert_equals(config[key]?.length, expected[key].length, `${prefix}.${key}.length`);
+ for (let i = 0; i < expected[key].length; i++) {
+ let attribute = expected[key][i];
+ if (typeof attribute === "string") {
+ assert_object_equals(
+ config[key][i],
+ { name: attribute, namespace: null },
+ `${prefix}.${key}[${i}] should match`,
+ );
+ } else {
+ assert_object_equals(
+ config[key][i],
+ attribute,
+ `${prefix}.${key}[${i}] should match`,
+ );
+ }
+ }
+ }
+
+ assert_attrs("attributes", config, expected);
+ assert_attrs("removeAttributes", config, expected);
+
+ function assert_elems(key) {
+ if (!(key in expected)) {
+ return;
+ }
+
+ if (expected[key] === undefined) {
+ assert_false(key in config, `Unexpected '${key}' in config`);
+ return;
+ }
+
+ assert_true(key in config, `Missing '${key}' from config`);
+ assert_equals(config[key]?.length, expected[key].length, `${key}.length`);
+
+ const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+ for (let i = 0; i < expected[key].length; i++) {
+ let element = expected[key][i];
+ // To make writing tests a bit easier we also support the shorthand string syntax.
+ if (typeof element === "string") {
+ let extra = key === "elements" ? { removeAttributes: [] } : { };
+ assert_object_equals(
+ config[key][i],
+ { name: element, namespace: XHTML_NS, ...extra },
+ `${key}[${i}] should match`,
+ );
+ } else {
+ if (key === "elements") {
+ assert_equals(config[key][i].name, element.name, `${key}[${i}].name should match`);
+ let ns = "namespace" in element ? element.namespace : XHTML_NS;
+ assert_equals(config[key][i].namespace, ns, `${key}[${i}].namespace should match`);
+
+ assert_attrs("attributes", config[key][i], element, `config.elements[${i}]`);
+ assert_attrs("removeAttributes", config[key][i], element, `config.elements[${i}]`);
+ } else {
+ assert_object_equals(config[key][i], element, `${key}[${i}] should match`);
+ }
+ }
+ }
+ }
+
+ assert_elems("elements");
+ assert_elems("removeElements");
+ assert_elems("replaceWithChildrenElements");
+}