mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-10-09 12:51:45 -04:00
webapi: Sanitizer API usage
Builds ontop of https://github.com/lightpanda-io/browser/pull/3604 to actually use a Sanitizer across 6 apis (some of which we didn't have at all, and some of which were incomplete). Namely setHtml and setHTMLUnsafe for Element, ShadowRoot and Document.
This commit is contained in:
11 files changed
+724
-56
No files matched your search
@@ -16,12 +16,15 @@
|
||||
// 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/>.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Parser = @import("../parser/Parser.zig");
|
||||
|
||||
const Node = @import("../webapi/Node.zig");
|
||||
const Element = @import("../webapi/Element.zig");
|
||||
const Document = @import("../webapi/Document.zig");
|
||||
const HTMLDocument = @import("../webapi/HTMLDocument.zig");
|
||||
const ShadowRoot = @import("../webapi/ShadowRoot.zig");
|
||||
const slotting = @import("../webapi/element/slotting.zig");
|
||||
|
||||
@@ -101,6 +104,44 @@ pub fn fragment(frame: *Frame, node: *Node, html: []const u8, opts: FragmentPars
|
||||
try Element.Html.Picture.childrenInserted(node, frame);
|
||||
}
|
||||
|
||||
pub const HtmlDocumentOpts = struct {
|
||||
allow_declarative_shadow: bool = false,
|
||||
};
|
||||
|
||||
// Build a detached HTMLDocument from `html` (DOMParser.parseFromString and
|
||||
// Document.parseHTML). The caller sets its URL.
|
||||
pub fn htmlDocument(frame: *Frame, html: []const u8, opts: HtmlDocumentOpts) !*HTMLDocument {
|
||||
const arena = try frame.getArena(.medium, "parse.htmlDocument");
|
||||
defer arena.release();
|
||||
|
||||
// Frame-side hooks triggered from `Build.created` / `nodeIsReady`
|
||||
// (external stylesheet fetches, script execution, mutation-observer
|
||||
// fan-out, default-script injection) treat the parsed nodes as detached
|
||||
// and skip side effects on the live document.
|
||||
const previous_parse_mode = frame._parse_mode;
|
||||
frame._parse_mode = .fragment;
|
||||
defer frame._parse_mode = previous_parse_mode;
|
||||
|
||||
const doc = try frame._factory.document(HTMLDocument{ ._proto = undefined });
|
||||
|
||||
var normalized = std.mem.trim(u8, html, &std.ascii.whitespace);
|
||||
if (normalized.len == 0) {
|
||||
normalized = "<html></html>";
|
||||
}
|
||||
|
||||
var parser = Parser.init(arena.allocator(), doc.asNode(), frame, .{
|
||||
.allow_declarative_shadow = opts.allow_declarative_shadow,
|
||||
});
|
||||
parser.parse(normalized);
|
||||
if (parser.terminated) {
|
||||
return error.ExecutionTerminated;
|
||||
}
|
||||
if (parser.err) |pe| {
|
||||
return pe.err;
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
// Build a detached XMLDocument from `xml` (DOMParser.parseFromString and
|
||||
// XMLHttpRequest.responseXML). Returns null when the input isn't well-formed
|
||||
// XML.
|
||||
|
||||
@@ -952,7 +952,7 @@ test "browser.markdown: declarative shadow DOM renders through piercing" {
|
||||
const host = try doc.createElement("div", null, frame);
|
||||
try host.setHTMLUnsafe(
|
||||
\\<div><template shadowrootmode="open"><p>shadow content</p></template></div>
|
||||
, frame);
|
||||
, null, frame);
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
|
||||
@@ -1468,7 +1468,7 @@ test "browser.screenshot: shadow dom and slots" {
|
||||
const div = try doc.createElement("div", null, frame);
|
||||
try div.setHTMLUnsafe(
|
||||
\\<x-host><template shadowrootmode="open"><p>shadow <slot></slot></p></template>light</x-host>
|
||||
, frame);
|
||||
, null, frame);
|
||||
|
||||
var builder: Builder = .{ .arena = testing.arena_allocator, .frame = frame, .tree = .{ .frame = frame, .state = .{ .root = div.asNode() } } };
|
||||
try builder.render(div.asNode());
|
||||
|
||||
@@ -368,3 +368,236 @@
|
||||
testing.expectEqual(false, config.javascriptURLs);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=setHTML>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.setHTML('<p id=a onclick="x()" title=t data-x=1>hi<script>boom()<\/script><!-- c --></p><iframe></iframe>');
|
||||
testing.expectEqual('<p title="t">hi</p>', div.innerHTML);
|
||||
|
||||
// an explicit "default" is the same thing
|
||||
div.setHTML('<b onclick="x()">b</b>', {sanitizer: 'default'});
|
||||
testing.expectEqual('<b>b</b>', div.innerHTML);
|
||||
|
||||
testing.expectError('TypeError', () => div.setHTML('x', {sanitizer: 'nope'}));
|
||||
testing.expectError('TypeError', () => div.setHTML('x', {sanitizer: {elements: [], removeElements: []}}));
|
||||
|
||||
div.setHTML('');
|
||||
testing.expectEqual('', div.innerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=setHTMLConfig>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
|
||||
// A dictionary on a safe method defaults to not keeping comments or data-*.
|
||||
div.setHTML('<p data-x=1 id=i>a<!--c--></p>', {sanitizer: {attributes: ['id']}});
|
||||
testing.expectEqual('<p id="i">a</p>', div.innerHTML);
|
||||
|
||||
// ... but unsafe methods, like the constructor, default to keeping them.
|
||||
div.setHTMLUnsafe('<p data-x=1 id=i>a<!--c--></p>', {sanitizer: {attributes: ['id']}});
|
||||
testing.expectEqual('<p data-x="1" id="i">a<!--c--></p>', div.innerHTML);
|
||||
|
||||
div.setHTML('<div><p>a<b>b</b></p><i>i</i></div>', {sanitizer: {elements: ['div', 'p']}});
|
||||
testing.expectEqual('<div><p>a</p></div>', div.innerHTML);
|
||||
|
||||
div.setHTML('<div><p>a<b>b<i>i</i></b>c</p></div>', {sanitizer: {replaceWithChildrenElements: ['b', 'p']}});
|
||||
testing.expectEqual('<div>ab<i>i</i>c</div>', div.innerHTML);
|
||||
|
||||
div.setHTML('<div><span>s</span><em>e</em></div>', {sanitizer: {removeElements: ['span']}});
|
||||
testing.expectEqual('<div><em>e</em></div>', div.innerHTML);
|
||||
|
||||
// per-element attribute lists
|
||||
div.setHTML('<p id=1 title=2>a</p><em id=3 title=4>b</em>', {sanitizer: {
|
||||
elements: [{name: 'p', attributes: ['id']}, {name: 'em', removeAttributes: ['title']}],
|
||||
attributes: ['title'],
|
||||
}});
|
||||
testing.expectEqual('<p id="1" title="2">a</p><em>b</em>', div.innerHTML);
|
||||
|
||||
// an element's removeAttributes must be a subset of the global attributes
|
||||
testing.expectError('TypeError', () => div.setHTML('x', {sanitizer: {
|
||||
elements: [{name: 'em', removeAttributes: ['id']}],
|
||||
attributes: ['title'],
|
||||
}}));
|
||||
|
||||
// A safe method still removes unsafe markup the config allows.
|
||||
div.setHTML('<script>x()<\/script><b onclick=x()>b</b>', {sanitizer: {elements: ['script', 'b'], attributes: ['onclick']}});
|
||||
testing.expectEqual('<b>b</b>', div.innerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=setHTMLUnsafe>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.setHTMLUnsafe('<b onclick="x()">b</b><script>x()<\/script><!--c-->');
|
||||
testing.expectEqual('<b onclick="x()">b</b><script>x()<\/script><!--c-->', div.innerHTML);
|
||||
|
||||
// The sanitizer's own configuration is used as-is.
|
||||
const sanitizer = new Sanitizer({removeElements: ['i']});
|
||||
const before = sanitizer.get();
|
||||
div.setHTMLUnsafe('<b onclick="x()">b</b><i>i</i>', {sanitizer});
|
||||
testing.expectEqual('<b onclick="x()">b</b>', div.innerHTML);
|
||||
|
||||
// A safe method must not remove unsafe from the caller's Sanitizer.
|
||||
div.setHTML('<b onclick="x()">b</b><i>i</i>', {sanitizer});
|
||||
testing.expectEqual('<b>b</b>', div.innerHTML);
|
||||
testing.expectEqual(before, sanitizer.get());
|
||||
|
||||
// The default configuration on an unsafe method is still the default one
|
||||
div.setHTMLUnsafe('<b onclick="x()">b</b><script><\/script>', {sanitizer: 'default'});
|
||||
testing.expectEqual('<b>b</b>', div.innerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=textMerge>
|
||||
{
|
||||
// Text left adjacent by a removal is merged, as it would be had the node
|
||||
// never been parsed.
|
||||
const div = document.createElement('div');
|
||||
div.setHTML('a <!-- c --> b');
|
||||
testing.expectEqual(1, div.childNodes.length);
|
||||
testing.expectEqual('a b', div.firstChild.data);
|
||||
|
||||
div.setHTML('<p>x<script>s<\/script>y<!--c-->z<b>b</b></p>');
|
||||
testing.expectEqual(2, div.firstChild.childNodes.length);
|
||||
testing.expectEqual('xyz', div.firstChild.firstChild.data);
|
||||
|
||||
// replaced elements merge at both edges
|
||||
div.setHTML('<p>x<span>y<i>i</i>z</span>w</p>', {sanitizer: {replaceWithChildrenElements: ['span']}});
|
||||
const p = div.firstChild;
|
||||
testing.expectEqual(3, p.childNodes.length);
|
||||
testing.expectEqual('xy', p.childNodes[0].data);
|
||||
testing.expectEqual('zw', p.childNodes[2].data);
|
||||
|
||||
div.setHTML('<p>x<b><i>y</i></b>z</p>', {sanitizer: {replaceWithChildrenElements: ['b', 'i']}});
|
||||
testing.expectEqual(1, div.firstChild.childNodes.length);
|
||||
testing.expectEqual('xyz', div.firstChild.firstChild.data);
|
||||
|
||||
// nothing removed: nothing touched
|
||||
div.setHTMLUnsafe('<p>x<!--c-->y</p>');
|
||||
testing.expectEqual(3, div.firstChild.childNodes.length);
|
||||
|
||||
// in the other trees too
|
||||
const doc = Document.parseHTML('a<!--c-->b<template>c<!--c-->d</template>', {sanitizer: {elements: ['html', 'head', 'body', 'template']}});
|
||||
testing.expectEqual(2, doc.body.childNodes.length);
|
||||
testing.expectEqual('ab', doc.body.firstChild.data);
|
||||
testing.expectEqual(1, doc.body.lastChild.content.childNodes.length);
|
||||
testing.expectEqual('cd', doc.body.lastChild.content.firstChild.data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=scriptContext>
|
||||
{
|
||||
const script = document.createElement('script');
|
||||
script.textContent = 'untouched';
|
||||
script.setHTML('<b>b</b>');
|
||||
testing.expectEqual('untouched', script.textContent);
|
||||
|
||||
script.setHTMLUnsafe('<b>b</b>');
|
||||
testing.expectEqual('<b>b</b>', script.innerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=javascriptURLs>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
|
||||
// safe: always removed, whatever the config says
|
||||
div.setHTML('<a href="javascript:x()">a</a><a href="https://x/">b</a>', {sanitizer: {javascriptURLs: true}});
|
||||
testing.expectEqual('<a>a</a><a href="https://x/">b</a>', div.innerHTML);
|
||||
|
||||
// unsafe: removed only when the config says so
|
||||
div.setHTMLUnsafe('<a href="javascript:x()">a</a>', {sanitizer: {}});
|
||||
testing.expectEqual('<a href="javascript:x()">a</a>', div.innerHTML);
|
||||
|
||||
const off = {sanitizer: {javascriptURLs: false}};
|
||||
for (const url of ['javascript:x()', ' JaVaScRiPt:x()', '\x01java\nscript:x()', 'java\tscript:x()']) {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('href', url);
|
||||
div.setHTMLUnsafe(a.outerHTML, off);
|
||||
testing.expectEqual(null, div.firstChild.getAttribute('href'), {script_id: 'javascriptURLs'});
|
||||
}
|
||||
div.setHTMLUnsafe('<a href="javascriptx:x()">a</a><a href="xjavascript:x()">b</a><p href="javascript:x()"></p>', off);
|
||||
testing.expectEqual('<a href="javascriptx:x()">a</a><a href="xjavascript:x()">b</a><p href="javascript:x()"></p>', div.innerHTML);
|
||||
|
||||
div.setHTMLUnsafe('<area href="javascript:x()"><form action="javascript:x()"><button formaction="javascript:x()"></button></form>', off);
|
||||
testing.expectEqual('<area><form><button></button></form>', div.innerHTML);
|
||||
|
||||
div.setHTMLUnsafe('<svg><a href="javascript:x()"></a><a xlink:href="javascript:x()"></a></svg>', off);
|
||||
testing.expectEqual('<svg><a></a><a></a></svg>', div.innerHTML);
|
||||
|
||||
div.setHTMLUnsafe('<math><mi href="javascript:x()"></mi></math>', off);
|
||||
testing.expectEqual('<math><mi></mi></math>', div.innerHTML);
|
||||
|
||||
div.setHTMLUnsafe('<svg><animate attributeName="href"></animate><set attributeName="xlink:href"></set><animate attributeName="x"></animate></svg>', off);
|
||||
testing.expectEqual('<svg><animate></animate><set></set><animate attributeName="x"></animate></svg>', div.innerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=nestedTrees>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.setHTML('<template><b onclick=x()>b</b><script><\/script></template>', {sanitizer: {elements: ['template', 'b', 'script']}});
|
||||
testing.expectEqual('<b>b</b>', div.firstChild.innerHTML);
|
||||
|
||||
const host = document.createElement('div');
|
||||
host.setHTMLUnsafe('<div><template shadowrootmode=open><i onclick=x()>i</i><script><\/script></template></div>', {sanitizer: {removeElements: ['script'], removeAttributes: ['onclick']}});
|
||||
testing.expectEqual('<i>i</i>', host.firstChild.shadowRoot.innerHTML);
|
||||
|
||||
// setHTML on a template sets its contents
|
||||
const template = document.createElement('template');
|
||||
template.setHTML('<b onclick=x()>b</b>');
|
||||
testing.expectEqual(0, template.childNodes.length);
|
||||
testing.expectEqual('<b>b</b>', template.content.firstChild.outerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=shadowRoot>
|
||||
{
|
||||
const host = document.createElement('div');
|
||||
const root = host.attachShadow({mode: 'open'});
|
||||
root.setHTML('<b onclick=x()>b</b><script><\/script>');
|
||||
testing.expectEqual('<b>b</b>', root.innerHTML);
|
||||
|
||||
root.setHTMLUnsafe('<b onclick=x()>b</b>');
|
||||
testing.expectEqual('<b onclick="x()">b</b>', root.innerHTML);
|
||||
|
||||
root.setHTMLUnsafe('<b onclick=x()>b</b>', {sanitizer: {}});
|
||||
testing.expectEqual('<b onclick="x()">b</b>', root.innerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=mutationRecord>
|
||||
{
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = '<p>old</p>';
|
||||
const observer = new MutationObserver(() => {});
|
||||
observer.observe(div, {childList: true});
|
||||
div.setHTML('<b>a</b><i>b</i><script><\/script>');
|
||||
const records = observer.takeRecords();
|
||||
testing.expectEqual(1, records.length);
|
||||
testing.expectEqual(1, records[0].removedNodes.length);
|
||||
testing.expectEqual(2, records[0].addedNodes.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=parseHTML>
|
||||
{
|
||||
const doc = Document.parseHTML('<title>t</title><p onclick=x() data-x=1>a<script>x()<\/script></p>');
|
||||
testing.expectEqual(true, doc instanceof Document);
|
||||
testing.expectEqual('about:blank', doc.URL);
|
||||
testing.expectEqual('<p>a</p>', doc.body.innerHTML);
|
||||
testing.expectEqual('t', doc.title);
|
||||
|
||||
const unsafe = Document.parseHTMLUnsafe('<p onclick=x()>a<script>x()<\/script></p>');
|
||||
testing.expectEqual('<p onclick="x()">a<script>x()<\/script></p>', unsafe.body.innerHTML);
|
||||
|
||||
const filtered = Document.parseHTMLUnsafe('<p>a<i>b</i></p>', {sanitizer: {removeElements: ['i']}});
|
||||
testing.expectEqual('<p>a</p>', filtered.body.innerHTML);
|
||||
|
||||
// html isn't allowed, so it all goes
|
||||
testing.expectEqual(null, Document.parseHTML('<div>text', {sanitizer: {elements: ['body', 'div']}}).documentElement);
|
||||
testing.expectError('TypeError', () => Document.parseHTML('x', {sanitizer: {replaceWithChildrenElements: ['html']}}));
|
||||
}
|
||||
</script>
|
||||
@@ -21,11 +21,9 @@ const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Parser = @import("../parser/Parser.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const Document = @import("Document.zig");
|
||||
const HTMLDocument = @import("HTMLDocument.zig");
|
||||
|
||||
const DOMParser = @This();
|
||||
|
||||
@@ -45,42 +43,8 @@ pub fn parseFromString(
|
||||
|
||||
switch (target_mime) {
|
||||
.@"text/html" => {
|
||||
const arena = try frame.getArena(.medium, "DOMParser.parseFromString");
|
||||
defer arena.release();
|
||||
|
||||
// DOMParser builds a detached Document. Borrow the same fragment
|
||||
// parse-mode that `Frame.parse` uses so frame-side hooks
|
||||
// triggered from `Build.created` / `nodeIsReady` (external
|
||||
// stylesheet fetches, script execution, mutation-observer fan-out,
|
||||
// default-script injection) treat the parsed nodes as detached and
|
||||
// skip side effects on the live document. The frame's
|
||||
// `_parse_mode` is restored on exit.
|
||||
const previous_parse_mode = frame._parse_mode;
|
||||
frame._parse_mode = .fragment;
|
||||
defer frame._parse_mode = previous_parse_mode;
|
||||
|
||||
// Create a new HTMLDocument
|
||||
const doc = try frame._factory.document(HTMLDocument{
|
||||
._proto = undefined,
|
||||
});
|
||||
const doc = try Frame.parse.htmlDocument(frame, html, .{});
|
||||
doc.asDocument()._url = frame.url;
|
||||
|
||||
var normalized = std.mem.trim(u8, html, &std.ascii.whitespace);
|
||||
if (normalized.len == 0) {
|
||||
normalized = "<html></html>";
|
||||
}
|
||||
|
||||
// Parse HTML into the document
|
||||
var parser = Parser.init(arena.allocator(), doc.asNode(), frame, .{});
|
||||
parser.parse(normalized);
|
||||
if (parser.terminated) {
|
||||
return error.ExecutionTerminated;
|
||||
}
|
||||
|
||||
if (parser.err) |pe| {
|
||||
return pe.err;
|
||||
}
|
||||
|
||||
return doc.asDocument();
|
||||
},
|
||||
else => {
|
||||
|
||||
@@ -40,6 +40,7 @@ const DOMImplementation = @import("DOMImplementation.zig");
|
||||
const StyleSheetList = @import("css/StyleSheetList.zig");
|
||||
const FontFaceSet = @import("css/FontFaceSet.zig");
|
||||
const Selection = @import("Selection.zig");
|
||||
const Sanitizer = @import("Sanitizer.zig");
|
||||
const XPathResult = @import("XPathResult.zig");
|
||||
const XPathExpression = @import("XPathExpression.zig");
|
||||
|
||||
@@ -1543,6 +1544,16 @@ pub const JsApi = struct {
|
||||
return frame._factory.genericDocument(.{ .url = "about:blank", .charset = "UTF-8" });
|
||||
}
|
||||
|
||||
pub const parseHTML = bridge.function(_parseHTML, .{ .static = true });
|
||||
fn _parseHTML(html: []const u8, options: ?Sanitizer.Options, frame: *Frame) !*Document {
|
||||
return Sanitizer.parseHTML(html, options, true, frame);
|
||||
}
|
||||
|
||||
pub const parseHTMLUnsafe = bridge.function(_parseHTMLUnsafe, .{ .static = true });
|
||||
fn _parseHTMLUnsafe(html: []const u8, options: ?Sanitizer.Options, frame: *Frame) !*Document {
|
||||
return Sanitizer.parseHTML(html, options, false, frame);
|
||||
}
|
||||
|
||||
pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{});
|
||||
pub const onclick = bridge.accessor(Document.getOnClick, Document.setOnClick, .{});
|
||||
pub const ontouchstart = bridge.accessor(handlerAccessor(.ontouchstart).get, handlerAccessor(.ontouchstart).set, .{});
|
||||
|
||||
@@ -165,12 +165,6 @@ pub fn setInnerHTML(self: *DocumentFragment, html: []const u8, frame: *Frame) !v
|
||||
return parent.setHTML(html, .{}, frame);
|
||||
}
|
||||
|
||||
/// allows declarative shadow dom
|
||||
pub fn setHTMLUnsafe(self: *DocumentFragment, html: []const u8, frame: *Frame) !void {
|
||||
const parent = self.asNode();
|
||||
return parent.setHTML(html, .{ .allow_declarative_shadow = true }, frame);
|
||||
}
|
||||
|
||||
pub fn cloneFragment(self: *DocumentFragment, deep: bool, document: *const Node.Document, frame: *Frame) !*Node {
|
||||
const fragment = try DocumentFragment.init(document, frame);
|
||||
const fragment_node = fragment.asNode();
|
||||
|
||||
@@ -29,6 +29,7 @@ const Node = @import("Node.zig");
|
||||
const ShadowRoot = @import("ShadowRoot.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
const collections = @import("collections.zig");
|
||||
const Sanitizer = @import("Sanitizer.zig");
|
||||
|
||||
const Selector = @import("selector/Selector.zig");
|
||||
const Animation = @import("animation/Animation.zig");
|
||||
@@ -627,10 +628,20 @@ pub fn setInnerHTML(self: *Element, html: []const u8, frame: *Frame) !void {
|
||||
return parent.setHTML(html, .{}, frame);
|
||||
}
|
||||
|
||||
pub fn setHTML(self: *Element, html: []const u8, options: ?Sanitizer.Options, frame: *Frame) !void {
|
||||
return Sanitizer.setAndFilterHTML(self.htmlTarget(), self, html, options, true, frame);
|
||||
}
|
||||
|
||||
/// allows declarative shadow dom
|
||||
pub fn setHTMLUnsafe(self: *Element, html: []const u8, frame: *Frame) !void {
|
||||
const parent = self.asNode();
|
||||
return parent.setHTML(html, .{ .allow_declarative_shadow = true }, frame);
|
||||
pub fn setHTMLUnsafe(self: *Element, html: []const u8, options: ?Sanitizer.Options, frame: *Frame) !void {
|
||||
return Sanitizer.setAndFilterHTML(self.htmlTarget(), self, html, options, false, frame);
|
||||
}
|
||||
|
||||
fn htmlTarget(self: *Element) *Node {
|
||||
if (self.is(Html.Template)) |template| {
|
||||
return template.getContent().asNode();
|
||||
}
|
||||
return self.asNode();
|
||||
}
|
||||
|
||||
pub fn getId(self: *const Element) ?[]const u8 {
|
||||
@@ -2662,6 +2673,7 @@ pub const JsApi = struct {
|
||||
pub const assignedSlot = bridge.accessor(Element.getAssignedSlot, null, .{});
|
||||
pub const attachShadow = bridge.function(_attachShadow, .{});
|
||||
pub const insertAdjacentHTML = bridge.function(Element.insertAdjacentHTML, .{ .ce_reactions = true });
|
||||
pub const setHTML = bridge.function(Element.setHTML, .{ .ce_reactions = true });
|
||||
pub const setHTMLUnsafe = bridge.function(Element.setHTMLUnsafe, .{ .ce_reactions = true });
|
||||
pub const insertAdjacentElement = bridge.function(Element.insertAdjacentElement, .{ .ce_reactions = true });
|
||||
pub const insertAdjacentText = bridge.function(Element.insertAdjacentText, .{ .ce_reactions = true });
|
||||
|
||||
@@ -1613,6 +1613,28 @@ pub fn setHTML(self: *Node, html: []const u8, opts: Frame.parse.FragmentParseOpt
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replaceAllWithFragment(self: *Node, fragment: *Node, frame: *Frame) !void {
|
||||
frame.domChanged();
|
||||
|
||||
const notify = Frame.observers.hasMutationObservers(frame);
|
||||
var added: std.ArrayList(*Node) = .empty;
|
||||
if (notify) {
|
||||
var it = fragment.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
try added.append(frame.call_arena, child);
|
||||
}
|
||||
}
|
||||
|
||||
const removed = try self.removeAllChildrenCollecting(notify, frame);
|
||||
try frame.moveAllChildren(fragment, self, null, .silent_parent);
|
||||
|
||||
if (notify and (removed.items.len > 0 or added.items.len > 0)) {
|
||||
// The point here is to batch all of the adds/remove and get a combined
|
||||
// mutation record
|
||||
Frame.observers.notifyChildListChange(frame, self, added.items, removed.items, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Writes a JSON representation of the node and its children
|
||||
pub fn jsonStringify(self: *const Node, writer: *std.json.Stringify) !void {
|
||||
// stupid json api requires this to be const,
|
||||
|
||||
@@ -30,7 +30,11 @@ const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const Element = @import("Element.zig");
|
||||
const TreeWalker = @import("TreeWalker.zig");
|
||||
const defaults = @import("sanitizer_defaults.zig");
|
||||
|
||||
const String = lp.String;
|
||||
@@ -221,6 +225,10 @@ const Config = struct {
|
||||
|
||||
// ?js.Value because not provided, undefined and null are all handled differently
|
||||
pub fn init(configuration_: ?js.Value, exec: *const Execution) !*Sanitizer {
|
||||
return create(configuration_, true, exec);
|
||||
}
|
||||
|
||||
fn create(configuration_: ?js.Value, permissive_defaults: bool, exec: *const Execution) !*Sanitizer {
|
||||
const arena = try exec.getPinnedArena(.small, "Sanitizer");
|
||||
errdefer arena.release();
|
||||
|
||||
@@ -247,7 +255,7 @@ pub fn init(configuration_: ?js.Value, exec: *const Execution) !*Sanitizer {
|
||||
}
|
||||
|
||||
const config: Config = if (configuration.isNull()) .{} else try configuration.toZig(Config);
|
||||
if (try self.setFromConfig(config) == false) {
|
||||
if (try self.setFromConfig(config, permissive_defaults) == false) {
|
||||
return exec.js.typeError("invalid Sanitizer configuration");
|
||||
}
|
||||
}
|
||||
@@ -301,7 +309,7 @@ fn setFromDefault(self: *Sanitizer) !void {
|
||||
self._javascript_urls = false;
|
||||
}
|
||||
|
||||
fn setFromConfig(self: *Sanitizer, config: Config) !bool {
|
||||
fn setFromConfig(self: *Sanitizer, config: Config, permissive_defaults: bool) !bool {
|
||||
var all_new = true;
|
||||
|
||||
const arena = self._arena;
|
||||
@@ -348,11 +356,11 @@ fn setFromConfig(self: *Sanitizer, config: Config) !bool {
|
||||
self._remove_processing_instructions = try self.targetSet(pis, &all_new);
|
||||
}
|
||||
|
||||
self._comments = Config.boolean(config.comments, true);
|
||||
self._comments = Config.boolean(config.comments, permissive_defaults);
|
||||
if (self._allow_attributes != null or config.dataAttributes != null) {
|
||||
self._data_attributes = Config.boolean(config.dataAttributes, true);
|
||||
self._data_attributes = Config.boolean(config.dataAttributes, permissive_defaults);
|
||||
}
|
||||
self._javascript_urls = Config.boolean(config.javascriptURLs, true);
|
||||
self._javascript_urls = Config.boolean(config.javascriptURLs, permissive_defaults);
|
||||
|
||||
if (config.elements == null and config.removeElements == null) {
|
||||
self._remove_elements = .empty;
|
||||
@@ -361,7 +369,11 @@ fn setFromConfig(self: *Sanitizer, config: Config) !bool {
|
||||
self._remove_attributes = .empty;
|
||||
}
|
||||
if (self._allow_processing_instructions == null and self._remove_processing_instructions == null) {
|
||||
self._remove_processing_instructions = .empty;
|
||||
if (permissive_defaults) {
|
||||
self._remove_processing_instructions = .empty;
|
||||
} else {
|
||||
self._allow_processing_instructions = .empty;
|
||||
}
|
||||
}
|
||||
|
||||
return all_new and self.isValid();
|
||||
@@ -992,6 +1004,379 @@ fn hasDataAttribute(set: NameSet) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Like init: we need to tell the difference between not-set, null and undefined
|
||||
// AND, this can actually be a Sanitizer config already.
|
||||
pub const Options = struct {
|
||||
sanitizer: ?js.Value = null,
|
||||
};
|
||||
|
||||
const FromOptions = struct {
|
||||
owned: bool,
|
||||
sanitizer: *Sanitizer,
|
||||
|
||||
fn release(self: FromOptions, page: *Page) void {
|
||||
if (self.owned) {
|
||||
// We have to free a Sanitizer we created, versus a Sanitizer that
|
||||
// was passed to use from JS.
|
||||
self.sanitizer.deinit(page);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn fromOptions(options: ?Options, safe: bool, exec: *const Execution) !?FromOptions {
|
||||
const spec = blk: {
|
||||
const o = options orelse break :blk null;
|
||||
const spec = o.sanitizer orelse break :blk null;
|
||||
if (spec.isUndefined()) {
|
||||
break :blk null;
|
||||
}
|
||||
if (spec.toZig(*Sanitizer)) |sanitizer| {
|
||||
return .{ .sanitizer = sanitizer, .owned = false };
|
||||
} else |_| {}
|
||||
break :blk spec;
|
||||
};
|
||||
|
||||
if (spec == null and safe == false) {
|
||||
return null;
|
||||
}
|
||||
// A missing spec takes the "default" preset
|
||||
return .{ .sanitizer = try create(spec, safe == false, exec), .owned = true };
|
||||
}
|
||||
|
||||
pub fn setAndFilterHTML(target: *Node, context: *Element, html: []const u8, options: ?Options, safe: bool, frame: *Frame) !void {
|
||||
if (safe and std.mem.eql(u8, context.getLocalName(), "script")) {
|
||||
if (context._namespace == .html or context._namespace == .svg) {
|
||||
// hahaha, nice try!
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const resolved = try fromOptions(options, safe, &frame.js.execution);
|
||||
defer if (resolved) |r| r.release(frame.page);
|
||||
|
||||
// Parsed into a detached fragment, so that nothing is connected (no fetch,
|
||||
// no custom element reaction) until it's been sanitized.
|
||||
const fragment = (try Node.DocumentFragment.init(target.getDocument(frame), frame)).asNode();
|
||||
if (html.len > 0) {
|
||||
try Frame.parse.fragment(frame, fragment, html, .{ .context = context, .allow_declarative_shadow = true });
|
||||
}
|
||||
if (resolved) |r| {
|
||||
try r.sanitizer.sanitize(fragment, safe, frame);
|
||||
}
|
||||
try target.replaceAllWithFragment(fragment, frame);
|
||||
}
|
||||
|
||||
pub fn parseHTML(html: []const u8, options: ?Options, safe: bool, frame: *Frame) !*Node.Document {
|
||||
const resolved = try fromOptions(options, safe, &frame.js.execution);
|
||||
defer if (resolved) |r| r.release(frame.page);
|
||||
|
||||
const document = (try Frame.parse.htmlDocument(frame, html, .{ .allow_declarative_shadow = true })).asDocument();
|
||||
document._url = "about:blank";
|
||||
if (resolved) |r| {
|
||||
try r.sanitizer.sanitize(document.asNode(), safe, frame);
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
fn sanitize(self: *const Sanitizer, root: *Node, safe: bool, frame: *Frame) !void {
|
||||
const arena = frame.call_arena;
|
||||
|
||||
// A template's contents and a shadow root are trees of their own
|
||||
var trees: std.ArrayList(*Node) = .empty;
|
||||
try trees.append(arena, root);
|
||||
|
||||
var remove_attributes: std.ArrayList([]const u8) = .empty;
|
||||
|
||||
// Parents that lost a child, whose text nodes might now be adjacent.
|
||||
var touched: std.ArrayList(*Node) = .empty;
|
||||
|
||||
while (trees.pop()) |tree| {
|
||||
var tw = TreeWalker.FullExcludeSelf.init(tree, .{});
|
||||
while (tw.next()) |node| {
|
||||
const parent = node._parent.?;
|
||||
const element = node.is(Element) orelse {
|
||||
if (self.keepNonElement(node) == false) {
|
||||
tw.skipChildren();
|
||||
frame.removeNode(parent, node, .{ .reconnect_to = null });
|
||||
try touch(&touched, parent, arena);
|
||||
}
|
||||
continue;
|
||||
};
|
||||
|
||||
const element_name: Name = .{ .name = .wrap(element.getLocalName()), .namespace = elementNamespace(element, frame) };
|
||||
switch (self.elementAction(element_name, safe)) {
|
||||
.keep => {},
|
||||
.remove => {
|
||||
tw.skipChildren();
|
||||
frame.removeNode(parent, node, .{ .reconnect_to = null });
|
||||
try touch(&touched, parent, arena);
|
||||
continue;
|
||||
},
|
||||
.replace => {
|
||||
// Move the children up; the walker's next node is the
|
||||
// first of them (or, if there are none, what follows).
|
||||
// run_ready = false: nothing gets to act on them until
|
||||
// they've been sanitized.
|
||||
if (node.firstChild() != null) {
|
||||
const previous_root = node.getRootNode(.{});
|
||||
while (node.firstChild()) |child| {
|
||||
frame.removeNode(node, child, .{ .reconnect_to = parent });
|
||||
try frame.insertNodeRelative(parent, child, .{ .before = node }, .{ .previous_root = previous_root, .run_ready = false });
|
||||
}
|
||||
}
|
||||
frame.removeNode(parent, node, .{ .reconnect_to = null });
|
||||
try touch(&touched, parent, arena);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
|
||||
if (element.is(Element.Html.Template)) |template| {
|
||||
try trees.append(arena, template.getContent().asNode());
|
||||
}
|
||||
if (element.hostedShadowRoot(frame)) |shadow_root| {
|
||||
try trees.append(arena, shadow_root.asNode());
|
||||
}
|
||||
|
||||
// Collected first: removing one can run script (attributeChangedCallback)
|
||||
remove_attributes.clearRetainingCapacity();
|
||||
for (element.attributeEntries()) |*entry| {
|
||||
if (self.keepAttribute(element_name, entry.name(), entry.value(), safe) == false) {
|
||||
try remove_attributes.append(arena, entry.name());
|
||||
}
|
||||
}
|
||||
for (remove_attributes.items) |name| {
|
||||
element.removeAttributeSafe(.wrap(name), frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buffer: std.ArrayList(u8) = .empty;
|
||||
for (touched.items) |parent| {
|
||||
try mergeAdjacentText(parent, &buffer, frame);
|
||||
}
|
||||
}
|
||||
|
||||
fn touch(touched: *std.ArrayList(*Node), parent: *Node, arena: Allocator) !void {
|
||||
// Siblings are removed one after another, so this catches most repeats.
|
||||
// Those it misses just get a second, no-op, merge.
|
||||
if (touched.getLastOrNull() != parent) {
|
||||
try touched.append(arena, parent);
|
||||
}
|
||||
}
|
||||
|
||||
// if we removed a node between two text nodes, we need to merge the text ndoes
|
||||
|
||||
fn mergeAdjacentText(parent: *Node, buffer: *std.ArrayList(u8), frame: *Frame) !void {
|
||||
var child = parent.firstChild();
|
||||
while (child) |node| {
|
||||
var next = node.nextSibling();
|
||||
const text = node.is(Node.CData.Text) orelse {
|
||||
child = next;
|
||||
continue;
|
||||
};
|
||||
if (next == null or next.?.is(Node.CData.Text) == null) {
|
||||
child = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.clearRetainingCapacity();
|
||||
try buffer.appendSlice(frame.call_arena, text.ownData());
|
||||
while (next) |sibling| {
|
||||
const sibling_text = sibling.is(Node.CData.Text) orelse break;
|
||||
try buffer.appendSlice(frame.call_arena, sibling_text.ownData());
|
||||
next = sibling.nextSibling();
|
||||
frame.removeNode(parent, sibling, .{ .reconnect_to = null });
|
||||
}
|
||||
text.asCData()._data = try frame.dupeSSO(buffer.items);
|
||||
child = next;
|
||||
}
|
||||
}
|
||||
|
||||
fn keepNonElement(self: *const Sanitizer, node: *Node) bool {
|
||||
const cdata = node.is(Node.CData) orelse return true; // doctype
|
||||
return switch (cdata._type) {
|
||||
.text, .cdata_section => true,
|
||||
.comment => self._comments == true,
|
||||
.processing_instruction => {
|
||||
const target = cdata.subtype(Node.CData.ProcessingInstruction)._target;
|
||||
if (self._allow_processing_instructions) |allowed| {
|
||||
return allowed.contains(target);
|
||||
}
|
||||
return self._remove_processing_instructions.?.contains(target) == false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn elementAction(self: *const Sanitizer, name: Name, safe: bool) enum { keep, remove, replace } {
|
||||
if (safe) {
|
||||
for (defaults.baseline_remove_elements) |unsafe| {
|
||||
if (staticName(unsafe).eql(name)) {
|
||||
return .remove;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (self._replace_elements) |replace| {
|
||||
if (replace.contains(name)) {
|
||||
return .replace;
|
||||
}
|
||||
}
|
||||
if (self._allow_elements) |allowed| {
|
||||
return if (allowed.contains(name)) .keep else .remove;
|
||||
}
|
||||
return if (self._remove_elements.?.contains(name)) .remove else .keep;
|
||||
}
|
||||
|
||||
fn keepAttribute(self: *const Sanitizer, element: Name, qualified_name: []const u8, value: []const u8, safe: bool) bool {
|
||||
const name = attributeName(element.namespace, qualified_name);
|
||||
|
||||
const element_allowed = self._element_allow_attributes.getPtr(element);
|
||||
if (self._element_remove_attributes.getPtr(element)) |removed| {
|
||||
if (removed.contains(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (self._allow_attributes) |allowed| {
|
||||
if (allowed.contains(name) == false and
|
||||
(element_allowed == null or element_allowed.?.contains(name) == false) and
|
||||
(self._data_attributes != true or name.isDataAttribute() == false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (element_allowed) |set| {
|
||||
if (set.contains(name) == false) {
|
||||
return false;
|
||||
}
|
||||
} else if (self._remove_attributes.?.contains(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (safe and name.namespace == .none and isEventHandler(qualified_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (safe or self._javascript_urls != true) {
|
||||
if (isNavigatingURLAttribute(element, name) and isJavascriptURL(value)) {
|
||||
return false;
|
||||
}
|
||||
if (isAnimatingURLAttribute(element, name) and (std.mem.eql(u8, value, "href") or std.mem.eql(u8, value, "xlink:href"))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
fn elementNamespace(element: *Element, frame: *Frame) Namespace {
|
||||
return switch (element._namespace) {
|
||||
.html => .xhtml,
|
||||
.svg => .svg,
|
||||
.mathml => .mathml,
|
||||
.xml => .xml,
|
||||
.null => .none,
|
||||
.unknown => .intern(element.getNamespaceUri(frame)),
|
||||
};
|
||||
}
|
||||
|
||||
// Attributes' namespaces aren't tracked, but the HTML parser only ever gives a
|
||||
// namespace to these attributes of foreign (SVG/MathML) elements.
|
||||
// https://html.spec.whatwg.org/#adjust-foreign-attributes
|
||||
const foreign_attributes = std.StaticStringMap(defaults.Name).initComptime(.{
|
||||
.{ "xlink:actuate", defaults.Name{ .name = "actuate", .namespace = .xlink } },
|
||||
.{ "xlink:arcrole", defaults.Name{ .name = "arcrole", .namespace = .xlink } },
|
||||
.{ "xlink:href", defaults.Name{ .name = "href", .namespace = .xlink } },
|
||||
.{ "xlink:role", defaults.Name{ .name = "role", .namespace = .xlink } },
|
||||
.{ "xlink:show", defaults.Name{ .name = "show", .namespace = .xlink } },
|
||||
.{ "xlink:title", defaults.Name{ .name = "title", .namespace = .xlink } },
|
||||
.{ "xlink:type", defaults.Name{ .name = "type", .namespace = .xlink } },
|
||||
.{ "xml:lang", defaults.Name{ .name = "lang", .namespace = .xml } },
|
||||
.{ "xml:space", defaults.Name{ .name = "space", .namespace = .xml } },
|
||||
.{ "xmlns", defaults.Name{ .name = "xmlns", .namespace = .xmlns } },
|
||||
.{ "xmlns:xlink", defaults.Name{ .name = "xlink", .namespace = .xmlns } },
|
||||
});
|
||||
|
||||
fn attributeName(element_namespace: Namespace, qualified_name: []const u8) Name {
|
||||
if (element_namespace == .svg or element_namespace == .mathml) {
|
||||
if (foreign_attributes.get(qualified_name)) |name| {
|
||||
return staticName(name);
|
||||
}
|
||||
}
|
||||
return .{ .name = .wrap(qualified_name), .namespace = .none };
|
||||
}
|
||||
|
||||
fn isEventHandler(name: []const u8) bool {
|
||||
if (std.mem.startsWith(u8, name, "on") == false) {
|
||||
return false;
|
||||
}
|
||||
return std.sort.binarySearch([]const u8, defaults.event_handler_attributes, name, struct {
|
||||
fn order(key: []const u8, item: []const u8) std.math.Order {
|
||||
return std.mem.order(u8, key, item);
|
||||
}
|
||||
}.order) != null;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#built-in-navigating-url-attributes-list
|
||||
fn isNavigatingURLAttribute(element: Name, attribute: Name) bool {
|
||||
const local = attribute.name.str();
|
||||
switch (element.namespace) {
|
||||
.xhtml => {
|
||||
if (attribute.namespace != .none) {
|
||||
return false;
|
||||
}
|
||||
const tag = element.name.str();
|
||||
if (std.mem.eql(u8, tag, "a") or std.mem.eql(u8, tag, "area")) {
|
||||
return std.mem.eql(u8, local, "href");
|
||||
}
|
||||
if (std.mem.eql(u8, tag, "form")) {
|
||||
return std.mem.eql(u8, local, "action");
|
||||
}
|
||||
if (std.mem.eql(u8, tag, "button") or std.mem.eql(u8, tag, "input")) {
|
||||
return std.mem.eql(u8, local, "formaction");
|
||||
}
|
||||
return false;
|
||||
},
|
||||
// any MathML element, not just <a>
|
||||
.svg, .mathml => {
|
||||
if (element.namespace == .svg and std.mem.eql(u8, element.name.str(), "a") == false) {
|
||||
return false;
|
||||
}
|
||||
return (attribute.namespace == .none or attribute.namespace == .xlink) and std.mem.eql(u8, local, "href");
|
||||
},
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
|
||||
fn isAnimatingURLAttribute(element: Name, attribute: Name) bool {
|
||||
if (element.namespace != .svg or attribute.namespace != .none) {
|
||||
return false;
|
||||
}
|
||||
if (std.mem.eql(u8, attribute.name.str(), "attributeName") == false) {
|
||||
return false;
|
||||
}
|
||||
const tag = element.name.str();
|
||||
return std.mem.eql(u8, tag, "animate") or std.mem.eql(u8, tag, "animateTransform") or std.mem.eql(u8, tag, "set");
|
||||
}
|
||||
|
||||
fn isJavascriptURL(value: []const u8) bool {
|
||||
const trimmed = std.mem.trimStart(u8, value, "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ");
|
||||
const scheme = "javascript:";
|
||||
var i: usize = 0;
|
||||
for (trimmed) |c| {
|
||||
if (c == '\t' or c == '\n' or c == '\r') {
|
||||
continue;
|
||||
}
|
||||
if (std.ascii.toLower(c) != scheme[i]) {
|
||||
return false;
|
||||
}
|
||||
i += 1;
|
||||
if (i == scheme.len) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Sanitizer);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const Frame = @import("../Frame.zig");
|
||||
|
||||
const Node = @import("Node.zig");
|
||||
const Element = @import("Element.zig");
|
||||
const Sanitizer = @import("Sanitizer.zig");
|
||||
const DocumentFragment = @import("DocumentFragment.zig");
|
||||
|
||||
const ShadowRoot = @This();
|
||||
@@ -110,8 +111,12 @@ fn getSerializable(self: *const ShadowRoot) bool {
|
||||
return self._serializable;
|
||||
}
|
||||
|
||||
pub fn setHTMLUnsafe(self: *ShadowRoot, html: []const u8, frame: *Frame) !void {
|
||||
return self.asDocumentFragment().setHTMLUnsafe(html, frame);
|
||||
pub fn setHTML(self: *ShadowRoot, html: []const u8, options: ?Sanitizer.Options, frame: *Frame) !void {
|
||||
return Sanitizer.setAndFilterHTML(self.asNode(), self._host, html, options, true, frame);
|
||||
}
|
||||
|
||||
pub fn setHTMLUnsafe(self: *ShadowRoot, html: []const u8, options: ?Sanitizer.Options, frame: *Frame) !void {
|
||||
return Sanitizer.setAndFilterHTML(self.asNode(), self._host, html, options, false, frame);
|
||||
}
|
||||
|
||||
pub fn getHTML(self: *ShadowRoot, opts: dump.Opts.Shadow.Declarative, writer: *std.Io.Writer, frame: *Frame) !void {
|
||||
@@ -219,6 +224,7 @@ pub const JsApi = struct {
|
||||
return self.getElementById(try value.toZig([]const u8), frame);
|
||||
}
|
||||
pub const adoptedStyleSheets = bridge.accessor(ShadowRoot.getAdoptedStyleSheets, ShadowRoot.setAdoptedStyleSheets, .{});
|
||||
pub const setHTML = bridge.function(ShadowRoot.setHTML, .{ .ce_reactions = true });
|
||||
pub const setHTMLUnsafe = bridge.function(ShadowRoot.setHTMLUnsafe, .{ .ce_reactions = true });
|
||||
pub const getHTML = bridge.function(_getHTML, .{});
|
||||
const GetHTMLOpts = struct {
|
||||
|
||||
Reference in new issue
Block a user