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"));
+}