From f5c458047ed759c65e4468714feeb538f8aa864c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 26 Aug 2026 16:34:26 +0800 Subject: [PATCH] WebAPI: Add WorkerNavigator A worker gets a distinct Navigator type (WorkerNavigator) which only exposes a subset of what Navigator does. Previously, we achieved this by exposing Navigator but with a bunch of accessors / functions with .{.exposed = .window}. That worked fine, except the type name was still "Navigator". This introduces an actual WorkerNavigator. --- src/browser/js/bridge.zig | 3 +- src/browser/tests/worker/navigator-worker.js | 7 + src/browser/tests/worker/worker.html | 6 + src/browser/webapi/Navigator.zig | 14 +- src/browser/webapi/WorkerGlobalScope.zig | 6 +- src/browser/webapi/WorkerNavigator.zig | 135 +++++++++++++++++++ 6 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 src/browser/webapi/WorkerNavigator.zig diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 1188f8b4f..ea5d627ce 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -1241,7 +1241,7 @@ pub const PageJsApis = flattenTypes(&.{ const worker_common_apis = [_]type{ @import("../webapi/WorkerGlobalScope.zig"), @import("../webapi/WorkerLocation.zig"), - @import("../webapi/Navigator.zig"), + @import("../webapi/WorkerNavigator.zig"), @import("../webapi/NavigatorUAData.zig"), @import("../webapi/Permissions.zig"), @import("../webapi/StorageManager.zig"), @@ -1329,6 +1329,7 @@ pub const JsApis = blk: { @import("../webapi/SharedWorkerGlobalScope.zig").JsApi, @import("../webapi/WorkerGlobalScope.zig").JsApi, @import("../webapi/WorkerLocation.zig").JsApi, + @import("../webapi/WorkerNavigator.zig").JsApi, }; if (lp.build_config.wpt_extensions == false) { break :blk base; diff --git a/src/browser/tests/worker/navigator-worker.js b/src/browser/tests/worker/navigator-worker.js index 4136e2013..f5b3171cf 100644 --- a/src/browser/tests/worker/navigator-worker.js +++ b/src/browser/tests/worker/navigator-worker.js @@ -15,6 +15,10 @@ onmessage = async function(event) { const results = { has_navigator: typeof navigator !== 'undefined', + // The worker realm exposes WorkerNavigator, not Navigator. + is_worker_navigator: Object.getPrototypeOf(navigator) === WorkerNavigator.prototype, + to_string_tag: Object.prototype.toString.call(navigator), + no_navigator_interface: typeof Navigator === 'undefined', // userAgent must match the value the page sees (passed in via postMessage). user_agent: navigator.userAgent, user_agent_matches_page: navigator.userAgent === event.data.pageUserAgent, @@ -25,6 +29,7 @@ onmessage = async function(event) { identity_stable: navigator === navigator, // Permissions + permissions_distinct: navigator.permissions !== navigator, permission_name: status.name, permission_state: status.state, @@ -40,6 +45,8 @@ onmessage = async function(event) { no_plugins: navigator.plugins === undefined, no_register_protocol_handler: navigator.registerProtocolHandler === undefined, no_model_context: navigator.modelContext === undefined, + no_send_beacon: navigator.sendBeacon === undefined, + no_cookie_enabled: navigator.cookieEnabled === undefined, }; postMessage({ ok: true, results }); } catch (e) { diff --git a/src/browser/tests/worker/worker.html b/src/browser/tests/worker/worker.html index 2b953aff1..5a8562968 100644 --- a/src/browser/tests/worker/worker.html +++ b/src/browser/tests/worker/worker.html @@ -270,6 +270,9 @@ const r = data.results; testing.expectEqual(true, r.has_navigator); + testing.expectEqual(true, r.is_worker_navigator); + testing.expectEqual('[object WorkerNavigator]', r.to_string_tag); + testing.expectEqual(true, r.no_navigator_interface); testing.expectEqual(true, r.user_agent_matches_page); testing.expectEqual('Netscape', r.app_name); testing.expectTrue(r.platform.length > 0); @@ -277,6 +280,7 @@ testing.expectEqual(true, r.identity_stable); // Permissions (transitive: Permissions -> PermissionStatus) + testing.expectEqual(true, r.permissions_distinct); testing.expectEqual('geolocation', r.permission_name); testing.expectEqual('prompt', r.permission_state); @@ -292,6 +296,8 @@ testing.expectEqual(true, r.no_plugins); testing.expectEqual(true, r.no_register_protocol_handler); testing.expectEqual(true, r.no_model_context); + testing.expectEqual(true, r.no_send_beacon); + testing.expectEqual(true, r.no_cookie_enabled); }); } diff --git a/src/browser/webapi/Navigator.zig b/src/browser/webapi/Navigator.zig index 8d53de7a7..68c56f631 100644 --- a/src/browser/webapi/Navigator.zig +++ b/src/browser/webapi/Navigator.zig @@ -265,17 +265,15 @@ pub const JsApi = struct { pub const globalPrivacyControl = bridge.accessor(Navigator.getGlobalPrivacyControl, null, .{}); pub const javaEnabled = bridge.function(Navigator.javaEnabled, .{}); - pub const sendBeacon = bridge.function(Navigator.sendBeacon, .{ .exposed = .window, .noop = true }); + pub const sendBeacon = bridge.function(Navigator.sendBeacon, .{ .noop = true }); pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{}); pub const storage = bridge.accessor(Navigator.getStorage, null, .{}); pub const userAgentData = bridge.accessor(Navigator.getUserAgentData, null, .{}); - - // window only - pub const plugins = bridge.accessor(Navigator.getPlugins, null, .{ .exposed = .window }); - pub const geolocation = bridge.accessor(Navigator.getGeolocation, null, .{ .exposed = .window }); - pub const modelContext = bridge.accessor(Navigator.getModelContext, null, .{ .exposed = .window }); - pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{ .exposed = .window }); - pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{ .exposed = .window }); + pub const plugins = bridge.accessor(Navigator.getPlugins, null, .{}); + pub const geolocation = bridge.accessor(Navigator.getGeolocation, null, .{}); + pub const modelContext = bridge.accessor(Navigator.getModelContext, null, .{}); + pub const registerProtocolHandler = bridge.function(Navigator.registerProtocolHandler, .{}); + pub const unregisterProtocolHandler = bridge.function(Navigator.unregisterProtocolHandler, .{}); }; const testing = @import("../../testing.zig"); diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig index e65a68dc9..11b73053c 100644 --- a/src/browser/webapi/WorkerGlobalScope.zig +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -36,7 +36,7 @@ const ScriptManagerBase = @import("../ScriptManagerBase.zig"); const Event = @import("Event.zig"); const Crypto = @import("Crypto.zig"); const Console = @import("Console.zig"); -const Navigator = @import("Navigator.zig"); +const WorkerNavigator = @import("WorkerNavigator.zig"); const Timers = @import("Timers.zig"); const Scheduler = @import("Scheduler.zig"); const EventTarget = @import("EventTarget.zig"); @@ -105,7 +105,7 @@ _message_ports: std.DoublyLinkedList = .{}, _proto: *EventTarget, _console: Console = .init, _crypto: Crypto = .init, -_navigator: Navigator = .init, +_navigator: WorkerNavigator = .init, _performance: Performance, _idb_factory: ?*idb.IDBFactory = null, _on_error: ?JS.Function.Global = null, @@ -300,7 +300,7 @@ pub fn getCrypto(self: *WorkerGlobalScope) *Crypto { return &self._crypto; } -pub fn getNavigator(self: *WorkerGlobalScope) *Navigator { +pub fn getNavigator(self: *WorkerGlobalScope) *WorkerNavigator { return &self._navigator; } diff --git a/src/browser/webapi/WorkerNavigator.zig b/src/browser/webapi/WorkerNavigator.zig new file mode 100644 index 000000000..cced8ae70 --- /dev/null +++ b/src/browser/webapi/WorkerNavigator.zig @@ -0,0 +1,135 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +const js = @import("../js/js.zig"); +const Execution = js.Execution; + +const Navigator = @import("Navigator.zig"); +const Permissions = @import("Permissions.zig"); +const StorageManager = @import("StorageManager.zig"); +const NavigatorUAData = @import("NavigatorUAData.zig"); + +const WorkerNavigator = @This(); + +comptime { + // protect against identity_map conflict (make sure _pad: bool does its job) + for ([_][]const u8{ "_permissions", "_storage", "_ua_data" }) |name| { + if (@offsetOf(WorkerNavigator, name) == 0) { + @compileError(name ++ " aliases the WorkerNavigator"); + } + } +} + +_pad: bool = false, +_permissions: Permissions = .{}, +_storage: StorageManager = .{}, +_ua_data: NavigatorUAData = .{}, + +pub const init: WorkerNavigator = .{}; + +pub fn getUserAgent(_: *const WorkerNavigator, exec: *const Execution) []const u8 { + return Navigator.getUserAgent(&Navigator.init, exec); +} + +pub fn getLanguages(_: *const WorkerNavigator) [2][]const u8 { + return Navigator.getLanguages(&Navigator.init); +} + +pub fn getAppName(_: *const WorkerNavigator) []const u8 { + return Navigator.getAppName(&Navigator.init); +} + +pub fn getAppCodeName(_: *const WorkerNavigator) []const u8 { + return Navigator.getAppCodeName(&Navigator.init); +} + +pub fn getAppVersion(_: *const WorkerNavigator) []const u8 { + return Navigator.getAppVersion(&Navigator.init); +} + +pub fn getLanguage(_: *const WorkerNavigator) []const u8 { + return Navigator.getLanguage(&Navigator.init); +} + +pub fn getOnLine(_: *const WorkerNavigator) bool { + return Navigator.getOnLine(&Navigator.init); +} + +pub fn getHardwareConcurrency(_: *const WorkerNavigator) u32 { + return Navigator.getHardwareConcurrency(&Navigator.init); +} + +pub fn getDeviceMemory(_: *const WorkerNavigator) f64 { + return Navigator.getDeviceMemory(&Navigator.init); +} + +pub fn getVendor(_: *const WorkerNavigator) []const u8 { + return Navigator.getVendor(&Navigator.init); +} + +pub fn getProduct(_: *const WorkerNavigator) []const u8 { + return Navigator.getProduct(&Navigator.init); +} + +pub fn getGlobalPrivacyControl(_: *const WorkerNavigator) bool { + return Navigator.getGlobalPrivacyControl(&Navigator.init); +} + +pub fn getPlatform(_: *const WorkerNavigator) []const u8 { + return Navigator.getPlatform(&Navigator.init); +} + +pub fn getPermissions(self: *WorkerNavigator) *Permissions { + return &self._permissions; +} + +pub fn getStorage(self: *WorkerNavigator) *StorageManager { + return &self._storage; +} + +pub fn getUserAgentData(self: *WorkerNavigator) *NavigatorUAData { + return &self._ua_data; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(WorkerNavigator); + + pub const Meta = struct { + pub const name = "WorkerNavigator"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const userAgent = bridge.accessor(WorkerNavigator.getUserAgent, null, .{}); + pub const appName = bridge.accessor(WorkerNavigator.getAppName, null, .{}); + pub const appCodeName = bridge.accessor(WorkerNavigator.getAppCodeName, null, .{}); + pub const appVersion = bridge.accessor(WorkerNavigator.getAppVersion, null, .{}); + pub const platform = bridge.accessor(WorkerNavigator.getPlatform, null, .{}); + pub const language = bridge.accessor(WorkerNavigator.getLanguage, null, .{}); + pub const languages = bridge.accessor(WorkerNavigator.getLanguages, null, .{}); + pub const onLine = bridge.accessor(WorkerNavigator.getOnLine, null, .{}); + pub const hardwareConcurrency = bridge.accessor(WorkerNavigator.getHardwareConcurrency, null, .{}); + pub const deviceMemory = bridge.accessor(WorkerNavigator.getDeviceMemory, null, .{}); + pub const vendor = bridge.accessor(WorkerNavigator.getVendor, null, .{}); + pub const product = bridge.accessor(WorkerNavigator.getProduct, null, .{}); + pub const globalPrivacyControl = bridge.accessor(WorkerNavigator.getGlobalPrivacyControl, null, .{}); + + pub const permissions = bridge.accessor(WorkerNavigator.getPermissions, null, .{}); + pub const storage = bridge.accessor(WorkerNavigator.getStorage, null, .{}); + pub const userAgentData = bridge.accessor(WorkerNavigator.getUserAgentData, null, .{}); +};