mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 14:43:28 +00:00
418 lines
15 KiB
Zig
418 lines
15 KiB
Zig
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
|
//
|
|
// Francis Bouvier <francis@lightpanda.io>
|
|
// Pierre Tachoire <pierre@lightpanda.io>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const std = @import("std");
|
|
const Page = @import("../../browser/page.zig").Page;
|
|
const Notification = @import("../../notification.zig").Notification;
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
pub fn processMessage(cmd: anytype) !void {
|
|
const action = std.meta.stringToEnum(enum {
|
|
enable,
|
|
getFrameTree,
|
|
setLifecycleEventsEnabled,
|
|
addScriptToEvaluateOnNewDocument,
|
|
createIsolatedWorld,
|
|
navigate,
|
|
stopLoading,
|
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
|
|
|
switch (action) {
|
|
.enable => return cmd.sendResult(null, .{}),
|
|
.getFrameTree => return getFrameTree(cmd),
|
|
.setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),
|
|
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
|
|
.createIsolatedWorld => return createIsolatedWorld(cmd),
|
|
.navigate => return navigate(cmd),
|
|
.stopLoading => return cmd.sendResult(null, .{}),
|
|
}
|
|
}
|
|
|
|
const Frame = struct {
|
|
id: []const u8,
|
|
loaderId: []const u8,
|
|
url: []const u8,
|
|
domainAndRegistry: []const u8 = "",
|
|
securityOrigin: []const u8,
|
|
mimeType: []const u8 = "text/html",
|
|
adFrameStatus: struct {
|
|
adFrameType: []const u8 = "none",
|
|
} = .{},
|
|
secureContextType: []const u8,
|
|
crossOriginIsolatedContextType: []const u8 = "NotIsolated",
|
|
gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
|
|
};
|
|
|
|
fn getFrameTree(cmd: anytype) !void {
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
|
|
|
return cmd.sendResult(.{
|
|
.frameTree = .{
|
|
.frame = Frame{
|
|
.id = target_id,
|
|
.loaderId = bc.loader_id,
|
|
.securityOrigin = bc.security_origin,
|
|
.url = bc.getURL() orelse "about:blank",
|
|
.secureContextType = bc.secure_context_type,
|
|
},
|
|
},
|
|
}, .{});
|
|
}
|
|
|
|
fn setLifecycleEventsEnabled(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
enabled: bool,
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
if (params.enabled) {
|
|
try bc.lifecycleEventsEnable();
|
|
} else {
|
|
bc.lifecycleEventsDisable();
|
|
}
|
|
return cmd.sendResult(null, .{});
|
|
}
|
|
|
|
// TODO: hard coded method
|
|
// With the command we receive a script we need to store and run for each new document.
|
|
// Note that the worldName refers to the name given to the isolated world.
|
|
fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
|
|
// const params = (try cmd.params(struct {
|
|
// source: []const u8,
|
|
// worldName: ?[]const u8 = null,
|
|
// includeCommandLineAPI: bool = false,
|
|
// runImmediately: bool = false,
|
|
// })) orelse return error.InvalidParams;
|
|
|
|
return cmd.sendResult(.{
|
|
.identifier = "1",
|
|
}, .{});
|
|
}
|
|
|
|
fn createIsolatedWorld(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
frameId: []const u8,
|
|
worldName: []const u8,
|
|
grantUniveralAccess: bool,
|
|
})) orelse return error.InvalidParams;
|
|
if (!params.grantUniveralAccess) {
|
|
std.debug.print("grantUniveralAccess == false is not yet implemented", .{});
|
|
// When grantUniveralAccess == false and the client attempts to resolve
|
|
// or otherwise access a DOM or other JS Object from another context that should fail.
|
|
}
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
|
|
const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess);
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
try world.createContextAndLoadPolyfills(bc.arena, page);
|
|
const js_context = &world.executor.context.?;
|
|
|
|
// Create the auxdata json for the contextCreated event
|
|
// Calling contextCreated will assign a Id to the context and send the contextCreated event
|
|
const aux_data = try std.fmt.allocPrint(cmd.arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{params.frameId});
|
|
bc.inspector.contextCreated(js_context, world.name, "", aux_data, false);
|
|
|
|
return cmd.sendResult(.{ .executionContextId = js_context.v8_context.debugContextId() }, .{});
|
|
}
|
|
|
|
fn navigate(cmd: anytype) !void {
|
|
const params = (try cmd.params(struct {
|
|
url: []const u8,
|
|
// referrer: ?[]const u8 = null,
|
|
// transitionType: ?[]const u8 = null, // TODO: enum
|
|
// frameId: ?[]const u8 = null,
|
|
// referrerPolicy: ?[]const u8 = null, // TODO: enum
|
|
})) orelse return error.InvalidParams;
|
|
|
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
|
|
|
// didn't create?
|
|
// const target_id = bc.target_id orelse return error.TargetIdNotLoaded;
|
|
|
|
// didn't attach?
|
|
if (bc.session_id == null) {
|
|
return error.SessionIdNotLoaded;
|
|
}
|
|
|
|
var page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
bc.loader_id = bc.cdp.loader_id_gen.next();
|
|
|
|
try page.navigate(params.url, .{
|
|
.reason = .address_bar,
|
|
.cdp_id = cmd.input.id,
|
|
});
|
|
}
|
|
|
|
pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void {
|
|
// detachTarget could be called, in which case, we still have a page doing
|
|
// things, but no session.
|
|
const session_id = bc.session_id orelse return;
|
|
|
|
bc.loader_id = bc.cdp.loader_id_gen.next();
|
|
const loader_id = bc.loader_id;
|
|
const target_id = bc.target_id orelse unreachable;
|
|
|
|
bc.reset();
|
|
|
|
var cdp = bc.cdp;
|
|
const reason_: ?[]const u8 = switch (event.opts.reason) {
|
|
.anchor => "anchorClick",
|
|
.script, .history, .navigation => "scriptInitiated",
|
|
.form => switch (event.opts.method) {
|
|
.GET => "formSubmissionGet",
|
|
.POST => "formSubmissionPost",
|
|
else => unreachable,
|
|
},
|
|
.address_bar => null,
|
|
};
|
|
if (reason_) |reason| {
|
|
try cdp.sendEvent("Page.frameScheduledNavigation", .{
|
|
.frameId = target_id,
|
|
.delay = 0,
|
|
.reason = reason,
|
|
.url = event.url,
|
|
}, .{ .session_id = session_id });
|
|
|
|
try cdp.sendEvent("Page.frameRequestedNavigation", .{
|
|
.frameId = target_id,
|
|
.reason = reason,
|
|
.url = event.url,
|
|
.disposition = "currentTab",
|
|
}, .{ .session_id = session_id });
|
|
}
|
|
|
|
// frameStartedNavigating event
|
|
try cdp.sendEvent("Page.frameStartedNavigating", .{
|
|
.frameId = target_id,
|
|
.url = event.url,
|
|
.loaderId = loader_id,
|
|
.navigationType = "differentDocument",
|
|
}, .{ .session_id = session_id });
|
|
|
|
// frameStartedLoading event
|
|
try cdp.sendEvent("Page.frameStartedLoading", .{
|
|
.frameId = target_id,
|
|
}, .{ .session_id = session_id });
|
|
|
|
// Drivers are sensitive to the order of events. Some more than others.
|
|
// The result for the Page.navigate seems like it _must_ come after
|
|
// the frameStartedLoading, but before any lifecycleEvent. So we
|
|
// unfortunately have to put the input_id ito the NavigateOpts which gets
|
|
// passed back into the notification.
|
|
if (event.opts.cdp_id) |input_id| {
|
|
try cdp.sendJSON(.{
|
|
.id = input_id,
|
|
.result = .{
|
|
.frameId = target_id,
|
|
.loaderId = loader_id,
|
|
},
|
|
.sessionId = session_id,
|
|
});
|
|
}
|
|
|
|
if (bc.page_life_cycle_events) {
|
|
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
|
.name = "init",
|
|
.frameId = target_id,
|
|
.loaderId = loader_id,
|
|
.timestamp = event.timestamp,
|
|
}, .{ .session_id = session_id });
|
|
}
|
|
|
|
if (reason_ != null) {
|
|
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
|
|
.frameId = target_id,
|
|
}, .{ .session_id = session_id });
|
|
}
|
|
|
|
// When we actually recreated the context we should have the inspector send this event, see: resetContextGroup
|
|
// Sending this event will tell the client that the context ids they had are invalid and the context shouls be dropped
|
|
// The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts.
|
|
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
|
|
|
{
|
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
|
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
|
|
bc.inspector.contextCreated(
|
|
page.js,
|
|
"",
|
|
try page.origin(arena),
|
|
aux_data,
|
|
true,
|
|
);
|
|
}
|
|
for (bc.isolated_worlds.items) |*isolated_world| {
|
|
const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
|
|
// Calling contextCreated will assign a new Id to the context and send the contextCreated event
|
|
bc.inspector.contextCreated(
|
|
&isolated_world.executor.context.?,
|
|
isolated_world.name,
|
|
"://",
|
|
aux_json,
|
|
false,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn pageRemove(bc: anytype) !void {
|
|
// The main page is going to be removed, we need to remove contexts from other worlds first.
|
|
for (bc.isolated_worlds.items) |*isolated_world| {
|
|
try isolated_world.removeContext();
|
|
}
|
|
}
|
|
|
|
pub fn pageCreated(bc: anytype, page: *Page) !void {
|
|
for (bc.isolated_worlds.items) |*isolated_world| {
|
|
try isolated_world.createContextAndLoadPolyfills(bc.arena, page);
|
|
}
|
|
}
|
|
|
|
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
|
|
// detachTarget could be called, in which case, we still have a page doing
|
|
// things, but no session.
|
|
const session_id = bc.session_id orelse return;
|
|
const loader_id = bc.loader_id;
|
|
const target_id = bc.target_id orelse unreachable;
|
|
const timestamp = event.timestamp;
|
|
|
|
var cdp = bc.cdp;
|
|
// frameNavigated event
|
|
try cdp.sendEvent("Page.frameNavigated", .{
|
|
.type = "Navigation",
|
|
.frame = Frame{
|
|
.id = target_id,
|
|
.url = event.url,
|
|
.loaderId = bc.loader_id,
|
|
.securityOrigin = bc.security_origin,
|
|
.secureContextType = bc.secure_context_type,
|
|
},
|
|
}, .{ .session_id = session_id });
|
|
|
|
// The DOM.documentUpdated event must be send after the frameNavigated one.
|
|
// chromedp client expects to receive the events is this order.
|
|
// see https://github.com/chromedp/chromedp/issues/1558
|
|
try cdp.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id });
|
|
|
|
// domContentEventFired event
|
|
// TODO: partially hard coded
|
|
try cdp.sendEvent(
|
|
"Page.domContentEventFired",
|
|
.{ .timestamp = timestamp },
|
|
.{ .session_id = session_id },
|
|
);
|
|
|
|
// lifecycle DOMContentLoaded event
|
|
// TODO: partially hard coded
|
|
if (bc.page_life_cycle_events) {
|
|
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
|
.timestamp = timestamp,
|
|
.name = "DOMContentLoaded",
|
|
.frameId = target_id,
|
|
.loaderId = loader_id,
|
|
}, .{ .session_id = session_id });
|
|
}
|
|
|
|
// loadEventFired event
|
|
try cdp.sendEvent(
|
|
"Page.loadEventFired",
|
|
.{ .timestamp = timestamp },
|
|
.{ .session_id = session_id },
|
|
);
|
|
|
|
// lifecycle DOMContentLoaded event
|
|
if (bc.page_life_cycle_events) {
|
|
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
|
.timestamp = timestamp,
|
|
.name = "load",
|
|
.frameId = target_id,
|
|
.loaderId = loader_id,
|
|
}, .{ .session_id = session_id });
|
|
}
|
|
|
|
// frameStoppedLoading
|
|
return cdp.sendEvent("Page.frameStoppedLoading", .{
|
|
.frameId = target_id,
|
|
}, .{ .session_id = session_id });
|
|
}
|
|
|
|
pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void {
|
|
return sendPageLifecycle(bc, "networkIdle", event.timestamp);
|
|
}
|
|
|
|
pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetworkAlmostIdle) !void {
|
|
return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp);
|
|
}
|
|
|
|
fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u32) !void {
|
|
// detachTarget could be called, in which case, we still have a page doing
|
|
// things, but no session.
|
|
const session_id = bc.session_id orelse return;
|
|
|
|
const loader_id = bc.loader_id;
|
|
const target_id = bc.target_id orelse unreachable;
|
|
return bc.cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
|
|
.name = name,
|
|
.frameId = target_id,
|
|
.loaderId = loader_id,
|
|
.timestamp = timestamp,
|
|
}, .{ .session_id = session_id });
|
|
}
|
|
|
|
const LifecycleEvent = struct {
|
|
frameId: []const u8,
|
|
loaderId: ?[]const u8,
|
|
name: []const u8,
|
|
timestamp: u32,
|
|
};
|
|
|
|
const testing = @import("../testing.zig");
|
|
test "cdp.page: getFrameTree" {
|
|
var ctx = testing.context();
|
|
defer ctx.deinit();
|
|
|
|
{
|
|
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Page.getFrameTree", .params = .{ .targetId = "X" } }));
|
|
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
|
}
|
|
|
|
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .target_id = "TID-3" });
|
|
{
|
|
try ctx.processMessage(.{ .id = 11, .method = "Page.getFrameTree" });
|
|
try ctx.expectSentResult(.{
|
|
.frameTree = .{
|
|
.frame = .{
|
|
.id = "TID-3",
|
|
.loaderId = bc.loader_id,
|
|
.url = "about:blank",
|
|
.domainAndRegistry = "",
|
|
.securityOrigin = bc.security_origin,
|
|
.mimeType = "text/html",
|
|
.adFrameStatus = .{
|
|
.adFrameType = "none",
|
|
},
|
|
.secureContextType = bc.secure_context_type,
|
|
.crossOriginIsolatedContextType = "NotIsolated",
|
|
.gatedAPIFeatures = [_][]const u8{},
|
|
},
|
|
},
|
|
}, .{ .id = 11 });
|
|
}
|
|
}
|