From b6d63a9c91d8c8ab0cba7e2de6fcf15f97bff382 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 23 Jun 2026 19:58:07 +0800 Subject: [PATCH] webapi: Expose document.domain setter This is a deprecated API, but it's used in various WPT tests. I don't expect WPT tests to necessarily improve, but they should get further than just failing on the setter. This also includes a change where BroadcastChannel now uses the origin string rather than the js.Origin identity because BroadcastChannel should be based on the actual frame's origin...which used to be captured in Context.origin but which the new document.domain can change. --- src/browser/Page.zig | 16 ++-- src/browser/URL.zig | 11 +++ src/browser/js/Context.zig | 11 +-- src/browser/tests/document/document.html | 8 ++ src/browser/webapi/BroadcastChannel.zig | 13 ++- src/browser/webapi/Document.zig | 107 ++++++++++++++++++++++- 6 files changed, 146 insertions(+), 20 deletions(-) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 2a53d5916..a4eff039b 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -322,7 +322,7 @@ fn findPopupBy(self: *Page, comptime field: []const u8, id: u32) ?*Frame { // // The returned set is fixed, so a caller may run user JS (which can create or // tear down frames/workers) while walking it without invalidating the slice. -pub fn executionsForOrigin(self: *Page, arena: Allocator, origin: *js.Origin) ![]*js.Execution { +pub fn executionsForOrigin(self: *Page, arena: Allocator, origin: []const u8) ![]*js.Execution { var list: std.ArrayList(*js.Execution) = .empty; try appendFrameExecutions(&self.frame, origin, arena, &list); for (self.popups.items) |popup| { @@ -331,14 +331,18 @@ pub fn executionsForOrigin(self: *Page, arena: Allocator, origin: *js.Origin) ![ return list.items; } -fn appendFrameExecutions(frame: *Frame, origin: *js.Origin, arena: Allocator, list: *std.ArrayList(*js.Execution)) !void { - if (frame.js.origin == origin) { - try list.append(arena, &frame.js.execution); +fn appendFrameExecutions(frame: *Frame, origin: []const u8, arena: Allocator, list: *std.ArrayList(*js.Execution)) !void { + if (frame.origin) |fo| { + if (std.mem.eql(u8, fo, origin)) { + try list.append(arena, &frame.js.execution); + } } for (frame.workers.items) |worker| { const wgs = worker._worker_scope; - if (wgs.js.origin == origin) { - try list.append(arena, &wgs.js.execution); + if (wgs.origin) |wo| { + if (std.mem.eql(u8, wo, origin)) { + try list.append(arena, &wgs.js.execution); + } } } for (frame.child_frames.items) |child| { diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 27f0d917c..f1e7e25c5 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -537,6 +537,17 @@ pub fn getHostname(raw: [:0]const u8) []const u8 { return host[0..port_sep]; } +// Like getHostname, but for an origin serialization ("scheme://host[:port]"), +// which is not necessarily sentinel-terminated. Used where the document's host +// must come from its (possibly inherited) origin rather than its URL — e.g. an +// about:blank iframe whose url stays "about:blank" but whose origin is the +// parent's. +pub fn getOriginHostname(origin: []const u8) []const u8 { + const host = getHost(origin); + const port_sep = findPortSeparator(host) orelse return host; + return host[0..port_sep]; +} + pub fn getPort(raw: [:0]const u8) []const u8 { const host = getHost(raw); const port_sep = findPortSeparator(host) orelse return ""; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 089841e22..7cc777f23 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -229,18 +229,11 @@ pub fn deinit(self: *Context) void { v8.v8__MicrotaskQueue__DELETE(self.microtask_queue); } +// setOrigin is called at navigation (opaque -> real origin) and again when a +// script sets document.domain (real origin -> '!'-marked effective domain). pub fn setOrigin(self: *Context, key: ?[]const u8) !void { const env = self.env; const isolate = env.isolate; - - if (comptime IS_DEBUG) { - // A frame starts off with an opaque origin. After navigation, setOrigin - // is called. This is the only time setOrigin should be called for that - // frame. Therefore, when setOrigin is called, the previous origin should - // have been opaque and its rc should have been 1. - lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc }); - } - const origin = try self.page.getOrCreateOrigin(key); self.page.releaseOrigin(self.origin); diff --git a/src/browser/tests/document/document.html b/src/browser/tests/document/document.html index eb69f5d84..44587ae7d 100644 --- a/src/browser/tests/document/document.html +++ b/src/browser/tests/document/document.html @@ -62,6 +62,14 @@ testing.expectEqual(document.URL, document.documentURI); testing.expectEqual('', document.referrer); testing.expectEqual(testing.HOST, document.domain); + + document.domain = testing.HOST; + testing.expectEqual(testing.HOST, document.domain); + testing.expectError('SecurityError', () => { document.domain = 'example.org'; }); + testing.expectError('SecurityError', () => { document.domain = 'com'; }); + testing.expectError('SecurityError', () => { document.domain = ''; }); + // Rejected assignments must not have changed the domain. + testing.expectEqual(testing.HOST, document.domain);