diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index cd9de0719..9c821fa04 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -3151,6 +3151,12 @@ fn nodeIsReady(self: *Frame, comptime from_parser: bool, node: *Node) !void { log.err(.frame, "frame.nodeIsReady", .{ .err = err, .element = "iframe", .type = frame._type, .url = frame.url }); return err; }; + } else if (node.is(Element.Html.Meta)) |meta| { + const frame = if (comptime from_parser) self else node.ownerFrame(self); + meta.processRefresh(frame) catch |err| { + log.err(.frame, "frame.nodeIsReady", .{ .err = err, .element = "meta", .type = frame._type, .url = frame.url }); + return err; + }; } else if (node.is(Element.Html.Link)) |link| { const frame = if (comptime from_parser) self else node.ownerFrame(self); link.linkAddedCallback(frame) catch |err| { @@ -3728,6 +3734,30 @@ test "Page: isSameOrigin" { try testing.expectEqual(false, frame.isSameOrigin("//origin.com/foo")); } +test "Frame: static immediate meta refresh navigates" { + const page = try testing.pageTest("fixtures/meta_refresh.html", .{}); + defer page.close(); + + try testing.expect(std.mem.endsWith(u8, page.frame().?.url, "/fixtures/meta_refresh_target.html")); +} + +test "Frame: dynamic immediate meta refresh schedules navigation" { + const frame = try testing.createFrame(); + defer testing.test_session.closeAllPages(); + frame.url = "https://example.com/source"; + + const html = try frame.document.createElement("html", null, frame); + _ = try frame.document.asNode().appendChild(html.asNode(), frame); + const head = try frame.document.createElement("head", null, frame); + _ = try html.asNode().appendChild(head.asNode(), frame); + const meta = try frame.document.createElement("meta", null, frame); + try meta.setAttribute(comptime .wrap("http-equiv"), comptime .wrap("refresh"), frame); + try meta.setAttribute(comptime .wrap("content"), comptime .wrap("0; /target"), frame); + _ = try head.asNode().appendChild(meta.asNode(), frame); + + try testing.expectString("https://example.com/target", frame._queued_navigation.?.url); +} + test "Frame: httpMetadata after navigation" { testing.expectLog(&.{.http}); diff --git a/src/browser/tests/fixtures/meta_refresh.html b/src/browser/tests/fixtures/meta_refresh.html new file mode 100644 index 000000000..7c12e17d4 --- /dev/null +++ b/src/browser/tests/fixtures/meta_refresh.html @@ -0,0 +1,3 @@ + + +source diff --git a/src/browser/tests/fixtures/meta_refresh_target.html b/src/browser/tests/fixtures/meta_refresh_target.html new file mode 100644 index 000000000..296ed8c3a --- /dev/null +++ b/src/browser/tests/fixtures/meta_refresh_target.html @@ -0,0 +1,3 @@ + +target +

target

diff --git a/src/browser/webapi/element/html/Meta.zig b/src/browser/webapi/element/html/Meta.zig index 5ff50eff4..2ae016fa3 100644 --- a/src/browser/webapi/element/html/Meta.zig +++ b/src/browser/webapi/element/html/Meta.zig @@ -27,6 +27,7 @@ const Element = @import("../../Element.zig"); const HtmlElement = @import("../Html.zig"); const referrer = @import("../../../referrer.zig"); +const String = lp.String; const Meta = @This(); pub const Proto = HtmlElement; @@ -84,19 +85,64 @@ pub fn setScheme(self: *Meta, value: []const u8, frame: *Frame) !void { try self.asElement().setAttributeSafe(comptime .wrap("scheme"), .wrap(value), frame); } +pub fn processRefresh(self: *Meta, frame: *Frame) !void { + if (!self.asNode().isConnected()) return; + try self.scheduleRefresh(frame); +} + +fn scheduleRefresh(self: *Meta, frame: *Frame) !void { + if (!std.ascii.eqlIgnoreCase(self.getHttpEquiv(), "refresh")) return; + + const target = immediateRefreshTarget(self.getContent()) orelse return; + try frame.scheduleNavigation(target, .{ + .reason = .script, + .kind = .{ .replace = null }, + }, .{ .script = frame }); +} + +fn immediateRefreshTarget(content: []const u8) ?[]const u8 { + const trimmed = std.mem.trim(u8, content, &std.ascii.whitespace); + const separator = std.mem.indexOfAny(u8, trimmed, ";,") orelse return null; + const delay = std.mem.trim(u8, trimmed[0..separator], &std.ascii.whitespace); + const seconds = std.fmt.parseFloat(f64, delay) catch return null; + if (!std.math.isFinite(seconds) or seconds != 0) return null; + + var target = std.mem.trim(u8, trimmed[separator + 1 ..], &std.ascii.whitespace); + if (target.len >= 3 and std.ascii.eqlIgnoreCase(target[0..3], "url")) { + target = std.mem.trimStart(u8, target[3..], &std.ascii.whitespace); + if (target.len == 0 or target[0] != '=') return null; + target = std.mem.trim(u8, target[1..], &std.ascii.whitespace); + } + if (target.len >= 2 and ((target[0] == '"' and target[target.len - 1] == '"') or (target[0] == '\'' and target[target.len - 1] == '\''))) { + target = std.mem.trim(u8, target[1 .. target.len - 1], &std.ascii.whitespace); + } + return if (target.len == 0) null else target; +} + pub const Build = struct { // sets the document's referrer policy. pub fn created(node: *Node, frame: *Frame) !void { - const el = node.as(Element); - const name = el.getAttributeSafe(comptime .wrap("name")) orelse return; - if (std.ascii.eqlIgnoreCase(name, "referrer") == false) { - return; + const self = node.as(Meta); + const el = self.asElement(); + if (el.getAttributeSafe(comptime .wrap("name"))) |name| { + if (std.ascii.eqlIgnoreCase(name, "referrer")) { + if (el.getAttributeSafe(comptime .wrap("content"))) |content| { + if (referrer.parseMeta(content)) |rp| { + frame.referrer_policy = rp; + } + } + } } - const content = el.getAttributeSafe(comptime .wrap("content")) orelse return; - if (referrer.parseMeta(content)) |rp| { - frame.referrer_policy = rp; + + if (frame._parse_mode != .fragment) { + try self.scheduleRefresh(frame); } } + + pub fn attributeChange(element: *Element, name: String, _: String, frame: *Frame) !void { + if (!name.eql(comptime .wrap("http-equiv")) and !name.eql(comptime .wrap("content"))) return; + try element.as(Meta).processRefresh(frame); + } }; pub const JsApi = struct { @@ -114,3 +160,12 @@ pub const JsApi = struct { pub const media = bridge.accessor(MetaElement.getMedia, MetaElement.setMedia, .{ .ce_reactions = true }); pub const scheme = bridge.accessor(MetaElement.getScheme, MetaElement.setScheme, .{ .ce_reactions = true }); }; + +const testing = @import("../../../../testing.zig"); +test "WebApi: Meta immediate refresh target" { + try testing.expectString("/target", immediateRefreshTarget("0; /target").?); + try testing.expectString("/target", immediateRefreshTarget("0, URL = '/target'").?); + try testing.expectString("https://example.com/", immediateRefreshTarget("0.0; https://example.com/").?); + try testing.expectEqual(null, immediateRefreshTarget("1; /target")); + try testing.expectEqual(null, immediateRefreshTarget("invalid; /target")); +}