mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 01:36:15 -04:00
feat(cdp): honor Browser.grantPermissions / setPermission / resetPermissions
These three Browser methods were noops, so a CDP client could not change what navigator.permissions.query() reports - it always returned "prompt". Store permission state on the active Page (keyed by permission name) and have navigator.permissions.query() read it back. grantPermissions sets each listed permission to "granted", setPermission sets a single permission to an explicit state, and resetPermissions clears them (query falls back to "prompt"). State is scoped to the Page and resets on navigation. Adds a CDP round-trip test exercising all three methods.
This commit is contained in:
@@ -70,6 +70,12 @@ frame_arena: Allocator,
|
||||
// lifetime.
|
||||
origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
|
||||
|
||||
// Permission state set via CDP Browser.grantPermissions / setPermission /
|
||||
// resetPermissions, keyed by permission name (e.g. "geolocation"). Read back
|
||||
// by navigator.permissions.query(). Keys and values are duped into
|
||||
// frame_arena, so they live for (and reset with) the Page.
|
||||
permissions: std.StringHashMapUnmanaged([]const u8) = .empty,
|
||||
|
||||
// Identity tracking for the main world. All main-world contexts in this Page
|
||||
// share this, ensuring object identity works across same-origin frames.
|
||||
identity: js.Identity = .{},
|
||||
@@ -187,6 +193,10 @@ pub fn deinit(self: *Page) void {
|
||||
self.origins = .empty;
|
||||
}
|
||||
|
||||
// Keys and values are duped into frame_arena, released just below; drop
|
||||
// the map so we don't keep a dangling reference.
|
||||
self.permissions = .empty;
|
||||
|
||||
session.arena_pool.release(self.frame_arena);
|
||||
}
|
||||
|
||||
@@ -198,6 +208,16 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
|
||||
return self.session.releaseArena(allocator);
|
||||
}
|
||||
|
||||
// Set (or overwrite) the stored state for a permission. Name and state are
|
||||
// duped into frame_arena. Used by CDP Browser.grantPermissions / setPermission.
|
||||
pub fn setPermission(self: *Page, name: []const u8, state: []const u8) !void {
|
||||
const gop = try self.permissions.getOrPut(self.frame_arena, name);
|
||||
if (!gop.found_existing) {
|
||||
gop.key_ptr.* = try self.frame_arena.dupe(u8, name);
|
||||
}
|
||||
gop.value_ptr.* = try self.frame_arena.dupe(u8, state);
|
||||
}
|
||||
|
||||
pub fn getOrCreateOrigin(self: *Page, key_: ?[]const u8) !*js.Origin {
|
||||
const session = self.session;
|
||||
const key = key_ orelse {
|
||||
|
||||
@@ -37,15 +37,19 @@ _pad: bool = false,
|
||||
const QueryDescriptor = struct {
|
||||
name: []const u8,
|
||||
};
|
||||
// We always report 'prompt' (the default safe value — neither granted nor denied).
|
||||
|
||||
// Report the state set via CDP Browser.grantPermissions / setPermission, or
|
||||
// 'prompt' (the default safe value — neither granted nor denied) when unset.
|
||||
pub fn query(_: *const Permissions, qd: QueryDescriptor, exec: *const Execution) !js.Promise {
|
||||
const arena = try exec.getArena(.tiny, "PermissionStatus");
|
||||
errdefer exec.releaseArena(arena);
|
||||
|
||||
const state = exec.page.permissions.get(qd.name) orelse "prompt";
|
||||
|
||||
const status = try arena.create(PermissionStatus);
|
||||
status.* = .{
|
||||
._arena = arena,
|
||||
._state = "prompt",
|
||||
._state = try arena.dupe(u8, state),
|
||||
._name = try arena.dupe(u8, qd.name),
|
||||
};
|
||||
return exec.js.local.?.resolvePromise(status);
|
||||
|
||||
@@ -96,19 +96,50 @@ fn setWindowBounds(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
// Grant the listed permissions so navigator.permissions.query() reports them
|
||||
// as "granted". State is stored on the active Page and resets on navigation.
|
||||
fn grantPermissions(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
const params = (try cmd.params(struct {
|
||||
permissions: []const []const u8,
|
||||
origin: ?[]const u8 = null,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
for (params.permissions) |name| {
|
||||
try page.setPermission(name, "granted");
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
// Set a single permission to an explicit state ("granted", "denied" or
|
||||
// "prompt"), reflected by navigator.permissions.query().
|
||||
fn setPermission(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
const params = (try cmd.params(struct {
|
||||
permission: struct { name: []const u8 },
|
||||
setting: []const u8,
|
||||
origin: ?[]const u8 = null,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
try page.setPermission(params.permission.name, params.setting);
|
||||
|
||||
return cmd.sendResult(null, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
// Clear all granted permissions; navigator.permissions.query() falls back to
|
||||
// the default "prompt".
|
||||
fn resetPermissions(cmd: *CDP.Command) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
if (bc.session.currentPage()) |page| {
|
||||
page.permissions.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
@@ -146,3 +177,38 @@ test "cdp.browser: getWindowForTarget" {
|
||||
.bounds = .{ .windowState = "normal" },
|
||||
}, .{ .id = 33, .index = 0, .session_id = null });
|
||||
}
|
||||
|
||||
test "cdp.browser: grant/set/reset permissions reach navigator.permissions" {
|
||||
var ctx = try testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-PERM", .url = "cdp/dom1.html" });
|
||||
const page = bc.session.currentPage() orelse unreachable;
|
||||
|
||||
// grantPermissions: each listed permission becomes "granted".
|
||||
try ctx.processMessage(.{
|
||||
.id = 40,
|
||||
.method = "Browser.grantPermissions",
|
||||
.params = .{ .permissions = &[_][]const u8{ "geolocation", "notifications" } },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 40, .session_id = null });
|
||||
try testing.expectEqualSlices(u8, "granted", page.permissions.get("geolocation").?);
|
||||
try testing.expectEqualSlices(u8, "granted", page.permissions.get("notifications").?);
|
||||
|
||||
// setPermission: override a single permission to an explicit state.
|
||||
try ctx.processMessage(.{
|
||||
.id = 41,
|
||||
.method = "Browser.setPermission",
|
||||
.params = .{ .permission = .{ .name = "geolocation" }, .setting = "denied" },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 41, .session_id = null });
|
||||
try testing.expectEqualSlices(u8, "denied", page.permissions.get("geolocation").?);
|
||||
|
||||
// resetPermissions: clears everything; query falls back to "prompt".
|
||||
try ctx.processMessage(.{
|
||||
.id = 42,
|
||||
.method = "Browser.resetPermissions",
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 42, .session_id = null });
|
||||
try testing.expectEqual(@as(usize, 0), page.permissions.count());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user