diff --git a/src/browser/css/parser.zig b/src/browser/css/parser.zig index 63b863d8..d93fdb80 100644 --- a/src/browser/css/parser.zig +++ b/src/browser/css/parser.zig @@ -821,7 +821,8 @@ pub const Parser = struct { // nameStart returns whether c can be the first character of an identifier // (not counting an initial hyphen, or an escape sequence). fn nameStart(c: u8) bool { - return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127; + return 'a' <= c and c <= 'z' or 'A' <= c and c <= 'Z' or c == '_' or c > 127 or + '0' <= c and c <= '9'; } // nameChar returns whether c can be a character within an identifier @@ -890,7 +891,7 @@ test "parser.parseIdentifier" { err: bool = false, }{ .{ .s = "x", .exp = "x" }, - .{ .s = "96", .exp = "", .err = true }, + .{ .s = "96", .exp = "96", .err = false }, .{ .s = "-x", .exp = "-x" }, .{ .s = "r\\e9 sumé", .exp = "résumé" }, .{ .s = "r\\0000e9 sumé", .exp = "résumé" }, @@ -975,6 +976,7 @@ test "parser.parse" { .{ .s = ":root", .exp = .{ .pseudo_class = .root } }, .{ .s = ".\\:bar", .exp = .{ .class = ":bar" } }, .{ .s = ".foo\\:bar", .exp = .{ .class = "foo:bar" } }, + .{ .s = "[class=75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a]", .exp = .{ .attribute = .{ .key = "class", .val = "75c0fa18a94b9e3a6b8e14d6cbe688a27f5da10a", .op = .eql } } }, }; for (testcases) |tc| { diff --git a/src/browser/css/selector.zig b/src/browser/css/selector.zig index 00ef1558..a6402efc 100644 --- a/src/browser/css/selector.zig +++ b/src/browser/css/selector.zig @@ -993,6 +993,11 @@ test "Browser.CSS.Selector: matchFirst" { .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, .exp = 0, }, + .{ + .q = "[foo=1baz]", + .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, + .exp = 0, + }, .{ .q = "[foo!=bar]", .n = .{ .child = &.{ .name = "p", .sibling = &.{ .name = "p", .att = "bar" } } }, diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 3fcf14dc..3373d51d 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -151,18 +151,18 @@ pub fn CDPT(comptime TypeProvider: type) type { if (std.mem.eql(u8, input_session_id, "STARTUP")) { is_startup = true; } else if (self.isValidSessionId(input_session_id) == false) { - return command.sendError(-32001, "Unknown sessionId"); + return command.sendError(-32001, "Unknown sessionId", .{}); } } if (is_startup) { dispatchStartupCommand(&command) catch |err| { - command.sendError(-31999, @errorName(err)) catch {}; + command.sendError(-31999, @errorName(err), .{}) catch {}; return err; }; } else { dispatchCommand(&command, input.method) catch |err| { - command.sendError(-31998, @errorName(err)) catch {}; + command.sendError(-31998, @errorName(err), .{}) catch {}; return err; }; } @@ -331,7 +331,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { node_search_list: Node.Search.List, inspector: Inspector, - isolated_world: ?IsolatedWorld, + isolated_worlds: std.ArrayListUnmanaged(IsolatedWorld), http_proxy_changed: bool = false, @@ -375,7 +375,7 @@ pub fn BrowserContext(comptime CDP_T: type) type { .page_life_cycle_events = false, // TODO; Target based value .node_registry = registry, .node_search_list = undefined, - .isolated_world = null, + .isolated_worlds = .empty, .inspector = inspector, .notification_arena = cdp.notification_arena.allocator(), .intercept_state = try InterceptState.init(allocator), @@ -404,9 +404,10 @@ pub fn BrowserContext(comptime CDP_T: type) type { // so we need to shutdown the page one first. self.cdp.browser.closeSession(); - if (self.isolated_world) |*world| { + for (self.isolated_worlds.items) |*world| { world.deinit(); } + self.isolated_worlds.clearRetainingCapacity(); self.node_registry.deinit(); self.node_search_list.deinit(); self.cdp.browser.notification.unregisterAll(self); @@ -427,19 +428,19 @@ pub fn BrowserContext(comptime CDP_T: type) type { } pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { - if (self.isolated_world != null) { - return error.CurrentlyOnly1IsolatedWorldSupported; - } - var executor = try self.cdp.browser.env.newExecutionWorld(); errdefer executor.deinit(); - self.isolated_world = .{ - .name = try self.arena.dupe(u8, world_name), + const owned_name = try self.arena.dupe(u8, world_name); + const world = try self.isolated_worlds.addOne(self.arena); + + world.* = .{ + .name = owned_name, .executor = executor, .grant_universal_access = grant_universal_access, }; - return &self.isolated_world.?; + + return world; } pub fn nodeWriter(self: *Self, root: *const Node, opts: Node.Writer.Opts) Node.Writer { @@ -682,7 +683,14 @@ const IsolatedWorld = struct { // This also means this pointer becomes invalid after removePage untill a new page is created. // Currently we have only 1 page/frame and thus also only 1 state in the isolate world. pub fn createContext(self: *IsolatedWorld, page: *Page) !void { - if (self.executor.js_context != null) return error.Only1IsolatedContextSupported; + // if (self.executor.js_context != null) return error.Only1IsolatedContextSupported; + if (self.executor.js_context != null) { + log.warn(.cdp, "not implemented", .{ + .feature = "createContext: Not implemented second isolated context creation", + .info = "reuse existing context", + }); + return; + } _ = try self.executor.createJsContext( &page.window, page, @@ -691,6 +699,14 @@ const IsolatedWorld = struct { Env.GlobalMissingCallback.init(&self.polyfill_loader), ); } + + pub fn createContextAndLoadPolyfills(self: *IsolatedWorld, arena: Allocator, page: *Page) !void { + // We need to recreate the isolated world context + try self.createContext(page); + + const loader = @import("../browser/polyfill/polyfill.zig"); + try loader.preload(arena, &self.executor.js_context.?); + } }; // This is a generic because when we send a result we have two different @@ -757,10 +773,14 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type { return self.cdp.sendEvent(method, p, opts); } - pub fn sendError(self: *Self, code: i32, message: []const u8) !void { + const SendErrorOpts = struct { + include_session_id: bool = true, + }; + pub fn sendError(self: *Self, code: i32, message: []const u8, opts: SendErrorOpts) !void { return self.sender.sendJSON(.{ .id = self.input.id, .@"error" = .{ .code = code, .message = message }, + .sessionId = if (opts.include_session_id) self.input.session_id else null, }); } diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 64f24fdf..035918e8 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const log = @import("../../log.zig"); const Allocator = std.mem.Allocator; const Node = @import("../Node.zig"); const css = @import("../../browser/dom/css.zig"); @@ -39,6 +40,7 @@ pub fn processMessage(cmd: anytype) !void { getContentQuads, getBoxModel, requestChildNodes, + getFrameOwner, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { @@ -55,6 +57,7 @@ pub fn processMessage(cmd: anytype) !void { .getContentQuads => return getContentQuads(cmd), .getBoxModel => return getBoxModel(cmd), .requestChildNodes => return requestChildNodes(cmd), + .getFrameOwner => return getFrameOwner(cmd), } } @@ -67,6 +70,10 @@ fn getDocument(cmd: anytype) !void { }; const params = try cmd.params(Params) orelse Params{}; + if (params.pierce) { + log.warn(.cdp, "not implemented", .{ .feature = "DOM.getDocument: Not implemented pierce parameter" }); + } + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded; const doc = parser.documentHTMLToDocument(page.window.document); @@ -206,7 +213,9 @@ fn querySelector(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse return error.UnknownNode; + const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { + return cmd.sendError(-32000, "Could not find node with given id", .{}); + }; const selected_node = try css.querySelector( cmd.arena, @@ -233,7 +242,9 @@ fn querySelectorAll(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; - const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse return error.UnknownNode; + const node = bc.node_registry.lookup_by_id.get(params.nodeId) orelse { + return cmd.sendError(-32000, "Could not find node with given id", .{}); + }; const arena = cmd.arena; const selected_nodes = try css.querySelectorAll(arena, node._node, params.selector); @@ -266,10 +277,12 @@ fn resolveNode(cmd: anytype) !void { var js_context = page.main_context; if (params.executionContextId) |context_id| { if (js_context.v8_context.debugContextId() != context_id) { - var isolated_world = bc.isolated_world orelse return error.ContextNotFound; - js_context = &(isolated_world.executor.js_context orelse return error.ContextNotFound); - - if (js_context.v8_context.debugContextId() != context_id) return error.ContextNotFound; + for (bc.isolated_worlds.items) |*isolated_world| { + js_context = &(isolated_world.executor.js_context orelse return error.ContextNotFound); + if (js_context.v8_context.debugContextId() == context_id) { + break; + } + } else return error.ContextNotFound; } } @@ -300,16 +313,18 @@ fn describeNode(cmd: anytype) !void { nodeId: ?Node.Id = null, backendNodeId: ?Node.Id = null, objectId: ?[]const u8 = null, - depth: u32 = 1, + depth: i32 = 1, pierce: bool = false, })) orelse return error.InvalidParams; - if (params.depth != 1 or params.pierce) return error.NotImplemented; + if (params.pierce) { + log.warn(.cdp, "not implemented", .{ .feature = "DOM.describeNode: Not implemented pierce parameter" }); + } const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const node = try getNode(cmd.arena, bc, params.nodeId, params.backendNodeId, params.objectId); - return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{}) }, .{}); + return cmd.sendResult(.{ .node = bc.nodeWriter(node, .{ .depth = params.depth }) }, .{}); } // An array of quad vertices, x immediately followed by y for each point, points clock-wise. @@ -461,6 +476,24 @@ fn requestChildNodes(cmd: anytype) !void { return cmd.sendResult(null, .{}); } +fn getFrameOwner(cmd: anytype) !void { + const params = (try cmd.params(struct { + frameId: []const u8, + })) orelse return error.InvalidParams; + + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + const target_id = bc.target_id orelse return error.TargetNotLoaded; + if (std.mem.eql(u8, target_id, params.frameId) == false) { + return cmd.sendError(-32000, "Frame with the given id does not belong to the target.", .{}); + } + + const page = bc.session.currentPage() orelse return error.PageNotLoaded; + const doc = parser.documentHTMLToDocument(page.window.document); + + const node = try bc.node_registry.register(parser.documentToNode(doc)); + return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{}); +} + const testing = @import("../testing.zig"); test "cdp.dom: getSearchResults unknown search id" { @@ -534,16 +567,19 @@ test "cdp.dom: querySelector unknown search id" { _ = try ctx.loadBrowserContext(.{ .id = "BID-A", .html = "

1

2

" }); - try testing.expectError(error.UnknownNode, ctx.processMessage(.{ + try ctx.processMessage(.{ .id = 9, .method = "DOM.querySelector", .params = .{ .nodeId = 99, .selector = "" }, - })); - try testing.expectError(error.UnknownNode, ctx.processMessage(.{ + }); + try ctx.expectSentError(-32000, "Could not find node with given id", .{}); + + try ctx.processMessage(.{ .id = 9, .method = "DOM.querySelectorAll", .params = .{ .nodeId = 99, .selector = "" }, - })); + }); + try ctx.expectSentError(-32000, "Could not find node with given id", .{}); } test "cdp.dom: querySelector Node not found" { diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index c8a1788d..83e2b8f8 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -244,7 +244,14 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, msg: *const Notification. const transfer = msg.transfer; // We're missing a bunch of fields, but, for now, this seems like enough - try bc.cdp.sendEvent("Network.requestWillBeSent", .{ .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), .frameId = target_id, .loaderId = bc.loader_id, .documentUrl = DocumentUrlWriter.init(&page.url.uri), .request = TransferAsRequestWriter.init(transfer) }, .{ .session_id = session_id }); + try bc.cdp.sendEvent("Network.requestWillBeSent", .{ + .requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}), + .frameId = target_id, + .loaderId = bc.loader_id, + .documentUrl = DocumentUrlWriter.init(&page.url.uri), + .request = TransferAsRequestWriter.init(transfer), + .initiator = .{ .type = "other" }, + }, .{ .session_id = session_id }); } pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notification.ResponseHeaderDone) !void { diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 4495a474..978ae7a8 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -122,7 +122,7 @@ fn createIsolatedWorld(cmd: anytype) !void { const world = try bc.createIsolatedWorld(params.worldName, params.grantUniveralAccess); const page = bc.session.currentPage() orelse return error.PageNotLoaded; - try pageCreated(bc, page); + try world.createContextAndLoadPolyfills(bc.arena, page); const js_context = &world.executor.js_context.?; // Create the auxdata json for the contextCreated event @@ -259,7 +259,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa true, ); } - if (bc.isolated_world) |*isolated_world| { + 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( @@ -274,18 +274,14 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa pub fn pageRemove(bc: anytype) !void { // The main page is going to be removed, we need to remove contexts from other worlds first. - if (bc.isolated_world) |*isolated_world| { + for (bc.isolated_worlds.items) |*isolated_world| { try isolated_world.removeContext(); } } pub fn pageCreated(bc: anytype, page: *Page) !void { - if (bc.isolated_world) |*isolated_world| { - // We need to recreate the isolated world context - try isolated_world.createContext(page); - - const polyfill = @import("../../browser/polyfill/polyfill.zig"); - try polyfill.preload(bc.arena, &isolated_world.executor.js_context.?); + for (bc.isolated_worlds.items) |*isolated_world| { + try isolated_world.createContextAndLoadPolyfills(bc.arena, page); } } diff --git a/src/cdp/domains/runtime.zig b/src/cdp/domains/runtime.zig index 707b5912..138155db 100644 --- a/src/cdp/domains/runtime.zig +++ b/src/cdp/domains/runtime.zig @@ -27,6 +27,7 @@ pub fn processMessage(cmd: anytype) !void { addBinding, callFunctionOn, releaseObject, + getProperties, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index 87fe4324..b3b839ad 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -79,7 +79,7 @@ fn createBrowserContext(cmd: anytype) !void { } const bc = cmd.createBrowserContext() catch |err| switch (err) { - error.AlreadyExists => return cmd.sendError(-32000, "Cannot have more than one browser context at a time"), + error.AlreadyExists => return cmd.sendError(-32000, "Cannot have more than one browser context at a time", .{}), else => return err, }; @@ -102,7 +102,7 @@ fn disposeBrowserContext(cmd: anytype) !void { })) orelse return error.InvalidParams; if (cmd.cdp.disposeBrowserContext(params.browserContextId) == false) { - return cmd.sendError(-32602, "No browser context with the given id found"); + return cmd.sendError(-32602, "No browser context with the given id found", .{}); } try cmd.sendResult(null, .{}); } @@ -241,10 +241,10 @@ fn closeTarget(cmd: anytype) !void { } bc.session.removePage(); - if (bc.isolated_world) |*world| { + for (bc.isolated_worlds.items) |*world| { world.deinit(); - bc.isolated_world = null; } + bc.isolated_worlds.clearRetainingCapacity(); bc.target_id = null; }