Handle immediate meta refresh navigation

Assisted-By: devx/cb5edaca-b259-4956-a65f-f2168ea91830
This commit is contained in:
Scott Taylor committed 2026-08-25 23:24:45 -04:00
1 parent 0590bd0f80
commit 6ea37f4dcc
4 files changed
+98 -7

No files matched your search

+30
View File
@@ -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});
+3
View File
@@ -0,0 +1,3 @@
<!doctype html>
<meta http-equiv="refresh" content="0; meta_refresh_target.html">
<title>source</title>
+3
View File
@@ -0,0 +1,3 @@
<!doctype html>
<title>target</title>
<h1>target</h1>
+62 -7
View File
@@ -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 {
// <meta name=referrer> 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"));
}