From d2b495113c19ad32047254e9b8ef53cdef7025ee Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 25 May 2026 20:46:53 +0800 Subject: [PATCH] Improve document.write within an iframe This fixes two things. First, it better tracks the calling context and the target context, so that if a parent frame does a document.write within a child frame, any JavaScript is executed in the child frame's context (previously, it would be executed in the parent's context). Second, it ensures that, on document.write, we force-execute any pending scripts. This is related to https://github.com/lightpanda-io/browser/pull/2542 but applies specifically to document.write. --- src/browser/ScriptManagerBase.zig | 10 ++++ src/browser/tests/frames/document_write.html | 50 ++++++++++++++++++++ src/browser/webapi/Document.zig | 29 ++++++++++-- 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 src/browser/tests/frames/document_write.html diff --git a/src/browser/ScriptManagerBase.zig b/src/browser/ScriptManagerBase.zig index 02e6dbdf..f726d1fd 100644 --- a/src/browser/ScriptManagerBase.zig +++ b/src/browser/ScriptManagerBase.zig @@ -398,6 +398,16 @@ pub fn staticScriptsDone(self: *ScriptManagerBase) void { self.evaluate(); } +// A script-created parser (document.open/write/close) finished. Run any +// deferred scripts it produced. Unlike staticScriptsDone, this can run after +// the initial parse already completed (so it must not re-assert the flag): a +// frame that was loaded (or document.write'd into multiple times) keeps +// static_scripts_done set, and evaluate() only drains defer_scripts when it is. +pub fn scriptCreatedParseDone(self: *ScriptManagerBase) void { + self.static_scripts_done = true; + self.evaluate(); +} + pub fn evaluate(self: *ScriptManagerBase) void { if (self.is_evaluating) { // It's possible for a script.eval to cause evaluate to be called again. diff --git a/src/browser/tests/frames/document_write.html b/src/browser/tests/frames/document_write.html new file mode 100644 index 00000000..7565c28c --- /dev/null +++ b/src/browser/tests/frames/document_write.html @@ -0,0 +1,50 @@ + + + + + + + + diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 4a3d718a..89673633 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -718,7 +718,13 @@ pub fn writeln(self: *Document, text: []const []const u8, frame: *Frame) !void { return self.writeInternal(text, true, frame); } -fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, frame: *Frame) !void { +fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, call_frame: *Frame) !void { + // document.write acts on this document's own frame, which isn't necessarily + // the calling frame — e.g. a parent frame writing into an iframe's document. + // The markup (and any scripts it contains) must be parsed and run in that + // document's context, not the caller's. + const frame = self._frame orelse call_frame; + if (self._type == .xml) { return error.InvalidStateError; } @@ -730,10 +736,13 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool const html = blk: { var joined: std.ArrayList(u8) = .empty; for (text) |str| { - try joined.appendSlice(frame.call_arena, str); + // Scratch buffer, consumed synchronously below. Keep it on the + // active (calling) frame's call_arena: a script run by the parse + // could reset the document frame's call_arena underfoot. + try joined.appendSlice(call_frame.call_arena, str); } if (append_newline) { - try joined.append(frame.call_arena, '\n'); + try joined.append(call_frame.call_arena, '\n'); } break :blk joined.items; }; @@ -827,7 +836,9 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool self._write_insertion_point = children_to_insert.getLast(); } -pub fn open(self: *Document, frame: *Frame) !*Document { +pub fn open(self: *Document, call_frame: *Frame) !*Document { + const frame = self._frame orelse call_frame; + if (self._type == .xml) { return error.InvalidStateError; } @@ -869,7 +880,9 @@ pub fn open(self: *Document, frame: *Frame) !*Document { return self; } -pub fn close(self: *Document, frame: *Frame) !void { +pub fn close(self: *Document, call_frame: *Frame) !void { + const frame = self._frame orelse call_frame; + if (self._type == .xml) { return error.InvalidStateError; } @@ -889,6 +902,12 @@ pub fn close(self: *Document, frame: *Frame) !void { defer self._script_created_parser = null; try self._script_created_parser.?.done(); + // The write'd markup is fully parsed; run any deferred scripts it produced + // (e.g. inline modules) before firing the load event. This frame's initial + // parse may never have set static_scripts_done (e.g. a freshly-loaded + // iframe written into via document.write), so we can't rely on it. + frame._script_manager.base.scriptCreatedParseDone(); + frame.documentIsComplete(); }