diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index cf6e999e..1b035837 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -725,6 +725,9 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/collections.zig"), @import("../webapi/Console.zig"), @import("../webapi/Crypto.zig"), + @import("../webapi/Permissions.zig"), + @import("../webapi/Permissions.zig").PermissionStatus, + @import("../webapi/StorageManager.zig"), @import("../webapi/CSS.zig"), @import("../webapi/css/CSSRule.zig"), @import("../webapi/css/CSSRuleList.zig"), diff --git a/src/browser/tests/navigator/permissions.html b/src/browser/tests/navigator/permissions.html new file mode 100644 index 00000000..befecf8b --- /dev/null +++ b/src/browser/tests/navigator/permissions.html @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 5e69db07..36d32f1e 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -21,10 +21,14 @@ const builtin = @import("builtin"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); const PluginArray = @import("PluginArray.zig"); +const Permissions = @import("Permissions.zig"); +const StorageManager = @import("StorageManager.zig"); const Navigator = @This(); _pad: bool = false, _plugins: PluginArray = .{}, +_permissions: Permissions = Permissions.init, +_storage: StorageManager = StorageManager.init, pub const init: Navigator = .{}; @@ -55,6 +59,20 @@ pub fn getPlugins(self: *Navigator) *PluginArray { return &self._plugins; } +pub fn getPermissions(self: *Navigator) *Permissions { + return &self._permissions; +} + +pub fn getStorage(self: *Navigator) *StorageManager { + return &self._storage; +} + +pub fn getBattery(_: *const Navigator, page: *Page) !js.Promise { + // Battery API is not supported in headless mode. + // Return a rejected Promise — callers must .catch() this. + return js.Promise.reject(error.NotSupportedError, page); +} + pub fn registerProtocolHandler(_: *const Navigator, scheme: []const u8, url: [:0]const u8, page: *const Page) !void { try validateProtocolHandlerScheme(scheme); try validateProtocolHandlerURL(url, page); @@ -144,6 +162,7 @@ pub const JsApi = struct { pub const onLine = bridge.property(true, .{ .template = false }); pub const cookieEnabled = bridge.property(true, .{ .template = false }); pub const hardwareConcurrency = bridge.property(4, .{ .template = false }); + pub const deviceMemory = bridge.property(@as(f64, 8.0), .{ .template = false }); pub const maxTouchPoints = bridge.property(0, .{ .template = false }); pub const vendor = bridge.property("", .{ .template = false }); pub const product = bridge.property("Gecko", .{ .template = false }); @@ -156,4 +175,7 @@ pub const JsApi = struct { // Methods pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{}); + pub const getBattery = bridge.function(Navigator.getBattery, .{}); + pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{}); + pub const storage = bridge.accessor(Navigator.getStorage, null, .{}); }; diff --git a/src/browser/webapi/Permissions.zig b/src/browser/webapi/Permissions.zig new file mode 100644 index 00000000..5f5954b3 --- /dev/null +++ b/src/browser/webapi/Permissions.zig @@ -0,0 +1,61 @@ +// src/browser/webapi/Permissions.zig +// +// Minimal Permissions API stub. +// https://www.w3.org/TR/permissions/ +// +// Turnstile probes: navigator.permissions.query({ name: 'notifications' }) +// It expects a Promise resolving to { state: 'granted' | 'denied' | 'prompt' } + +const std = @import("std"); +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); + +const Permissions = @This(); + +// Padding to avoid zero-size struct pointer collisions +_pad: bool = false, + +pub const init: Permissions = .{}; + +const QueryDescriptor = struct { + name: []const u8, +}; + +const PermissionStatus = struct { + state: []const u8, + + pub const JsApi = struct { + pub const bridge = js.Bridge(PermissionStatus); + pub const Meta = struct { + pub const name = "PermissionStatus"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + pub const state = bridge.accessor(getState, null, .{}); + }; + + fn getState(self: *const PermissionStatus) []const u8 { + return self.state; + } +}; + +// query() returns a Promise. +// We always report 'prompt' (the default safe value — neither granted nor denied). +pub fn query(_: *const Permissions, _: QueryDescriptor, page: *Page) !js.Promise { + const status = try page._factory.create(PermissionStatus{ .state = "prompt" }); + return js.Promise.resolve(status, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Permissions); + + pub const Meta = struct { + pub const name = "Permissions"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + + pub const query = bridge.function(Permissions.query, .{ .dom_exception = true }); +}; diff --git a/src/browser/webapi/StorageManager.zig b/src/browser/webapi/StorageManager.zig new file mode 100644 index 00000000..280614a0 --- /dev/null +++ b/src/browser/webapi/StorageManager.zig @@ -0,0 +1,56 @@ +// src/browser/webapi/StorageManager.zig +// Minimal stub for navigator.storage +// https://storage.spec.whatwg.org/#storagemanager + +const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); + +const StorageManager = @This(); +_pad: bool = false, + +pub const init: StorageManager = .{}; + +const StorageEstimate = struct { + quota: u64, + usage: u64, + + pub const JsApi = struct { + pub const bridge = js.Bridge(StorageEstimate); + pub const Meta = struct { + pub const name = "StorageEstimate"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + pub const quota = bridge.accessor(getQuota, null, .{}); + pub const usage = bridge.accessor(getUsage, null, .{}); + }; + + fn getQuota(self: *const StorageEstimate) u64 { + return self.quota; + } + fn getUsage(self: *const StorageEstimate) u64 { + return self.usage; + } +}; + +// Returns a resolved Promise with plausible stub values. +// quota = 1GB, usage = 0 (headless browser has no real storage) +pub fn estimate(_: *const StorageManager, page: *Page) !js.Promise { + const est = try page._factory.create(StorageEstimate{ + .quota = 1024 * 1024 * 1024, // 1 GiB + .usage = 0, + }); + return js.Promise.resolve(est, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(StorageManager); + pub const Meta = struct { + pub const name = "StorageManager"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const empty_with_no_proto = true; + }; + pub const estimate = bridge.function(StorageManager.estimate, .{}); +};