Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/browser/EventManager.zig
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void

switch (target._type) {
.node => |node| try self.dispatchNode(node, event, &was_handled),
.xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue => {
.xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue, .navigation => {
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
try self.dispatchAll(list, target, event, &was_handled);
},
Expand Down
56 changes: 50 additions & 6 deletions src/browser/Page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const Mime = @import("Mime.zig");
const Factory = @import("Factory.zig");
const Session = @import("Session.zig");
const Scheduler = @import("Scheduler.zig");
const History = @import("webapi/History.zig");
const EventManager = @import("EventManager.zig");
const ScriptManager = @import("ScriptManager.zig");

Expand All @@ -58,6 +57,8 @@ const MutationObserver = @import("webapi/MutationObserver.zig");
const IntersectionObserver = @import("webapi/IntersectionObserver.zig");
const CustomElementDefinition = @import("webapi/CustomElementDefinition.zig");
const storage = @import("webapi/storage/storage.zig");
const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;

const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp;
Expand Down Expand Up @@ -210,7 +211,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self.window = try self._factory.eventTarget(Window{
._document = self.document,
._storage_bucket = storage_bucket,
._history = History.init(self),
._performance = Performance.init(),
._proto = undefined,
._location = &default_location,
Expand Down Expand Up @@ -271,7 +271,33 @@ fn registerBackgroundTasks(self: *Page) !void {
}.runMessageLoop, 250, .{ .name = "page.messageLoop" });
}

pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts, kind: NavigationKind) !void {
const session = self._session;

const resolved_url = try URL.resolve(
session.transfer_arena,
self.url,
request_url,
.{ .always_dupe = true },
);

// setting opts.force = true will force a page load.
// otherwise, we will need to ensure this is a true (not document) navigation.
if (!opts.force) {
// If we are navigating within the same document, just change URL.
if (URL.eqlDocument(self.url, resolved_url)) {
// update page url
self.url = resolved_url;

// update location
self.window._location = try Location.init(self.url, self);
self.document._location = self.window._location;

try session.navigation.updateEntries(resolved_url, kind, self, true);
return;
}
}

if (self._parse_state != .pre) {
// it's possible for navigate to be called multiple times on the
// same page (via CDP). We want to reset the page between each call.
Expand Down Expand Up @@ -328,6 +354,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
.timestamp = timestamp(.monotonic),
});

session.navigation._current_navigation_kind = kind;

http_client.request(.{
.ctx = self,
.url = self.url,
Expand Down Expand Up @@ -412,6 +440,14 @@ fn _documentIsComplete(self: *Page) !void {
self.window._on_load,
.{ .inject_target = false, .context = "page load" },
);

const pageshow_event = try PageTransitionEvent.init("pageshow", .{}, self);
try self._event_manager.dispatchWithFunction(
self.window.asEventTarget(),
pageshow_event.asEvent(),
self.window._on_pageshow,
.{ .context = "page show" },
);
}

fn pageHeaderDoneCallback(transfer: *Http.Transfer) !void {
Expand Down Expand Up @@ -493,6 +529,9 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
var self: *Page = @ptrCast(@alignCast(ctx));
self.clearTransferArena();

//We need to handle different navigation types differently.
try self._session.navigation.commitNavigation(self);

defer if (comptime IS_DEBUG) {
log.debug(.page, "page.load.complete", .{ .url = self.url });
};
Expand Down Expand Up @@ -528,9 +567,6 @@ fn pageDoneCallback(ctx: *anyopaque) !void {
},
else => unreachable,
}
// We need to handle different navigation types differently.
// @ZIGDOM
// try self._session.navigation.processNavigation(self);
}

fn pageErrorCallback(ctx: *anyopaque, err: anyerror) void {
Expand Down Expand Up @@ -1862,12 +1898,19 @@ const IdleNotification = union(enum) {
}
};

pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
const URLRaw = @import("URL.zig");
const current_origin = (try URLRaw.getOrigin(self.arena, self.url)) orelse return false;
return std.mem.startsWith(u8, url, current_origin);
}

