mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge pull request #2218 from lightpanda-io/user_agent_data
Adds navigator.userAgentData
This commit is contained in:
@@ -389,7 +389,27 @@ pub const WaitUntil = enum {
|
||||
/// Must be initialized with an allocator that outlives all HTTP connections.
|
||||
pub const HttpHeaders = struct {
|
||||
const user_agent_base: [:0]const u8 = "Lightpanda/1.0";
|
||||
pub const sec_ch_ua: [:0]const u8 = "Sec-Ch-Ua: \"Lightpanda\";v=\"1\"";
|
||||
|
||||
const Brand = struct {
|
||||
brand: [:0]const u8,
|
||||
version: [:0]const u8,
|
||||
};
|
||||
|
||||
/// Source of truth for client-hints brand data. Both the Sec-Ch-Ua
|
||||
/// HTTP header and navigator.userAgentData.brands derive from this
|
||||
/// list, so the two sides cannot drift.
|
||||
pub const brands = [_]Brand{
|
||||
.{ .brand = "Lightpanda", .version = "1" },
|
||||
};
|
||||
|
||||
pub const sec_ch_ua: [:0]const u8 = blk: {
|
||||
var out: [:0]const u8 = "Sec-Ch-Ua:";
|
||||
for (brands, 0..) |b, i| {
|
||||
const sep = if (i == 0) " " else ", ";
|
||||
out = out ++ sep ++ "\"" ++ b.brand ++ "\";v=\"" ++ b.version ++ "\"";
|
||||
}
|
||||
break :blk out;
|
||||
};
|
||||
|
||||
user_agent: [:0]const u8, // User agent value (e.g. "Lightpanda/1.0")
|
||||
user_agent_header: [:0]const u8,
|
||||
|
||||
@@ -809,8 +809,9 @@ pub fn documentIsLoaded(self: *Frame) void {
|
||||
|
||||
self._load_state = .load;
|
||||
self.document._ready_state = .interactive;
|
||||
self._documentIsLoaded() catch |err| {
|
||||
log.err(.frame, "document is loaded", .{ .err = err, .type = self._type, .url = self.url });
|
||||
self._documentIsLoaded() catch |err| switch (err) {
|
||||
error.JsException => {}, // already logged
|
||||
else => log.err(.frame, "document is loaded2", .{ .err = err, .type = self._type, .url = self.url }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -882,8 +883,9 @@ pub fn documentIsComplete(self: *Frame) void {
|
||||
}
|
||||
|
||||
self._load_state = .complete;
|
||||
self._documentIsComplete() catch |err| {
|
||||
log.err(.frame, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
|
||||
self._documentIsComplete() catch |err| switch (err) {
|
||||
error.JsException => {}, // already logged
|
||||
else => log.err(.frame, "document is complete", .{ .err = err, .type = self._type, .url = self.url }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -858,6 +858,7 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/EventTarget.zig"),
|
||||
@import("../webapi/Location.zig"),
|
||||
@import("../webapi/Navigator.zig"),
|
||||
@import("../webapi/NavigatorUAData.zig"),
|
||||
@import("../webapi/net/FormData.zig"),
|
||||
@import("../webapi/net/Headers.zig"),
|
||||
@import("../webapi/net/Request.zig"),
|
||||
@@ -936,6 +937,7 @@ pub const WorkerJsApis = flattenTypes(&.{
|
||||
@import("../webapi/AbortSignal.zig"),
|
||||
@import("../webapi/AbortController.zig"),
|
||||
@import("../webapi/URL.zig"),
|
||||
@import("../webapi/canvas/OffscreenCanvas.zig"),
|
||||
// @import("../webapi/Performance.zig"),
|
||||
});
|
||||
|
||||
|
||||
@@ -65,6 +65,49 @@
|
||||
testing.expectEqual(8, navigator.deviceMemory);
|
||||
</script>
|
||||
|
||||
<script id=userAgentData>
|
||||
testing.expectEqual(true, navigator.userAgentData !== undefined);
|
||||
testing.expectEqual(false, navigator.userAgentData.mobile);
|
||||
testing.expectEqual(true, navigator.userAgentData.platform.length > 0);
|
||||
|
||||
const validUAPlatforms = ['macOS', 'Windows', 'Linux', 'FreeBSD', 'Unknown'];
|
||||
testing.expectEqual(true, validUAPlatforms.includes(navigator.userAgentData.platform));
|
||||
|
||||
testing.expectEqual(1, navigator.userAgentData.brands.length);
|
||||
testing.expectEqual({brand: 'Lightpanda', version: "1"}, navigator.userAgentData.brands[0]);
|
||||
|
||||
// Same instance returned on repeated access
|
||||
testing.expectEqual(true, navigator.userAgentData === navigator.userAgentData);
|
||||
|
||||
const json = navigator.userAgentData.toJSON();
|
||||
testing.expectEqual(false, json.mobile);
|
||||
testing.expectEqual(navigator.userAgentData.platform, json.platform);
|
||||
testing.expectEqual(true, Array.isArray(json.brands));
|
||||
testing.expectEqual('Lightpanda', json.brands[0].brand);
|
||||
</script>
|
||||
|
||||
<script id=userAgentData_highEntropy type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
const p = navigator.userAgentData.getHighEntropyValues(['architecture', 'platformVersion', 'fullVersionList']);
|
||||
testing.expectTrue(p instanceof Promise);
|
||||
const v = await p;
|
||||
state.resolve();
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual(false, v.mobile);
|
||||
testing.expectEqual(navigator.userAgentData.platform, v.platform);
|
||||
testing.expectEqual(true, Array.isArray(v.brands));
|
||||
testing.expectEqual(true, Array.isArray(v.fullVersionList));
|
||||
testing.expectEqual('Lightpanda', v.fullVersionList[0].brand);
|
||||
testing.expectEqual('1.0.0.0', v.uaFullVersion);
|
||||
testing.expectEqual(false, v.wow64);
|
||||
testing.expectEqual(true, Array.isArray(v.formFactor));
|
||||
testing.expectEqual('Desktop', v.formFactor[0]);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=getBattery type=module>
|
||||
{
|
||||
const state = await testing.async();
|
||||
|
||||
@@ -26,6 +26,7 @@ const Frame = @import("../Frame.zig");
|
||||
const PluginArray = @import("PluginArray.zig");
|
||||
const Permissions = @import("Permissions.zig");
|
||||
const StorageManager = @import("StorageManager.zig");
|
||||
const NavigatorUAData = @import("NavigatorUAData.zig");
|
||||
|
||||
const log = lp.log;
|
||||
|
||||
@@ -34,6 +35,7 @@ _pad: bool = false,
|
||||
_plugins: PluginArray = .{},
|
||||
_permissions: Permissions = .{},
|
||||
_storage: StorageManager = .{},
|
||||
_ua_data: NavigatorUAData = .{},
|
||||
|
||||
pub const init: Navigator = .{};
|
||||
|
||||
@@ -72,6 +74,10 @@ pub fn getStorage(self: *Navigator) *StorageManager {
|
||||
return &self._storage;
|
||||
}
|
||||
|
||||
pub fn getUserAgentData(self: *Navigator) *NavigatorUAData {
|
||||
return &self._ua_data;
|
||||
}
|
||||
|
||||
pub fn getBattery(_: *const Navigator, frame: *Frame) !js.Promise {
|
||||
log.info(.not_implemented, "navigator.getBattery", .{});
|
||||
return frame.js.local.?.rejectErrorPromise(.{ .dom_exception = .{ .err = error.NotSupported } });
|
||||
@@ -182,6 +188,7 @@ pub const JsApi = struct {
|
||||
pub const getBattery = bridge.function(Navigator.getBattery, .{});
|
||||
pub const permissions = bridge.accessor(Navigator.getPermissions, null, .{});
|
||||
pub const storage = bridge.accessor(Navigator.getStorage, null, .{});
|
||||
pub const userAgentData = bridge.accessor(Navigator.getUserAgentData, null, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
134
src/browser/webapi/NavigatorUAData.zig
Normal file
134
src/browser/webapi/NavigatorUAData.zig
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (C) 2023-2026 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 builtin = @import("builtin");
|
||||
|
||||
const Config = @import("../../Config.zig");
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const NavigatorUAData = @This();
|
||||
|
||||
_pad: bool = false,
|
||||
|
||||
const Brand = struct {
|
||||
brand: []const u8,
|
||||
version: []const u8,
|
||||
};
|
||||
|
||||
pub fn getBrands(_: *const NavigatorUAData) []const Brand {
|
||||
return brandList();
|
||||
}
|
||||
|
||||
pub fn getMobile(_: *const NavigatorUAData) bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getPlatform(_: *const NavigatorUAData) []const u8 {
|
||||
return uaPlatform();
|
||||
}
|
||||
|
||||
pub fn toJSON(_: *const NavigatorUAData) struct {
|
||||
brands: []const Brand,
|
||||
mobile: bool,
|
||||
platform: []const u8,
|
||||
} {
|
||||
return .{
|
||||
.mobile = false,
|
||||
.brands = brandList(),
|
||||
.platform = uaPlatform(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getHighEntropyValues(_: *const NavigatorUAData, hints: []const []const u8, frame: *Frame) !js.Promise {
|
||||
// This should always return `brands` + `mobile` + `platform` and then whatever
|
||||
// "hints" field is requested (assuming the browser has permission), but it's
|
||||
// also valid to just return everything.
|
||||
|
||||
_ = hints;
|
||||
|
||||
return frame.js.local.?.resolvePromise(.{
|
||||
.brands = brandList(),
|
||||
.mobile = false,
|
||||
.platform = uaPlatform(),
|
||||
.architecture = uaArchitecture(),
|
||||
.bitness = uaBitness(),
|
||||
.model = "",
|
||||
.platformVersion = "",
|
||||
.uaFullVersion = "1.0.0.0",
|
||||
.fullVersionList = brandList(),
|
||||
.wow64 = false,
|
||||
.formFactor = [_][]const u8{"Desktop"},
|
||||
});
|
||||
}
|
||||
|
||||
fn brandList() []const Brand {
|
||||
const out = comptime blk: {
|
||||
const src = &Config.HttpHeaders.brands;
|
||||
var arr: [src.len]Brand = undefined;
|
||||
for (src, 0..) |b, i| {
|
||||
arr[i] = .{ .brand = b.brand, .version = b.version };
|
||||
}
|
||||
const final = arr;
|
||||
break :blk final;
|
||||
};
|
||||
return &out;
|
||||
}
|
||||
|
||||
fn uaPlatform() []const u8 {
|
||||
return switch (builtin.os.tag) {
|
||||
.macos => "macOS",
|
||||
.windows => "Windows",
|
||||
.linux => "Linux",
|
||||
.freebsd => "FreeBSD",
|
||||
else => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
fn uaArchitecture() []const u8 {
|
||||
return switch (builtin.cpu.arch) {
|
||||
.x86, .x86_64 => "x86",
|
||||
.aarch64, .aarch64_be, .arm, .armeb => "arm",
|
||||
else => "",
|
||||
};
|
||||
}
|
||||
|
||||
fn uaBitness() []const u8 {
|
||||
return switch (builtin.cpu.arch) {
|
||||
.x86_64, .aarch64, .aarch64_be, .powerpc64, .powerpc64le, .riscv64 => "64",
|
||||
else => "32",
|
||||
};
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(NavigatorUAData);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "NavigatorUAData";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
pub const empty_with_no_proto = true;
|
||||
};
|
||||
|
||||
pub const brands = bridge.accessor(NavigatorUAData.getBrands, null, .{});
|
||||
pub const mobile = bridge.accessor(NavigatorUAData.getMobile, null, .{});
|
||||
pub const platform = bridge.accessor(NavigatorUAData.getPlatform, null, .{});
|
||||
pub const toJSON = bridge.function(NavigatorUAData.toJSON, .{});
|
||||
pub const getHighEntropyValues = bridge.function(NavigatorUAData.getHighEntropyValues, .{});
|
||||
};
|
||||
@@ -18,11 +18,12 @@
|
||||
|
||||
const std = @import("std");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Frame = @import("../../Frame.zig");
|
||||
|
||||
const Blob = @import("../Blob.zig");
|
||||
const OffscreenCanvasRenderingContext2D = @import("OffscreenCanvasRenderingContext2D.zig");
|
||||
|
||||
const Execution = js.Execution;
|
||||
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
|
||||
const OffscreenCanvas = @This();
|
||||
|
||||
@@ -37,8 +38,8 @@ const DrawingContext = union(enum) {
|
||||
@"2d": *OffscreenCanvasRenderingContext2D,
|
||||
};
|
||||
|
||||
pub fn constructor(width: u32, height: u32, frame: *Frame) !*OffscreenCanvas {
|
||||
return frame._factory.create(OffscreenCanvas{
|
||||
pub fn constructor(width: u32, height: u32, exec: *Execution) !*OffscreenCanvas {
|
||||
return exec._factory.create(OffscreenCanvas{
|
||||
._width = width,
|
||||
._height = height,
|
||||
});
|
||||
@@ -60,9 +61,9 @@ pub fn setHeight(self: *OffscreenCanvas, value: u32) void {
|
||||
self._height = value;
|
||||
}
|
||||
|
||||
pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, frame: *Frame) !?DrawingContext {
|
||||
pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, exec: *Execution) !?DrawingContext {
|
||||
if (std.mem.eql(u8, context_type, "2d")) {
|
||||
const ctx = try frame._factory.create(OffscreenCanvasRenderingContext2D{});
|
||||
const ctx = try exec._factory.create(OffscreenCanvasRenderingContext2D{});
|
||||
return .{ .@"2d" = ctx };
|
||||
}
|
||||
|
||||
@@ -71,9 +72,9 @@ pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, frame: *Frame)
|
||||
|
||||
/// Returns a Promise that resolves to a Blob containing the image.
|
||||
/// Since we have no actual rendering, this returns an empty blob.
|
||||
pub fn convertToBlob(_: *OffscreenCanvas, frame: *Frame) !js.Promise {
|
||||
const blob = try Blob.init(null, null, frame._page);
|
||||
return frame.js.local.?.resolvePromise(blob);
|
||||
pub fn convertToBlob(_: *OffscreenCanvas, exec: *Execution) !js.Promise {
|
||||
const blob = try Blob.init(null, null, exec.context.page);
|
||||
return exec.context.local.?.resolvePromise(blob);
|
||||
}
|
||||
|
||||
/// Returns an ImageBitmap with the rendered content (stub).
|
||||
|
||||
@@ -27,6 +27,8 @@ const CanvasRenderingContext2D = @import("../../canvas/CanvasRenderingContext2D.
|
||||
const WebGLRenderingContext = @import("../../canvas/WebGLRenderingContext.zig");
|
||||
const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig");
|
||||
|
||||
const Execution = js.Execution;
|
||||
|
||||
const Canvas = @This();
|
||||
_proto: *HtmlElement,
|
||||
_cached: ?DrawingContext = null,
|
||||
@@ -97,10 +99,10 @@ pub fn getContext(self: *Canvas, context_type: []const u8, frame: *Frame) !?Draw
|
||||
|
||||
/// Transfers control of the canvas to an OffscreenCanvas.
|
||||
/// Returns an OffscreenCanvas with the same dimensions.
|
||||
pub fn transferControlToOffscreen(self: *Canvas, frame: *Frame) !*OffscreenCanvas {
|
||||
pub fn transferControlToOffscreen(self: *Canvas, exec: *Execution) !*OffscreenCanvas {
|
||||
const width = self.getWidth();
|
||||
const height = self.getHeight();
|
||||
return OffscreenCanvas.constructor(width, height, frame);
|
||||
return OffscreenCanvas.constructor(width, height, exec);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
Reference in New Issue
Block a user