mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-02 02:36:06 -04:00
fix: Custom-element constructor parser endless recursion
In a custom element, when this.innerHTML = '....' is called, we need to be careful to prevent endless recursion. The html5ever callback used to determine the context element should not invoke the custom-element constructor, else we'll enter an endless loop. This also fixes an ungating problem added with the new HttpClient when a waitForImport can block forever. Both issues were see on a WooCommerce site - though the HttpClient is only due to an earlier HttpClient refactor.
This commit is contained in:
@@ -211,6 +211,12 @@ _customized_builtin_disconnected_callback_invoked: std.AutoHashMapUnmanaged(*Ele
|
||||
// The constructor can access this to get the element being upgraded.
|
||||
_upgrading_element: ?*Node = null,
|
||||
|
||||
// Set when materializing the fragment parser's context element. The element
|
||||
// is never inserted into the tree so if its a custom element ,we must not run
|
||||
// its constructor (else we'll end up in an endless loop if the constructor
|
||||
// sets this.innerHTML = '...', which happens).
|
||||
_skip_custom_element_upgrade: bool = false,
|
||||
|
||||
// List of custom elements that were created before their definition was registered
|
||||
_undefined_custom_elements: std.ArrayList(*Element.Html.Custom) = .{},
|
||||
|
||||
|
||||
@@ -829,6 +829,12 @@ pub fn createElementNS(frame: *Frame, namespace: Element.Namespace, name: []cons
|
||||
._definition = definition,
|
||||
});
|
||||
|
||||
// Fragment-parse context element. It will not be inserted and
|
||||
// we should not run the custom element's constructor.
|
||||
if (frame._skip_custom_element_upgrade) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const def = definition orelse {
|
||||
const element = node.as(Element);
|
||||
const custom = element.is(Element.Html.Custom).?;
|
||||
|
||||
@@ -272,6 +272,7 @@ pub fn parseFragment(self: *Parser, html: []const u8) void {
|
||||
&self.container,
|
||||
self,
|
||||
createElementCallback,
|
||||
createContextElementCallback,
|
||||
getDataCallback,
|
||||
appendCallback,
|
||||
parseErrorCallback,
|
||||
@@ -439,6 +440,25 @@ fn createXMLElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualNa
|
||||
return _createElementCallbackWithDefaultnamespace(ctx, data, qname, attributes, .xml);
|
||||
}
|
||||
|
||||
// html5ever_parse_fragment materializes the fragment's context element through
|
||||
// this dedicated callback, never through createElementCallback. The context
|
||||
// element is a throwaway: html5ever only queries its name (and, for a
|
||||
// <template> context, its content), it is never inserted into the tree. It
|
||||
// must be built bare — running a custom element constructor here is unbounded
|
||||
// recursion when the constructor sets innerHTML: that re-parse needs a context
|
||||
// element of the same custom tag, reconstructing forever (stack overflow). No
|
||||
// _ce_reactions window is needed: nothing can enqueue reactions on a bare
|
||||
// create.
|
||||
fn createContextElementCallback(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator) callconv(.c) ?*anyopaque {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
self.frame._skip_custom_element_upgrade = true;
|
||||
defer self.frame._skip_custom_element_upgrade = false;
|
||||
return self._createElementCallback(data, qname, attributes, .unknown) catch |err| {
|
||||
self.err = .{ .err = err, .source = .create_element };
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
fn _createElementCallbackWithDefaultnamespace(ctx: *anyopaque, data: *anyopaque, qname: h5e.QualName, attributes: h5e.AttributeIterator, default_namespace: Element.Namespace) ?*anyopaque {
|
||||
const self: *Parser = @ptrCast(@alignCast(ctx));
|
||||
const cp = self.frame._ce_reactions.push();
|
||||
|
||||
@@ -75,6 +75,7 @@ pub extern "c" fn html5ever_parse_fragment(
|
||||
doc: *anyopaque,
|
||||
ctx: *anyopaque,
|
||||
createElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,
|
||||
createContextElementCallback: *const fn (ctx: *anyopaque, data: *anyopaque, QualName, AttributeIterator) callconv(.c) ?*anyopaque,
|
||||
elemNameCallback: *const fn (node_ref: *anyopaque) callconv(.c) *anyopaque,
|
||||
appendCallback: *const fn (ctx: *anyopaque, parent_ref: *anyopaque, NodeOrText) callconv(.c) void,
|
||||
parseErrorCallback: *const fn (ctx: *anyopaque, StringSlice) callconv(.c) void,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id="constructor_sets_innerhtml">
|
||||
{
|
||||
// A custom element whose constructor sets innerHTML. The innerHTML
|
||||
// setter fragment-parses with this element as the parsing context.
|
||||
// html5ever's parse_fragment builds a throwaway context element of the
|
||||
// same tag via our create_element callback; if we run its constructor
|
||||
// it sets innerHTML again -> re-parses -> rebuilds the context element
|
||||
// -> constructs forever (stack overflow). The context element must be
|
||||
// built bare, so the constructor runs exactly once.
|
||||
let calls = 0;
|
||||
class CeCtorInnerHtml extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
calls += 1;
|
||||
this.innerHTML = '<span>hi</span>';
|
||||
}
|
||||
}
|
||||
customElements.define('ce-ctor-innerhtml', CeCtorInnerHtml);
|
||||
|
||||
const el = document.createElement('ce-ctor-innerhtml');
|
||||
testing.expectEqual(1, calls);
|
||||
testing.expectEqual('<span>hi</span>', el.innerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="innerhtml_on_custom_element_host">
|
||||
{
|
||||
// Setting innerHTML on an existing custom-element instance parses with
|
||||
// that element as context. The throwaway context element html5ever
|
||||
// builds must NOT invoke the constructor a second time.
|
||||
let calls = 0;
|
||||
class CeHost extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
calls += 1;
|
||||
}
|
||||
}
|
||||
customElements.define('ce-host', CeHost);
|
||||
|
||||
const host = document.createElement('ce-host');
|
||||
testing.expectEqual(1, calls);
|
||||
host.innerHTML = '<b>x</b>';
|
||||
testing.expectEqual(1, calls);
|
||||
testing.expectEqual('<b>x</b>', host.innerHTML);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="fragment_children_still_upgrade">
|
||||
{
|
||||
// Only the context element is skipped: real custom elements that appear
|
||||
// *inside* the parsed fragment must still be constructed.
|
||||
let calls = 0;
|
||||
class CeChild extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
calls += 1;
|
||||
}
|
||||
}
|
||||
customElements.define('ce-child', CeChild);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = '<ce-child></ce-child><ce-child></ce-child>';
|
||||
testing.expectEqual(2, calls);
|
||||
testing.expectEqual(2, container.querySelectorAll('ce-child').length);
|
||||
}
|
||||
</script>
|
||||
@@ -29,9 +29,10 @@ use std::os::raw::{c_uchar, c_void};
|
||||
use types::*;
|
||||
|
||||
use encoding_rs::Encoding;
|
||||
use html5ever::driver::parse_fragment_for_element;
|
||||
use html5ever::interface::tree_builder::QuirksMode;
|
||||
use html5ever::tendril::{StrTendril, TendrilSink};
|
||||
use html5ever::{ns, parse_document, parse_fragment, LocalName, ParseOpts, Parser, QualName};
|
||||
use html5ever::{ns, parse_document, LocalName, ParseOpts, Parser, QualName};
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn html5ever_parse_document(
|
||||
@@ -466,6 +467,7 @@ pub extern "C" fn html5ever_parse_fragment(
|
||||
document: Ref,
|
||||
ctx: Ref,
|
||||
create_element_callback: CreateElementCallback,
|
||||
create_context_element_callback: CreateElementCallback,
|
||||
get_data_callback: GetDataCallback,
|
||||
append_callback: AppendCallback,
|
||||
parse_error_callback: ParseErrorCallback,
|
||||
@@ -537,12 +539,27 @@ pub extern "C" fn html5ever_parse_fragment(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
parse_fragment(
|
||||
let context_qname = QualName::new(None, ns!(html), context_local);
|
||||
let context_data = arena.alloc(sink::ElementData {
|
||||
qname: context_qname.clone(),
|
||||
mathml_annotation_xml_integration_point: false,
|
||||
});
|
||||
let mut context_attrs = CAttributeIterator { vec: vec![], pos: 0 };
|
||||
let context_elem = unsafe {
|
||||
(create_context_element_callback)(
|
||||
ctx,
|
||||
context_data as *mut _ as *mut c_void,
|
||||
CQualName::create(&context_qname),
|
||||
&mut context_attrs as *mut _ as *mut c_void,
|
||||
)
|
||||
};
|
||||
|
||||
parse_fragment_for_element(
|
||||
sink,
|
||||
opts,
|
||||
QualName::new(None, ns!(html), context_local),
|
||||
vec![], // attributes
|
||||
false, // context_element_allows_scripting
|
||||
context_elem,
|
||||
false, // context_element_allows_scripting
|
||||
None, // form_element
|
||||
)
|
||||
.from_utf8()
|
||||
.one(bytes);
|
||||
|
||||
@@ -647,14 +647,24 @@ fn settleConns(self: *Client) !void {
|
||||
fn dispatchCompleted(self: *Client, mode: DrainMode) void {
|
||||
if (mode == .all) {
|
||||
if (comptime IS_DEBUG) {
|
||||
// .all never inside a syncRequest, so nothing can be gating requests.
|
||||
std.debug.assert(self.blocking_requests.count() == 0);
|
||||
}
|
||||
// Called from the top; not inside a syncRequest. There cannot be a
|
||||
// blocking request, so this is the time to process any gated requests.
|
||||
}
|
||||
|
||||
// blocking_requests.count() is always true on tick(.all) but can also
|
||||
// happen mid .sync_wait. This is important to do. During blocking, we gated
|
||||
// every request. When the blocking requested completed, it only unblocked
|
||||
// it's frames request.
|
||||
if (self.blocking_requests.count() == 0) {
|
||||
var node = self.gated_queue.last;
|
||||
while (node) |n| {
|
||||
node = n.prev;
|
||||
const transfer: *Transfer = @fieldParentPtr("_queue_node", n);
|
||||
if (mode == .sync_wait and self.isGated(transfer)) {
|
||||
// still gated: a document is never delivered on a sync pump
|
||||
continue;
|
||||
}
|
||||
transfer._gated = false;
|
||||
self.gated_queue.remove(n);
|
||||
self.dispatch_queue.prepend(n);
|
||||
|
||||
Reference in New Issue
Block a user