pub const NavigateReason = enum {
anchor,
address_bar,
form,
script,
history,
navigation,
};

pub const NavigateOpts = struct {
Expand All @@ -1876,6 +1919,7 @@ pub const NavigateOpts = struct {
method: Http.Method = .GET,
body: ?[]const u8 = null,
header: ?[:0]const u8 = null,
force: bool = false,
};

const RequestCookieOpts = struct {
Expand Down
22 changes: 20 additions & 2 deletions src/browser/Session.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const log = @import("../log.zig");

const js = @import("js/js.zig");
const storage = @import("webapi/storage/storage.zig");
const Navigation = @import("webapi/navigation/Navigation.zig");
const History = @import("webapi/History.zig");

const Page = @import("Page.zig");
const Browser = @import("Browser.zig");
Expand Down Expand Up @@ -54,6 +56,9 @@ executor: js.ExecutionWorld,
cookie_jar: storage.Cookie.Jar,
storage_shed: storage.Shed,

history: History,
navigation: Navigation,

page: ?*Page = null,

// If the current page want to navigate to a new page
Expand All @@ -67,13 +72,17 @@ pub fn init(self: *Session, browser: *Browser) !void {
errdefer executor.deinit();

const allocator = browser.app.allocator;
const session_allocator = browser.session_arena.allocator();

self.* = .{
.browser = browser,
.executor = executor,
.storage_shed = .{},
.queued_navigation = null,
.arena = browser.session_arena.allocator(),
.arena = session_allocator,
.cookie_jar = storage.Cookie.Jar.init(allocator),
.navigation = Navigation.init(session_allocator),
.history = .{},
.transfer_arena = browser.transfer_arena.allocator(),
};
}
Expand All @@ -98,6 +107,9 @@ pub fn createPage(self: *Session) !*Page {
self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self);
const page = self.page.?;

// Creates a new NavigationEventTarget for this page.
try self.navigation.onNewPage(page);

log.debug(.browser, "create page", .{});
// start JS env
// Inform CDP the main page has been created such that additional context for other Worlds can be created as well
Expand All @@ -115,6 +127,8 @@ pub fn removePage(self: *Session) void {
self.page.?.deinit();
self.page = null;

self.navigation.onRemovePage();

log.debug(.browser, "remove page", .{});
}

Expand Down Expand Up @@ -177,7 +191,11 @@ fn processQueuedNavigation(self: *Session) !bool {
return err;
};

page.navigate(qn.url, qn.opts) catch |err| {
page.navigate(
qn.url,
qn.opts,
self.navigation._current_navigation_kind orelse .{ .push = null },
) catch |err| {
log.err(.browser, "queued navigation error", .{ .err = err, .url = qn.url });
return err;
};
Expand Down
107 changes: 104 additions & 3 deletions src/browser/URL.zig
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@ pub fn resolve(allocator: Allocator, base: [:0]const u8, path: anytype, comptime
return std.mem.joinZ(allocator, "", &.{ base[0..path_start], path });
}

var normalized_base: []const u8 = base;
if (std.mem.lastIndexOfScalar(u8, normalized_base[authority_start..], '/')) |pos| {
normalized_base = normalized_base[0 .. pos + authority_start];
var normalized_base: []const u8 = base[0..path_start];
if (path_start < base.len) {
if (std.mem.lastIndexOfScalar(u8, base[path_start + 1 ..], '/')) |pos| {
normalized_base = base[0 .. path_start + 1 + pos];
}
}

// trailing space so that we always have space to append the null terminator
Expand Down Expand Up @@ -268,6 +270,14 @@ pub fn getHost(raw: [:0]const u8) []const u8 {
return authority[0..path_start];
}

// Returns true if these two URLs point to the same document.
pub fn eqlDocument(first: [:0]const u8, second: [:0]const u8) bool {
// First '#' signifies the start of the fragment.
const first_hash_index = std.mem.indexOfScalar(u8, first, '#') orelse first.len;
const second_hash_index = std.mem.indexOfScalar(u8, second, '#') orelse second.len;
return std.mem.eql(u8, first[0..first_hash_index], second[0..second_hash_index]);
}

const KnownProtocol = enum {
@"http:",
@"https:",
Expand All @@ -286,6 +296,29 @@ test "URL: isCompleteHTTPUrl" {
try testing.expectEqual(false, isCompleteHTTPUrl("about"));
}

test "URL: resolve regression (#1093)" {
defer testing.reset();

const Case = struct {
base: [:0]const u8,
path: [:0]const u8,
expected: [:0]const u8,
};

const cases = [_]Case{
.{
.base = "https://alas.aws.amazon.com/alas2.html",
.path = "../static/bootstrap.min.css",
.expected = "https://alas.aws.amazon.com/static/bootstrap.min.css",
},
};

for (cases) |case| {
const result = try resolve(testing.arena_allocator, case.base, case.path, .{});
try testing.expectString(case.expected, result);
}
}

test "URL: resolve" {
defer testing.reset();

Expand Down Expand Up @@ -413,3 +446,71 @@ test "URL: resolve" {
try testing.expectString(case.expected, result);
}
}

test "URL: eqlDocument" {
defer testing.reset();
{
const url = "https://lightpanda.io/about";
try testing.expectEqual(true, eqlDocument(url, url));
}
{
const url1 = "https://lightpanda.io/about";
const url2 = "http://lightpanda.io/about";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about";
const url2 = "https://example.com/about";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io:8080/about";
const url2 = "https://lightpanda.io:9090/about";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about";
const url2 = "https://lightpanda.io/contact";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about?foo=bar";
const url2 = "https://lightpanda.io/about?baz=qux";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about#section1";
const url2 = "https://lightpanda.io/about#section2";
try testing.expectEqual(true, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about";
const url2 = "https://lightpanda.io/about/";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about?foo=bar";
const url2 = "https://lightpanda.io/about";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about";
const url2 = "https://lightpanda.io/about?foo=bar";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about?foo=bar";
const url2 = "https://lightpanda.io/about?foo=bar";
try testing.expectEqual(true, eqlDocument(url1, url2));
}
{
const url1 = "https://lightpanda.io/about?";
const url2 = "https://lightpanda.io/about";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
{
const url1 = "https://duckduckgo.com/";
const url2 = "https://duckduckgo.com/?q=lightpanda";
try testing.expectEqual(false, eqlDocument(url1, url2));
}
}
2 changes: 1 addition & 1 deletion src/browser/js/Context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOp
}

if (T == js.Value) {
return value.value;
return value.js_val;
}

if (T == js.Promise) {
Expand Down
6 changes: 6 additions & 0 deletions src/browser/js/Value.zig
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ pub fn toString(self: Value, allocator: Allocator) ![]const u8 {
return self.context.valueToString(self.js_val, .{ .allocator = allocator });
}

pub fn fromJson(ctx: *js.Context, json: []const u8) !Value {
const json_string = v8.String.initUtf8(ctx.isolate, json);
const value = try v8.Json.parse(ctx.v8_context, json_string);
return Value{ .context = ctx, .js_val = value };
}

pub fn toObject(self: Value) js.Object {
return .{
.context = self.context,
Expand Down
7 changes: 7 additions & 0 deletions src/browser/js/bridge.zig
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,9 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/ErrorEvent.zig"),
@import("../webapi/event/MessageEvent.zig"),
@import("../webapi/event/ProgressEvent.zig"),
@import("../webapi/event/NavigationCurrentEntryChangeEvent.zig"),
@import("../webapi/event/PageTransitionEvent.zig"),
@import("../webapi/event/PopStateEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
@import("../webapi/media/MediaError.zig"),
Expand Down Expand Up @@ -599,4 +602,8 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/File.zig"),
@import("../webapi/Screen.zig"),
@import("../webapi/PerformanceObserver.zig"),
@import("../webapi/navigation/Navigation.zig"),
@import("../webapi/navigation/NavigationEventTarget.zig"),
@import("../webapi/navigation/NavigationHistoryEntry.zig"),
@import("../webapi/navigation/NavigationActivation.zig"),
});
Loading
Loading