mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-08 20:32:00 -04:00
Merge pull request #3604 from lightpanda-io/sanitizer
webapi: Start of Sanitizer WebAPI
This commit is contained in:
4 files changed
+1823
No files matched your search
@@ -1271,6 +1271,7 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/XPathExpression.zig"),
|
||||
@import("../webapi/XPathEvaluator.zig"),
|
||||
@import("../webapi/collections/DOMStringList.zig"),
|
||||
@import("../webapi/Sanitizer.zig"),
|
||||
});
|
||||
|
||||
// APIs available on EVERY worker global — dedicated, shared and service. This
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="testing.js"></script>
|
||||
<body></body>
|
||||
|
||||
<script id=constructor>
|
||||
{
|
||||
testing.expectEqual('function', typeof Sanitizer);
|
||||
testing.expectEqual(true, new Sanitizer() instanceof Sanitizer);
|
||||
testing.expectEqual(true, new Sanitizer({}) instanceof Sanitizer);
|
||||
testing.expectEqual(true, new Sanitizer(null) instanceof Sanitizer);
|
||||
testing.expectEqual(true, new Sanitizer(undefined) instanceof Sanitizer);
|
||||
testing.expectEqual(true, new Sanitizer('default') instanceof Sanitizer);
|
||||
testing.expectEqual(true, new Sanitizer({unknown: [1, 2]}) instanceof Sanitizer);
|
||||
testing.expectError('TypeError', () => new Sanitizer('nope'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=emptyConfig>
|
||||
{
|
||||
// Neither half of a pair given: canonicalization fills in the empty
|
||||
// remove-lists, which is what "allow everything" looks like.
|
||||
testing.expectEqual({
|
||||
removeElements: [],
|
||||
removeAttributes: [],
|
||||
removeProcessingInstructions: [],
|
||||
comments: true,
|
||||
javascriptURLs: true,
|
||||
}, new Sanitizer({}).get());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=canonicalization>
|
||||
{
|
||||
const get = (config) => new Sanitizer(config).get();
|
||||
|
||||
testing.expectEqual([{name: 'div', namespace: 'http://www.w3.org/1999/xhtml', removeAttributes: []}],
|
||||
get({elements: ['div']}).elements);
|
||||
testing.expectEqual([{name: 'b', namespace: 'http://www.w3.org/1999/xhtml'}],
|
||||
get({removeElements: [{name: 'b'}]}).removeElements);
|
||||
testing.expectEqual([{name: 'b', namespace: null}],
|
||||
get({removeElements: [{name: 'b', namespace: null}]}).removeElements);
|
||||
// An empty namespace is the null namespace.
|
||||
testing.expectEqual([{name: 'b', namespace: null}],
|
||||
get({removeElements: [{name: 'b', namespace: ''}]}).removeElements);
|
||||
|
||||
// Attributes default to the null namespace, elements to XHTML.
|
||||
testing.expectEqual([{name: 'href', namespace: null}], get({attributes: ['href']}).attributes);
|
||||
testing.expectEqual([{target: 'xml-stylesheet'}],
|
||||
get({processingInstructions: ['xml-stylesheet']}).processingInstructions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=sorting>
|
||||
{
|
||||
// Namespace first, with null below every URI; then local name.
|
||||
const config = new Sanitizer({attributes: [
|
||||
{name: 'b', namespace: 'http://example.org/'},
|
||||
{name: 'z'},
|
||||
{name: 'a', namespace: 'http://example.org/'},
|
||||
{name: 'a'},
|
||||
]}).get();
|
||||
|
||||
testing.expectEqual([
|
||||
{name: 'a', namespace: null},
|
||||
{name: 'z', namespace: null},
|
||||
{name: 'a', namespace: 'http://example.org/'},
|
||||
{name: 'b', namespace: 'http://example.org/'},
|
||||
], config.attributes);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=booleanDefaults>
|
||||
{
|
||||
testing.expectEqual(false, new Sanitizer().get().comments);
|
||||
testing.expectEqual(true, new Sanitizer({}).get().comments);
|
||||
testing.expectEqual(false, new Sanitizer({comments: false}).get().comments);
|
||||
|
||||
testing.expectEqual(false, new Sanitizer().get().dataAttributes);
|
||||
testing.expectEqual(true, new Sanitizer({attributes: []}).get().dataAttributes);
|
||||
// dataAttributes only exists alongside an attribute allow-list.
|
||||
testing.expectEqual(false, 'dataAttributes' in new Sanitizer({}).get());
|
||||
testing.expectEqual(false, 'dataAttributes' in new Sanitizer({removeAttributes: []}).get());
|
||||
|
||||
// Web IDL: an explicit null is ToBoolean(null) = false, while an explicit
|
||||
// undefined leaves the member absent and so takes the default.
|
||||
testing.expectEqual(false, new Sanitizer({comments: null}).get().comments);
|
||||
testing.expectEqual(true, new Sanitizer({comments: 'abc'}).get().comments);
|
||||
|
||||
// The setters report whether they changed anything.
|
||||
const s = new Sanitizer();
|
||||
testing.expectEqual(true, s.setComments(true));
|
||||
testing.expectEqual(false, s.setComments(true));
|
||||
testing.expectEqual(true, s.get().comments);
|
||||
|
||||
testing.expectEqual(true, s.setDataAttributes(true));
|
||||
testing.expectEqual(false, s.setDataAttributes(true));
|
||||
testing.expectEqual(true, s.get().dataAttributes);
|
||||
|
||||
// Without an attribute allow-list there is no dataAttributes to set.
|
||||
testing.expectEqual(false, new Sanitizer({removeAttributes: []}).setDataAttributes(true));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=javascriptUrls>
|
||||
{
|
||||
testing.expectEqual(false, new Sanitizer().get().javascriptURLs);
|
||||
testing.expectEqual(true, new Sanitizer({}).get().javascriptURLs);
|
||||
testing.expectEqual(false, new Sanitizer({javascriptURLs: false}).get().javascriptURLs);
|
||||
testing.expectEqual(false, new Sanitizer({javascriptURLs: null}).get().javascriptURLs);
|
||||
testing.expectEqual(true, new Sanitizer({javascriptURLs: undefined}).get().javascriptURLs);
|
||||
|
||||
const s = new Sanitizer();
|
||||
testing.expectEqual(true, s.setJavascriptURLs(true));
|
||||
testing.expectEqual(false, s.setJavascriptURLs(true));
|
||||
testing.expectEqual(true, s.get().javascriptURLs);
|
||||
// removeUnsafe turns them back off, and reports that it changed something.
|
||||
testing.expectEqual(true, s.removeUnsafe());
|
||||
testing.expectEqual(false, s.get().javascriptURLs);
|
||||
}
|
||||
|
||||
{
|
||||
// Allowing every data attribute subsumes any named individually, and the
|
||||
// config has to stay valid.
|
||||
const s = new Sanitizer({attributes: ['data-x', 'id'], elements: [{name: 'div', attributes: ['data-y']}]});
|
||||
testing.expectEqual(true, s.setDataAttributes(true));
|
||||
testing.expectEqual([{name: 'id', namespace: null}], s.get().attributes);
|
||||
testing.expectEqual([], s.get().elements[0].attributes);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=invalidConfigs>
|
||||
{
|
||||
// Each pair is mutually exclusive.
|
||||
testing.expectError('TypeError', () => new Sanitizer({elements: [], removeElements: []}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({attributes: [], removeAttributes: []}));
|
||||
|
||||
// No duplicates, whichever spelling they arrive in.
|
||||
testing.expectError('TypeError', () => new Sanitizer({elements: ['abc', 'abc']}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({elements: ['abc', {name: 'abc'}]}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({attributes: ['abc', {name: 'abc', namespace: null}]}));
|
||||
|
||||
// <html>, <svg> and <math> can never be replaced with their children.
|
||||
testing.expectError('TypeError', () => new Sanitizer({replaceWithChildrenElements: ['html']}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({
|
||||
replaceWithChildrenElements: [{name: 'svg', namespace: 'http://www.w3.org/2000/svg'}],
|
||||
}));
|
||||
// ... but in another namespace it is just an element.
|
||||
testing.expectEqual(1, new Sanitizer({replaceWithChildrenElements: ['svg']}).get().replaceWithChildrenElements.length);
|
||||
|
||||
testing.expectError('TypeError', () => new Sanitizer({elements: ['p'], replaceWithChildrenElements: ['p']}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({removeElements: ['p'], replaceWithChildrenElements: ['p']}));
|
||||
|
||||
// A per-element list may not restate a global one.
|
||||
testing.expectError('TypeError', () => new Sanitizer({
|
||||
attributes: ['id'],
|
||||
elements: [{name: 'div', attributes: ['id']}],
|
||||
}));
|
||||
// A per-element removeAttributes must be a subset of the global allow-list.
|
||||
testing.expectError('TypeError', () => new Sanitizer({
|
||||
attributes: ['class'],
|
||||
elements: [{name: 'div', removeAttributes: ['title']}],
|
||||
}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({
|
||||
attributes: ['data-bar'],
|
||||
dataAttributes: true,
|
||||
}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({removeAttributes: [], dataAttributes: false}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({
|
||||
removeAttributes: [],
|
||||
elements: [{name: 'div', attributes: [], removeAttributes: []}],
|
||||
}));
|
||||
|
||||
testing.expectError('TypeError', () => new Sanitizer({processingInstructions: [], removeProcessingInstructions: []}));
|
||||
testing.expectError('TypeError', () => new Sanitizer({removeProcessingInstructions: ['x', {target: 'x'}]}));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=perElementAttributes>
|
||||
{
|
||||
{
|
||||
const s = new Sanitizer({elements: [{name: 'div', attributes: ['href', 'src']}]});
|
||||
testing.expectEqual(true, 'attributes' in s.get().elements[0]);
|
||||
testing.expectEqual(false, 'removeAttributes' in s.get().elements[0]);
|
||||
testing.expectEqual(2, s.get().elements[0].attributes.length);
|
||||
|
||||
// allowElement overwrites the per-element list rather than merging.
|
||||
s.allowElement({name: 'div', namespace: 'http://www.w3.org/1999/xhtml', attributes: ['class']});
|
||||
testing.expectEqual([{name: 'class', namespace: null}], s.get().elements[0].attributes);
|
||||
}
|
||||
|
||||
{
|
||||
// An element with neither list still reports an empty remove-list.
|
||||
const s = new Sanitizer({elements: ['div']});
|
||||
testing.expectEqual([], s.get().elements[0].removeAttributes);
|
||||
testing.expectEqual(false, 'attributes' in s.get().elements[0]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=elementModifiers>
|
||||
{
|
||||
const s = new Sanitizer({elements: ['div', 'p']});
|
||||
testing.expectEqual(2, s.get().elements.length);
|
||||
testing.expectEqual(true, s.allowElement('bla'));
|
||||
testing.expectEqual(false, s.allowElement('bla'));
|
||||
testing.expectEqual(3, s.get().elements.length);
|
||||
testing.expectEqual(true, s.removeElement({name: 'div'}));
|
||||
testing.expectEqual(2, s.get().elements.length);
|
||||
testing.expectEqual(true, s.replaceElementWithChildren({name: 'p', namespace: 'http://www.w3.org/1999/xhtml'}));
|
||||
testing.expectEqual([{name: 'bla', namespace: 'http://www.w3.org/1999/xhtml', removeAttributes: []}],
|
||||
s.get().elements);
|
||||
testing.expectEqual([{name: 'p', namespace: 'http://www.w3.org/1999/xhtml'}],
|
||||
s.get().replaceWithChildrenElements);
|
||||
testing.expectEqual(false, s.replaceElementWithChildren('html'));
|
||||
}
|
||||
|
||||
{
|
||||
// With a remove-list the same calls flip meaning.
|
||||
const s = new Sanitizer({removeElements: ['div', 'p']});
|
||||
testing.expectEqual(true, s.removeElement('bla'));
|
||||
testing.expectEqual(3, s.get().removeElements.length);
|
||||
testing.expectEqual(true, s.allowElement('p'));
|
||||
testing.expectEqual(2, s.get().removeElements.length);
|
||||
// A remove-list config has nowhere to put per-element attributes.
|
||||
testing.expectEqual(false, s.allowElement({name: 'div', attributes: ['id']}));
|
||||
}
|
||||
|
||||
{
|
||||
// An element with neither attribute list is the same as one with an empty
|
||||
// removeAttributes, so re-allowing it either way is not a change.
|
||||
const s = new Sanitizer({elements: ['div', {name: 'p', removeAttributes: []}]});
|
||||
testing.expectEqual(false, s.allowElement({name: 'div', removeAttributes: []}));
|
||||
testing.expectEqual(false, s.allowElement('p'));
|
||||
testing.expectEqual(true, s.allowElement('span'));
|
||||
testing.expectEqual(false, s.allowElement({name: 'span', removeAttributes: []}));
|
||||
testing.expectEqual(true, s.allowElement({name: 'span', attributes: []}));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=attributeModifiers>
|
||||
{
|
||||
const s = new Sanitizer({attributes: ['href', 'src']});
|
||||
testing.expectEqual(true, s.allowAttribute('id'));
|
||||
testing.expectEqual(false, s.allowAttribute('id'));
|
||||
testing.expectEqual(3, s.get().attributes.length);
|
||||
// A different namespace is a different attribute.
|
||||
testing.expectEqual(false, s.removeAttribute({name: 'href', namespace: 'http://example.org/'}));
|
||||
testing.expectEqual(true, s.removeAttribute({name: 'href'}));
|
||||
testing.expectEqual(true, s.removeAttribute({name: 'src', namespace: null}));
|
||||
testing.expectEqual([{name: 'id', namespace: null}], s.get().attributes);
|
||||
}
|
||||
|
||||
{
|
||||
const s = new Sanitizer({removeAttributes: ['href', 'src']});
|
||||
testing.expectEqual(true, s.removeAttribute('id'));
|
||||
testing.expectEqual(3, s.get().removeAttributes.length);
|
||||
testing.expectEqual(false, s.allowAttribute({name: 'href', namespace: 'http://example.org/'}));
|
||||
testing.expectEqual(true, s.allowAttribute('href'));
|
||||
testing.expectEqual(true, s.allowAttribute({name: 'src', namespace: null}));
|
||||
testing.expectEqual([{name: 'id', namespace: null}], s.get().removeAttributes);
|
||||
}
|
||||
|
||||
{
|
||||
// A global allow subsumes the per-element lists.
|
||||
const s = new Sanitizer({
|
||||
attributes: ['id', 'title'],
|
||||
elements: [{name: 'div', attributes: ['class', 'dir'], removeAttributes: ['title']}],
|
||||
});
|
||||
testing.expectEqual(true, s.removeAttribute('dir'));
|
||||
testing.expectEqual([{name: 'class', namespace: null}], s.get().elements[0].attributes);
|
||||
testing.expectEqual(true, s.removeAttribute('title'));
|
||||
testing.expectEqual([{name: 'id', namespace: null}], s.get().attributes);
|
||||
testing.expectEqual([], s.get().elements[0].removeAttributes);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=processingInstructionModifiers>
|
||||
{
|
||||
const s = new Sanitizer({processingInstructions: ['target-1', 'target-2']});
|
||||
testing.expectEqual(true, s.allowProcessingInstruction('target-3'));
|
||||
testing.expectEqual(false, s.allowProcessingInstruction('target-3'));
|
||||
testing.expectEqual(false, s.removeProcessingInstruction({target: 'target-4'}));
|
||||
testing.expectEqual(true, s.removeProcessingInstruction({target: 'target-1'}));
|
||||
testing.expectEqual(true, s.removeProcessingInstruction({target: 'target-2'}));
|
||||
testing.expectEqual([{target: 'target-3'}], s.get().processingInstructions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=removeUnsafe>
|
||||
{
|
||||
const s = new Sanitizer({});
|
||||
testing.expectEqual(true, s.removeUnsafe());
|
||||
testing.expectEqual(false, s.removeUnsafe());
|
||||
|
||||
const config = s.get();
|
||||
testing.expectEqual(false, 'elements' in config);
|
||||
testing.expectEqual(false, 'attributes' in config);
|
||||
testing.expectEqual([
|
||||
{name: 'base', namespace: 'http://www.w3.org/1999/xhtml'},
|
||||
{name: 'embed', namespace: 'http://www.w3.org/1999/xhtml'},
|
||||
{name: 'frame', namespace: 'http://www.w3.org/1999/xhtml'},
|
||||
{name: 'iframe', namespace: 'http://www.w3.org/1999/xhtml'},
|
||||
{name: 'object', namespace: 'http://www.w3.org/1999/xhtml'},
|
||||
{name: 'script', namespace: 'http://www.w3.org/1999/xhtml'},
|
||||
{name: 'script', namespace: 'http://www.w3.org/2000/svg'},
|
||||
{name: 'use', namespace: 'http://www.w3.org/2000/svg'},
|
||||
], config.removeElements);
|
||||
|
||||
const names = config.removeAttributes.map((a) => a.name);
|
||||
testing.expectEqual(true, names.length > 0);
|
||||
for (const attribute of config.removeAttributes) {
|
||||
testing.expectEqual(null, attribute.namespace);
|
||||
testing.expectEqual(true, attribute.name.startsWith('on'));
|
||||
}
|
||||
testing.expectEqual(true, names.every((n, i) => i === 0 || names[i - 1] < n));
|
||||
|
||||
testing.expectEqual(true, names.includes('onclick'));
|
||||
}
|
||||
|
||||
{
|
||||
// Nothing in the default config is unsafe, so removeUnsafe is a no-op on it.
|
||||
const before = new Sanitizer('default').get();
|
||||
const after = new Sanitizer('default');
|
||||
testing.expectEqual(false, after.removeUnsafe());
|
||||
|
||||
testing.expectEqual(true, before.elements.length > 0);
|
||||
testing.expectEqual(before.elements.length, after.get().elements.length);
|
||||
testing.expectEqual(before.attributes.length, after.get().attributes.length);
|
||||
testing.expectEqual(false, 'removeElements' in after.get());
|
||||
testing.expectEqual(false, 'removeAttributes' in after.get());
|
||||
}
|
||||
|
||||
{
|
||||
// lightpanda-specific: our event handler list is HTML's merged with the
|
||||
// handlers we compile ourselves, so neither side can go missing. Another
|
||||
// engine ships only its own set and is expected to fail this.
|
||||
const s = new Sanitizer({});
|
||||
s.removeUnsafe();
|
||||
const names = s.get().removeAttributes.map((a) => a.name);
|
||||
for (const name of ['onunload', 'onmouseenter', 'onbeforematch', 'onfullscreenchange']) {
|
||||
testing.expectEqual(true, names.includes(name), {script_id: 'removeUnsafe'});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=defaultConfig>
|
||||
{
|
||||
const config = new Sanitizer().get();
|
||||
|
||||
testing.expectEqual(121, config.elements.length);
|
||||
testing.expectEqual(58, config.attributes.length);
|
||||
testing.expectEqual(false, config.comments);
|
||||
testing.expectEqual(false, config.dataAttributes);
|
||||
|
||||
// Sorted by namespace URI, which puts MathML (1998) first and SVG (2000) last.
|
||||
testing.expectEqual({name: 'math', namespace: 'http://www.w3.org/1998/Math/MathML', attributes: []},
|
||||
config.elements[0]);
|
||||
testing.expectEqual('http://www.w3.org/2000/svg', config.elements[config.elements.length - 1].namespace);
|
||||
|
||||
// Nothing that runs script is in there.
|
||||
const names = config.elements.map((e) => e.name);
|
||||
for (const unsafe of ['script', 'iframe', 'object', 'embed', 'frame']) {
|
||||
testing.expectEqual(false, names.includes(unsafe));
|
||||
}
|
||||
testing.expectEqual(false, config.attributes.some((a) => a.name.startsWith('on')));
|
||||
|
||||
testing.expectEqual([], config.processingInstructions);
|
||||
testing.expectEqual(false, config.javascriptURLs);
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,429 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// default configuration for Sanitizer
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const global_event_handlers = @import("global_event_handlers.zig");
|
||||
|
||||
const Namespace = @import("Sanitizer.zig").Namespace;
|
||||
|
||||
pub const xhtml_ns = "http://www.w3.org/1999/xhtml";
|
||||
pub const svg_ns = "http://www.w3.org/2000/svg";
|
||||
pub const mathml_ns = "http://www.w3.org/1998/Math/MathML";
|
||||
pub const xlink_ns = "http://www.w3.org/1999/xlink";
|
||||
pub const xml_ns = "http://www.w3.org/XML/1998/namespace";
|
||||
pub const xmlns_ns = "http://www.w3.org/2000/xmlns/";
|
||||
|
||||
// A name as a table writes it, and as one arrives from JS: still a plain slice,
|
||||
// because a `Sanitizer.Name` holds an `lp.String`, which cannot be built at
|
||||
// comptime past 12 bytes -- and `animateTransform` and friends are longer.
|
||||
// `Sanitizer.staticName` / `ownName` turn one of these into a `Name`.
|
||||
pub const Name = struct {
|
||||
name: []const u8,
|
||||
namespace: Namespace,
|
||||
};
|
||||
|
||||
pub const Element = struct {
|
||||
name: []const u8,
|
||||
namespace: Namespace,
|
||||
attributes: []const Name = &.{},
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/#built-in-non-replaceable-elements-list
|
||||
pub const non_replaceable_elements: []const Name = &.{
|
||||
.{ .name = "html", .namespace = .xhtml },
|
||||
.{ .name = "svg", .namespace = .svg },
|
||||
.{ .name = "math", .namespace = .mathml },
|
||||
};
|
||||
|
||||
// https://wicg.github.io/sanitizer-api/#built-in-safe-default-configuration
|
||||
pub const default_elements: []const Element = &.{
|
||||
.{ .name = "math", .namespace = .mathml },
|
||||
.{ .name = "merror", .namespace = .mathml },
|
||||
.{ .name = "mfrac", .namespace = .mathml },
|
||||
.{ .name = "mi", .namespace = .mathml },
|
||||
.{ .name = "mmultiscripts", .namespace = .mathml },
|
||||
.{ .name = "mn", .namespace = .mathml },
|
||||
.{ .name = "mo", .namespace = .mathml, .attributes = &.{ .{ .name = "fence", .namespace = .none }, .{ .name = "form", .namespace = .none }, .{ .name = "largeop", .namespace = .none }, .{ .name = "lspace", .namespace = .none }, .{ .name = "maxsize", .namespace = .none }, .{ .name = "minsize", .namespace = .none }, .{ .name = "movablelimits", .namespace = .none }, .{ .name = "rspace", .namespace = .none }, .{ .name = "separator", .namespace = .none }, .{ .name = "stretchy", .namespace = .none }, .{ .name = "symmetric", .namespace = .none } } },
|
||||
.{ .name = "mover", .namespace = .mathml, .attributes = &.{.{ .name = "accent", .namespace = .none }} },
|
||||
.{ .name = "mpadded", .namespace = .mathml, .attributes = &.{ .{ .name = "depth", .namespace = .none }, .{ .name = "height", .namespace = .none }, .{ .name = "lspace", .namespace = .none }, .{ .name = "voffset", .namespace = .none }, .{ .name = "width", .namespace = .none } } },
|
||||
.{ .name = "mphantom", .namespace = .mathml },
|
||||
.{ .name = "mprescripts", .namespace = .mathml },
|
||||
.{ .name = "mroot", .namespace = .mathml },
|
||||
.{ .name = "mrow", .namespace = .mathml },
|
||||
.{ .name = "ms", .namespace = .mathml },
|
||||
.{ .name = "mspace", .namespace = .mathml, .attributes = &.{ .{ .name = "depth", .namespace = .none }, .{ .name = "height", .namespace = .none }, .{ .name = "width", .namespace = .none } } },
|
||||
.{ .name = "msqrt", .namespace = .mathml },
|
||||
.{ .name = "mstyle", .namespace = .mathml },
|
||||
.{ .name = "msub", .namespace = .mathml },
|
||||
.{ .name = "msubsup", .namespace = .mathml },
|
||||
.{ .name = "msup", .namespace = .mathml },
|
||||
.{ .name = "mtable", .namespace = .mathml },
|
||||
.{ .name = "mtd", .namespace = .mathml, .attributes = &.{ .{ .name = "columnspan", .namespace = .none }, .{ .name = "rowspan", .namespace = .none } } },
|
||||
.{ .name = "mtext", .namespace = .mathml },
|
||||
.{ .name = "mtr", .namespace = .mathml },
|
||||
.{ .name = "munder", .namespace = .mathml, .attributes = &.{.{ .name = "accentunder", .namespace = .none }} },
|
||||
.{ .name = "munderover", .namespace = .mathml, .attributes = &.{ .{ .name = "accent", .namespace = .none }, .{ .name = "accentunder", .namespace = .none } } },
|
||||
.{ .name = "semantics", .namespace = .mathml },
|
||||
.{ .name = "a", .namespace = .xhtml, .attributes = &.{ .{ .name = "href", .namespace = .none }, .{ .name = "hreflang", .namespace = .none }, .{ .name = "type", .namespace = .none } } },
|
||||
.{ .name = "abbr", .namespace = .xhtml },
|
||||
.{ .name = "address", .namespace = .xhtml },
|
||||
.{ .name = "article", .namespace = .xhtml },
|
||||
.{ .name = "aside", .namespace = .xhtml },
|
||||
.{ .name = "b", .namespace = .xhtml },
|
||||
.{ .name = "bdi", .namespace = .xhtml },
|
||||
.{ .name = "bdo", .namespace = .xhtml },
|
||||
.{ .name = "blockquote", .namespace = .xhtml, .attributes = &.{.{ .name = "cite", .namespace = .none }} },
|
||||
.{ .name = "body", .namespace = .xhtml },
|
||||
.{ .name = "br", .namespace = .xhtml },
|
||||
.{ .name = "caption", .namespace = .xhtml },
|
||||
.{ .name = "cite", .namespace = .xhtml },
|
||||
.{ .name = "code", .namespace = .xhtml },
|
||||
.{ .name = "col", .namespace = .xhtml, .attributes = &.{.{ .name = "span", .namespace = .none }} },
|
||||
.{ .name = "colgroup", .namespace = .xhtml, .attributes = &.{.{ .name = "span", .namespace = .none }} },
|
||||
.{ .name = "data", .namespace = .xhtml, .attributes = &.{.{ .name = "value", .namespace = .none }} },
|
||||
.{ .name = "dd", .namespace = .xhtml },
|
||||
.{ .name = "del", .namespace = .xhtml, .attributes = &.{ .{ .name = "cite", .namespace = .none }, .{ .name = "datetime", .namespace = .none } } },
|
||||
.{ .name = "dfn", .namespace = .xhtml },
|
||||
.{ .name = "div", .namespace = .xhtml },
|
||||
.{ .name = "dl", .namespace = .xhtml },
|
||||
.{ .name = "dt", .namespace = .xhtml },
|
||||
.{ .name = "em", .namespace = .xhtml },
|
||||
.{ .name = "figcaption", .namespace = .xhtml },
|
||||
.{ .name = "figure", .namespace = .xhtml },
|
||||
.{ .name = "footer", .namespace = .xhtml },
|
||||
.{ .name = "h1", .namespace = .xhtml },
|
||||
.{ .name = "h2", .namespace = .xhtml },
|
||||
.{ .name = "h3", .namespace = .xhtml },
|
||||
.{ .name = "h4", .namespace = .xhtml },
|
||||
.{ .name = "h5", .namespace = .xhtml },
|
||||
.{ .name = "h6", .namespace = .xhtml },
|
||||
.{ .name = "head", .namespace = .xhtml },
|
||||
.{ .name = "header", .namespace = .xhtml },
|
||||
.{ .name = "hgroup", .namespace = .xhtml },
|
||||
.{ .name = "hr", .namespace = .xhtml },
|
||||
.{ .name = "html", .namespace = .xhtml },
|
||||
.{ .name = "i", .namespace = .xhtml },
|
||||
.{ .name = "ins", .namespace = .xhtml, .attributes = &.{ .{ .name = "cite", .namespace = .none }, .{ .name = "datetime", .namespace = .none } } },
|
||||
.{ .name = "kbd", .namespace = .xhtml },
|
||||
.{ .name = "li", .namespace = .xhtml, .attributes = &.{.{ .name = "value", .namespace = .none }} },
|
||||
.{ .name = "main", .namespace = .xhtml },
|
||||
.{ .name = "mark", .namespace = .xhtml },
|
||||
.{ .name = "menu", .namespace = .xhtml },
|
||||
.{ .name = "nav", .namespace = .xhtml },
|
||||
.{ .name = "ol", .namespace = .xhtml, .attributes = &.{ .{ .name = "reversed", .namespace = .none }, .{ .name = "start", .namespace = .none }, .{ .name = "type", .namespace = .none } } },
|
||||
.{ .name = "p", .namespace = .xhtml },
|
||||
.{ .name = "pre", .namespace = .xhtml },
|
||||
.{ .name = "q", .namespace = .xhtml },
|
||||
.{ .name = "rp", .namespace = .xhtml },
|
||||
.{ .name = "rt", .namespace = .xhtml },
|
||||
.{ .name = "ruby", .namespace = .xhtml },
|
||||
.{ .name = "s", .namespace = .xhtml },
|
||||
.{ .name = "samp", .namespace = .xhtml },
|
||||
.{ .name = "search", .namespace = .xhtml },
|
||||
.{ .name = "section", .namespace = .xhtml },
|
||||
.{ .name = "small", .namespace = .xhtml },
|
||||
.{ .name = "span", .namespace = .xhtml },
|
||||
.{ .name = "strong", .namespace = .xhtml },
|
||||
.{ .name = "sub", .namespace = .xhtml },
|
||||
.{ .name = "sup", .namespace = .xhtml },
|
||||
.{ .name = "table", .namespace = .xhtml },
|
||||
.{ .name = "tbody", .namespace = .xhtml },
|
||||
.{ .name = "td", .namespace = .xhtml, .attributes = &.{ .{ .name = "colspan", .namespace = .none }, .{ .name = "headers", .namespace = .none }, .{ .name = "rowspan", .namespace = .none } } },
|
||||
.{ .name = "tfoot", .namespace = .xhtml },
|
||||
.{ .name = "th", .namespace = .xhtml, .attributes = &.{ .{ .name = "abbr", .namespace = .none }, .{ .name = "colspan", .namespace = .none }, .{ .name = "headers", .namespace = .none }, .{ .name = "rowspan", .namespace = .none }, .{ .name = "scope", .namespace = .none } } },
|
||||
.{ .name = "thead", .namespace = .xhtml },
|
||||
.{ .name = "time", .namespace = .xhtml, .attributes = &.{.{ .name = "datetime", .namespace = .none }} },
|
||||
.{ .name = "title", .namespace = .xhtml },
|
||||
.{ .name = "tr", .namespace = .xhtml },
|
||||
.{ .name = "u", .namespace = .xhtml },
|
||||
.{ .name = "ul", .namespace = .xhtml },
|
||||
.{ .name = "var", .namespace = .xhtml },
|
||||
.{ .name = "wbr", .namespace = .xhtml },
|
||||
.{ .name = "a", .namespace = .svg, .attributes = &.{ .{ .name = "href", .namespace = .none }, .{ .name = "hreflang", .namespace = .none }, .{ .name = "type", .namespace = .none } } },
|
||||
.{ .name = "circle", .namespace = .svg, .attributes = &.{ .{ .name = "cx", .namespace = .none }, .{ .name = "cy", .namespace = .none }, .{ .name = "pathLength", .namespace = .none }, .{ .name = "r", .namespace = .none } } },
|
||||
.{ .name = "defs", .namespace = .svg },
|
||||
.{ .name = "desc", .namespace = .svg },
|
||||
.{ .name = "ellipse", .namespace = .svg, .attributes = &.{ .{ .name = "cx", .namespace = .none }, .{ .name = "cy", .namespace = .none }, .{ .name = "pathLength", .namespace = .none }, .{ .name = "rx", .namespace = .none }, .{ .name = "ry", .namespace = .none } } },
|
||||
.{ .name = "foreignObject", .namespace = .svg, .attributes = &.{ .{ .name = "height", .namespace = .none }, .{ .name = "width", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
|
||||
.{ .name = "g", .namespace = .svg },
|
||||
.{ .name = "line", .namespace = .svg, .attributes = &.{ .{ .name = "pathLength", .namespace = .none }, .{ .name = "x1", .namespace = .none }, .{ .name = "x2", .namespace = .none }, .{ .name = "y1", .namespace = .none }, .{ .name = "y2", .namespace = .none } } },
|
||||
.{ .name = "marker", .namespace = .svg, .attributes = &.{ .{ .name = "markerHeight", .namespace = .none }, .{ .name = "markerUnits", .namespace = .none }, .{ .name = "markerWidth", .namespace = .none }, .{ .name = "orient", .namespace = .none }, .{ .name = "preserveAspectRatio", .namespace = .none }, .{ .name = "refX", .namespace = .none }, .{ .name = "refY", .namespace = .none }, .{ .name = "viewBox", .namespace = .none } } },
|
||||
.{ .name = "metadata", .namespace = .svg },
|
||||
.{ .name = "path", .namespace = .svg, .attributes = &.{ .{ .name = "d", .namespace = .none }, .{ .name = "pathLength", .namespace = .none } } },
|
||||
.{ .name = "polygon", .namespace = .svg, .attributes = &.{ .{ .name = "pathLength", .namespace = .none }, .{ .name = "points", .namespace = .none } } },
|
||||
.{ .name = "polyline", .namespace = .svg, .attributes = &.{ .{ .name = "pathLength", .namespace = .none }, .{ .name = "points", .namespace = .none } } },
|
||||
.{ .name = "rect", .namespace = .svg, .attributes = &.{ .{ .name = "height", .namespace = .none }, .{ .name = "pathLength", .namespace = .none }, .{ .name = "rx", .namespace = .none }, .{ .name = "ry", .namespace = .none }, .{ .name = "width", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
|
||||
.{ .name = "svg", .namespace = .svg, .attributes = &.{ .{ .name = "height", .namespace = .none }, .{ .name = "preserveAspectRatio", .namespace = .none }, .{ .name = "viewBox", .namespace = .none }, .{ .name = "width", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
|
||||
.{ .name = "text", .namespace = .svg, .attributes = &.{ .{ .name = "dx", .namespace = .none }, .{ .name = "dy", .namespace = .none }, .{ .name = "lengthAdjust", .namespace = .none }, .{ .name = "rotate", .namespace = .none }, .{ .name = "textLength", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
|
||||
.{ .name = "textPath", .namespace = .svg, .attributes = &.{ .{ .name = "lengthAdjust", .namespace = .none }, .{ .name = "method", .namespace = .none }, .{ .name = "path", .namespace = .none }, .{ .name = "side", .namespace = .none }, .{ .name = "spacing", .namespace = .none }, .{ .name = "startOffset", .namespace = .none }, .{ .name = "textLength", .namespace = .none } } },
|
||||
.{ .name = "title", .namespace = .svg },
|
||||
.{ .name = "tspan", .namespace = .svg, .attributes = &.{ .{ .name = "dx", .namespace = .none }, .{ .name = "dy", .namespace = .none }, .{ .name = "lengthAdjust", .namespace = .none }, .{ .name = "rotate", .namespace = .none }, .{ .name = "textLength", .namespace = .none }, .{ .name = "x", .namespace = .none }, .{ .name = "y", .namespace = .none } } },
|
||||
};
|
||||
|
||||
pub const default_attributes: []const Name = &.{
|
||||
.{ .name = "alignment-baseline", .namespace = .none },
|
||||
.{ .name = "baseline-shift", .namespace = .none },
|
||||
.{ .name = "clip-path", .namespace = .none },
|
||||
.{ .name = "clip-rule", .namespace = .none },
|
||||
.{ .name = "color", .namespace = .none },
|
||||
.{ .name = "color-interpolation", .namespace = .none },
|
||||
.{ .name = "cursor", .namespace = .none },
|
||||
.{ .name = "dir", .namespace = .none },
|
||||
.{ .name = "direction", .namespace = .none },
|
||||
.{ .name = "display", .namespace = .none },
|
||||
.{ .name = "displaystyle", .namespace = .none },
|
||||
.{ .name = "dominant-baseline", .namespace = .none },
|
||||
.{ .name = "fill", .namespace = .none },
|
||||
.{ .name = "fill-opacity", .namespace = .none },
|
||||
.{ .name = "fill-rule", .namespace = .none },
|
||||
.{ .name = "font-family", .namespace = .none },
|
||||
.{ .name = "font-size", .namespace = .none },
|
||||
.{ .name = "font-size-adjust", .namespace = .none },
|
||||
.{ .name = "font-stretch", .namespace = .none },
|
||||
.{ .name = "font-style", .namespace = .none },
|
||||
.{ .name = "font-variant", .namespace = .none },
|
||||
.{ .name = "font-weight", .namespace = .none },
|
||||
.{ .name = "lang", .namespace = .none },
|
||||
.{ .name = "letter-spacing", .namespace = .none },
|
||||
.{ .name = "marker-end", .namespace = .none },
|
||||
.{ .name = "marker-mid", .namespace = .none },
|
||||
.{ .name = "marker-start", .namespace = .none },
|
||||
.{ .name = "mathbackground", .namespace = .none },
|
||||
.{ .name = "mathcolor", .namespace = .none },
|
||||
.{ .name = "mathsize", .namespace = .none },
|
||||
.{ .name = "opacity", .namespace = .none },
|
||||
.{ .name = "paint-order", .namespace = .none },
|
||||
.{ .name = "pointer-events", .namespace = .none },
|
||||
.{ .name = "scriptlevel", .namespace = .none },
|
||||
.{ .name = "shape-rendering", .namespace = .none },
|
||||
.{ .name = "stop-color", .namespace = .none },
|
||||
.{ .name = "stop-opacity", .namespace = .none },
|
||||
.{ .name = "stroke", .namespace = .none },
|
||||
.{ .name = "stroke-dasharray", .namespace = .none },
|
||||
.{ .name = "stroke-dashoffset", .namespace = .none },
|
||||
.{ .name = "stroke-linecap", .namespace = .none },
|
||||
.{ .name = "stroke-linejoin", .namespace = .none },
|
||||
.{ .name = "stroke-miterlimit", .namespace = .none },
|
||||
.{ .name = "stroke-opacity", .namespace = .none },
|
||||
.{ .name = "stroke-width", .namespace = .none },
|
||||
.{ .name = "text-anchor", .namespace = .none },
|
||||
.{ .name = "text-decoration", .namespace = .none },
|
||||
.{ .name = "text-overflow", .namespace = .none },
|
||||
.{ .name = "text-rendering", .namespace = .none },
|
||||
.{ .name = "title", .namespace = .none },
|
||||
.{ .name = "transform", .namespace = .none },
|
||||
.{ .name = "transform-origin", .namespace = .none },
|
||||
.{ .name = "unicode-bidi", .namespace = .none },
|
||||
.{ .name = "vector-effect", .namespace = .none },
|
||||
.{ .name = "visibility", .namespace = .none },
|
||||
.{ .name = "white-space", .namespace = .none },
|
||||
.{ .name = "word-spacing", .namespace = .none },
|
||||
.{ .name = "writing-mode", .namespace = .none },
|
||||
};
|
||||
|
||||
// https://html.spec.whatwg.org/#built-in-safe-baseline-configuration
|
||||
// Every HTML element the spec marks "Sanitization: Unsafe" (base, embed,
|
||||
// iframe, object, script), plus the obsolete frame and SVG's script and use.
|
||||
pub const baseline_remove_elements: []const Name = &.{
|
||||
.{ .name = "base", .namespace = .xhtml },
|
||||
.{ .name = "embed", .namespace = .xhtml },
|
||||
.{ .name = "frame", .namespace = .xhtml },
|
||||
.{ .name = "iframe", .namespace = .xhtml },
|
||||
.{ .name = "object", .namespace = .xhtml },
|
||||
.{ .name = "script", .namespace = .xhtml },
|
||||
.{ .name = "script", .namespace = .svg },
|
||||
.{ .name = "use", .namespace = .svg },
|
||||
};
|
||||
|
||||
// The baseline's own removeAttributes list is empty; `remove unsafe` instead
|
||||
// walks every "event handler content attribute". We fold lightpanda's own
|
||||
// handler set into HTML's list so that a handler added to `Handler` -- which is
|
||||
// what an `on*` content attribute is compiled against -- can never be left
|
||||
// behind by removeUnsafe().
|
||||
pub const event_handler_attributes: []const []const u8 = blk: {
|
||||
@setEvalBranchQuota(200_000);
|
||||
const handlers = std.meta.fieldNames(global_event_handlers.Handler);
|
||||
var all: [html_event_handler_attributes.len + handlers.len][]const u8 = undefined;
|
||||
for (html_event_handler_attributes, 0..) |name, i| {
|
||||
all[i] = name;
|
||||
}
|
||||
for (handlers, 0..) |name, i| {
|
||||
all[html_event_handler_attributes.len + i] = name;
|
||||
}
|
||||
std.mem.sort([]const u8, &all, {}, struct {
|
||||
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
|
||||
return std.mem.lessThan(u8, a, b);
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
var unique: [all.len][]const u8 = undefined;
|
||||
var len: usize = 0;
|
||||
for (all) |name| {
|
||||
if (len == 0 or std.mem.eql(u8, unique[len - 1], name) == false) {
|
||||
unique[len] = name;
|
||||
len += 1;
|
||||
}
|
||||
}
|
||||
const final = unique[0..len].*;
|
||||
break :blk &final;
|
||||
};
|
||||
|
||||
const html_event_handler_attributes: []const []const u8 = &.{
|
||||
"onabort",
|
||||
"onactivate",
|
||||
"onafterprint",
|
||||
"onanimationcancel",
|
||||
"onanimationend",
|
||||
"onanimationiteration",
|
||||
"onanimationstart",
|
||||
"onautofill",
|
||||
"onauxclick",
|
||||
"onbeforecopy",
|
||||
"onbeforecut",
|
||||
"onbeforefilter",
|
||||
"onbeforeinput",
|
||||
"onbeforepaste",
|
||||
"onbeforeprint",
|
||||
"onbeforetoggle",
|
||||
"onbeforeunload",
|
||||
"onbegin",
|
||||
"onblur",
|
||||
"oncancel",
|
||||
"oncanplay",
|
||||
"oncanplaythrough",
|
||||
"onchange",
|
||||
"onclick",
|
||||
"onclose",
|
||||
"oncommand",
|
||||
"oncontentvisibilityautostatechange",
|
||||
"oncontextlost",
|
||||
"oncontextmenu",
|
||||
"oncontextrestored",
|
||||
"oncopy",
|
||||
"oncuechange",
|
||||
"oncut",
|
||||
"ondblclick",
|
||||
"ondrag",
|
||||
"ondragend",
|
||||
"ondragenter",
|
||||
"ondragleave",
|
||||
"ondragover",
|
||||
"ondragstart",
|
||||
"ondrop",
|
||||
"ondurationchange",
|
||||
"onemptied",
|
||||
"onend",
|
||||
"onended",
|
||||
"onerror",
|
||||
"onfocus",
|
||||
"onfocusin",
|
||||
"onfocusout",
|
||||
"onformdata",
|
||||
"ongotpointercapture",
|
||||
"onhashchange",
|
||||
"oninput",
|
||||
"oninstallresult",
|
||||
"oninvalid",
|
||||
"onkeydown",
|
||||
"onkeypress",
|
||||
"onkeyup",
|
||||
"onlanguagechange",
|
||||
"onload",
|
||||
"onloadeddata",
|
||||
"onloadedmetadata",
|
||||
"onloadstart",
|
||||
"onlocation",
|
||||
"onlostpointercapture",
|
||||
"onmessage",
|
||||
"onmessageerror",
|
||||
"onmousedown",
|
||||
"onmouseenter",
|
||||
"onmouseleave",
|
||||
"onmousemove",
|
||||
"onmouseout",
|
||||
"onmouseover",
|
||||
"onmouseup",
|
||||
"onmousewheel",
|
||||
"onmove",
|
||||
"onoffline",
|
||||
"ononline",
|
||||
"onorientationchange",
|
||||
"onpagehide",
|
||||
"onpageshow",
|
||||
"onpaste",
|
||||
"onpause",
|
||||
"onplay",
|
||||
"onplaying",
|
||||
"onpointercancel",
|
||||
"onpointerdown",
|
||||
"onpointerenter",
|
||||
"onpointerleave",
|
||||
"onpointermove",
|
||||
"onpointerout",
|
||||
"onpointerover",
|
||||
"onpointerrawupdate",
|
||||
"onpointerup",
|
||||
"onpopstate",
|
||||
"onprogress",
|
||||
"onpromptaction",
|
||||
"onpromptdismiss",
|
||||
"onratechange",
|
||||
"onrepeat",
|
||||
"onreset",
|
||||
"onresize",
|
||||
"onscroll",
|
||||
"onscrollend",
|
||||
"onscrollsnapchange",
|
||||
"onscrollsnapchanging",
|
||||
"onsearch",
|
||||
"onsecuritypolicyviolation",
|
||||
"onseeked",
|
||||
"onseeking",
|
||||
"onselect",
|
||||
"onselectionchange",
|
||||
"onselectstart",
|
||||
"onshow",
|
||||
"onslotchange",
|
||||
"onstalled",
|
||||
"onstream",
|
||||
"onstorage",
|
||||
"onsubmit",
|
||||
"onsuspend",
|
||||
"ontimeupdate",
|
||||
"ontimezonechange",
|
||||
"ontoggle",
|
||||
"ontouchcancel",
|
||||
"ontouchend",
|
||||
"ontouchmove",
|
||||
"ontouchstart",
|
||||
"ontransitionend",
|
||||
"onunload",
|
||||
"onvalidationstatuschange",
|
||||
"onvolumechange",
|
||||
"onwaiting",
|
||||
"onwebkitanimationend",
|
||||
"onwebkitanimationiteration",
|
||||
"onwebkitanimationstart",
|
||||
"onwebkitfullscreenchange",
|
||||
"onwebkitfullscreenerror",
|
||||
"onwebkittransitionend",
|
||||
"onwheel",
|
||||
};
|
||||
Reference in new issue
Block a user