mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Merge branch 'main' into agent
This commit is contained in:
@@ -602,6 +602,9 @@ const Synthetic = struct {
|
||||
}
|
||||
|
||||
fn run(transfer: *Transfer, _: *anyopaque) void {
|
||||
// prevents a callback that triggers a navigation queue from killing
|
||||
// this transfer from under us.
|
||||
transfer.state = .completing;
|
||||
defer transfer.deinit();
|
||||
|
||||
const fulfilled = build(transfer) catch |err| {
|
||||
|
||||
@@ -941,6 +941,7 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/ModelContext.zig"),
|
||||
@import("../webapi/Navigator.zig"),
|
||||
@import("../webapi/NavigatorUAData.zig"),
|
||||
@import("../webapi/Notification.zig"),
|
||||
@import("../webapi/net/FormData.zig"),
|
||||
@import("../webapi/net/Headers.zig"),
|
||||
@import("../webapi/net/Request.zig"),
|
||||
|
||||
@@ -158,3 +158,16 @@
|
||||
testing.expectEqual(null, element.getContext('webgl'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="CanvasRenderingContext2D#drawImage">
|
||||
{
|
||||
const ctx = document.createElement('canvas').getContext('2d');
|
||||
const img = new Image();
|
||||
const src = document.createElement('canvas');
|
||||
// No-op, but must accept the (image, dx, dy), (image, dx, dy, dw, dh) and
|
||||
// 9-arg overloads without throwing.
|
||||
ctx.drawImage(img, 0, 0);
|
||||
ctx.drawImage(src, 0, 0, 10, 10);
|
||||
testing.expectEqual(undefined, ctx.drawImage(src, 0, 0, 10, 10, 0, 0, 10, 10));
|
||||
}
|
||||
</script>
|
||||
|
||||
47
src/browser/tests/notification.html
Normal file
47
src/browser/tests/notification.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="./testing.js"></script>
|
||||
|
||||
<script id=Notification>
|
||||
{
|
||||
testing.expectEqual("function", typeof Notification);
|
||||
testing.expectEqual("default", Notification.permission);
|
||||
testing.expectEqual(2, Notification.maxActions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=Notification#requestPermission>
|
||||
{
|
||||
testing.async(async () => {
|
||||
testing.expectEqual("default", await Notification.requestPermission());
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=Notification#constructor>
|
||||
{
|
||||
const n = new Notification("hello", { body: "world", icon: "i.png", tag: "t", lang: "en", dir: "rtl", silent: true });
|
||||
testing.expectEqual(true, n instanceof Notification);
|
||||
testing.expectEqual(true, n instanceof EventTarget);
|
||||
testing.expectEqual("hello", n.title);
|
||||
testing.expectEqual("world", n.body);
|
||||
testing.expectEqual("i.png", n.icon);
|
||||
testing.expectEqual("t", n.tag);
|
||||
testing.expectEqual("en", n.lang);
|
||||
testing.expectEqual("rtl", n.dir);
|
||||
testing.expectEqual(true, n.silent);
|
||||
testing.expectEqual("[object Notification]", Object.prototype.toString.call(n));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=Notification#defaults>
|
||||
{
|
||||
const n = new Notification("only-title");
|
||||
testing.expectEqual("only-title", n.title);
|
||||
testing.expectEqual("", n.body);
|
||||
testing.expectEqual("auto", n.dir);
|
||||
testing.expectEqual(false, n.silent);
|
||||
testing.expectEqual(false, n.requireInteraction);
|
||||
// close() is a no-op but must exist and not throw.
|
||||
n.close();
|
||||
}
|
||||
</script>
|
||||
@@ -50,6 +50,7 @@ pub const Type = union(enum) {
|
||||
font_face_set: *@import("css/FontFaceSet.zig"),
|
||||
websocket: *@import("net/WebSocket.zig"),
|
||||
cookie_store: *@import("storage/CookieStore.zig"),
|
||||
notification: *@import("Notification.zig"),
|
||||
};
|
||||
|
||||
pub fn init(page: *Page) !*EventTarget {
|
||||
@@ -161,6 +162,7 @@ pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
|
||||
.font_face_set => writer.writeAll("<FontFaceSet>"),
|
||||
.websocket => writer.writeAll("<WebSocket>"),
|
||||
.cookie_store => writer.writeAll("<CookieStore>"),
|
||||
.notification => writer.writeAll("<Notification>"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -184,6 +186,7 @@ pub fn toString(self: *EventTarget) []const u8 {
|
||||
.font_face_set => return "[object FontFaceSet]",
|
||||
.websocket => return "[object WebSocket]",
|
||||
.cookie_store => return "[object CookieStore]",
|
||||
.notification => return "[object Notification]",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -241,7 +241,6 @@ pub fn setCookie(_: *HTMLDocument, cookie_str: []const u8, frame: *Frame) ![]con
|
||||
// Invalid cookies should be silently ignored, not throw errors
|
||||
return "";
|
||||
};
|
||||
errdefer c.deinit();
|
||||
if (c.http_only) {
|
||||
c.deinit();
|
||||
return ""; // HttpOnly cookies cannot be set from JS
|
||||
|
||||
175
src/browser/webapi/Notification.zig
Normal file
175
src/browser/webapi/Notification.zig
Normal file
@@ -0,0 +1,175 @@
|
||||
// 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 lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
|
||||
const Execution = js.Execution;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Notification = @This();
|
||||
|
||||
_rc: lp.RC(u8) = .{},
|
||||
_arena: Allocator,
|
||||
_proto: *EventTarget,
|
||||
_title: []const u8,
|
||||
_body: []const u8 = "",
|
||||
_icon: []const u8 = "",
|
||||
_image: []const u8 = "",
|
||||
_badge: []const u8 = "",
|
||||
_tag: []const u8 = "",
|
||||
_lang: []const u8 = "",
|
||||
_dir: []const u8 = "auto",
|
||||
_silent: bool = false,
|
||||
_require_interaction: bool = false,
|
||||
_renotify: bool = false,
|
||||
|
||||
const Options = struct {
|
||||
body: ?[]const u8 = null,
|
||||
icon: ?[]const u8 = null,
|
||||
image: ?[]const u8 = null,
|
||||
badge: ?[]const u8 = null,
|
||||
tag: ?[]const u8 = null,
|
||||
lang: ?[]const u8 = null,
|
||||
dir: ?[]const u8 = null,
|
||||
silent: ?bool = null,
|
||||
requireInteraction: ?bool = null,
|
||||
renotify: ?bool = null,
|
||||
};
|
||||
|
||||
pub fn init(title: []const u8, options_: ?Options, exec: *const Execution) !*Notification {
|
||||
const arena = try exec.getArena(.small, "Notification");
|
||||
errdefer exec.releaseArena(arena);
|
||||
|
||||
const options = options_ orelse Options{};
|
||||
return exec._factory.eventTargetWithAllocator(arena, Notification{
|
||||
._arena = arena,
|
||||
._proto = undefined,
|
||||
._title = try arena.dupe(u8, title),
|
||||
._body = if (options.body) |v| try arena.dupe(u8, v) else "",
|
||||
._icon = if (options.icon) |v| try arena.dupe(u8, v) else "",
|
||||
._image = if (options.image) |v| try arena.dupe(u8, v) else "",
|
||||
._badge = if (options.badge) |v| try arena.dupe(u8, v) else "",
|
||||
._tag = if (options.tag) |v| try arena.dupe(u8, v) else "",
|
||||
._lang = if (options.lang) |v| try arena.dupe(u8, v) else "",
|
||||
._dir = if (options.dir) |d| try arena.dupe(u8, d) else "auto",
|
||||
._silent = options.silent orelse false,
|
||||
._require_interaction = options.requireInteraction orelse false,
|
||||
._renotify = options.renotify orelse false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Notification, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *Notification, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *Notification) void {
|
||||
self._rc.acquire();
|
||||
}
|
||||
|
||||
pub fn close(_: *Notification) void {}
|
||||
|
||||
fn getPermission() []const u8 {
|
||||
return "default";
|
||||
}
|
||||
|
||||
fn getMaxActions() u32 {
|
||||
return 2;
|
||||
}
|
||||
|
||||
fn requestPermission(_: ?js.Function, exec: *const Execution) !js.Promise {
|
||||
return exec.js.local.?.resolvePromise("default");
|
||||
}
|
||||
|
||||
fn getTitle(self: *const Notification) []const u8 {
|
||||
return self._title;
|
||||
}
|
||||
fn getBody(self: *const Notification) []const u8 {
|
||||
return self._body;
|
||||
}
|
||||
fn getIcon(self: *const Notification) []const u8 {
|
||||
return self._icon;
|
||||
}
|
||||
fn getImage(self: *const Notification) []const u8 {
|
||||
return self._image;
|
||||
}
|
||||
fn getBadge(self: *const Notification) []const u8 {
|
||||
return self._badge;
|
||||
}
|
||||
fn getTag(self: *const Notification) []const u8 {
|
||||
return self._tag;
|
||||
}
|
||||
fn getLang(self: *const Notification) []const u8 {
|
||||
return self._lang;
|
||||
}
|
||||
fn getDir(self: *const Notification) []const u8 {
|
||||
return self._dir;
|
||||
}
|
||||
fn getSilent(self: *const Notification) bool {
|
||||
return self._silent;
|
||||
}
|
||||
fn getRequireInteraction(self: *const Notification) bool {
|
||||
return self._require_interaction;
|
||||
}
|
||||
fn getRenotify(self: *const Notification) bool {
|
||||
return self._renotify;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Notification);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "Notification";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(Notification.init, .{});
|
||||
|
||||
pub const permission = bridge.accessor(getPermission, null, .{ .static = true });
|
||||
pub const maxActions = bridge.accessor(getMaxActions, null, .{ .static = true });
|
||||
pub const requestPermission = bridge.function(Notification.requestPermission, .{ .static = true });
|
||||
|
||||
pub const close = bridge.function(Notification.close, .{ .noop = true });
|
||||
|
||||
pub const title = bridge.accessor(getTitle, null, .{});
|
||||
pub const body = bridge.accessor(getBody, null, .{});
|
||||
pub const icon = bridge.accessor(getIcon, null, .{});
|
||||
pub const image = bridge.accessor(getImage, null, .{});
|
||||
pub const badge = bridge.accessor(getBadge, null, .{});
|
||||
pub const tag = bridge.accessor(getTag, null, .{});
|
||||
pub const lang = bridge.accessor(getLang, null, .{});
|
||||
pub const dir = bridge.accessor(getDir, null, .{});
|
||||
pub const silent = bridge.accessor(getSilent, null, .{});
|
||||
pub const requireInteraction = bridge.accessor(getRequireInteraction, null, .{});
|
||||
pub const renotify = bridge.accessor(getRenotify, null, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: Notification" {
|
||||
try testing.htmlRunner("notification.html", .{});
|
||||
}
|
||||
@@ -83,6 +83,10 @@ pub fn createImageData(
|
||||
|
||||
pub fn putImageData(_: *const CanvasRenderingContext2D, _: *ImageData, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
|
||||
|
||||
// CanvasImageSource (HTMLImageElement, HTMLCanvasElement, ImageBitmap, ...) is
|
||||
// just taken as a js.Value for now since we don't use it, and that's much easier.
|
||||
pub fn drawImage(_: *const CanvasRenderingContext2D, _: js.Value, _: f64, _: f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64, _: ?f64) void {}
|
||||
|
||||
pub fn getImageData(
|
||||
_: *const CanvasRenderingContext2D,
|
||||
_: i32, // sx
|
||||
@@ -150,6 +154,7 @@ pub const JsApi = struct {
|
||||
pub const createImageData = bridge.function(CanvasRenderingContext2D.createImageData, .{ .dom_exception = true });
|
||||
|
||||
pub const putImageData = bridge.function(CanvasRenderingContext2D.putImageData, .{ .noop = true });
|
||||
pub const drawImage = bridge.function(CanvasRenderingContext2D.drawImage, .{ .noop = true });
|
||||
pub const getImageData = bridge.function(CanvasRenderingContext2D.getImageData, .{ .dom_exception = true });
|
||||
pub const save = bridge.function(CanvasRenderingContext2D.save, .{ .noop = true });
|
||||
pub const restore = bridge.function(CanvasRenderingContext2D.restore, .{ .noop = true });
|
||||
|
||||
@@ -481,10 +481,9 @@ pub const Jar = struct {
|
||||
/// Checks if addition comes from HTTP request or JS context.
|
||||
comptime is_http: bool,
|
||||
) !void {
|
||||
const is_expired = isCookieExpired(&cookie, request_time);
|
||||
defer if (is_expired) {
|
||||
cookie.deinit();
|
||||
};
|
||||
// `add` takes ownership of `cookie` unconditionally on entry.
|
||||
var stored = false;
|
||||
defer if (!stored) cookie.deinit();
|
||||
|
||||
if (self.cookies.items.len >= max_jar_size) {
|
||||
return error.CookieJarQuotaExceeded;
|
||||
@@ -493,6 +492,8 @@ pub const Jar = struct {
|
||||
return error.CookieSizeExceeded;
|
||||
}
|
||||
|
||||
const is_expired = isCookieExpired(&cookie, request_time);
|
||||
|
||||
for (self.cookies.items, 0..) |*c, i| {
|
||||
// We're only looking for the equal one.
|
||||
if (areCookiesEqual(&cookie, c) == false) {
|
||||
@@ -502,7 +503,6 @@ pub const Jar = struct {
|
||||
// RFC 6265bis 5.7.2: a non-HTTP API (e.g. document.cookie) must
|
||||
// not replace an HttpOnly cookie.
|
||||
if (c.http_only and is_http == false) {
|
||||
if (is_expired == false) cookie.deinit();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -519,6 +519,7 @@ pub const Jar = struct {
|
||||
// after the assignment, c points at the new cookie.
|
||||
c.deinit();
|
||||
self.cookies.items[i] = cookie;
|
||||
stored = true;
|
||||
self.dispatchChange(.changed, &self.cookies.items[i]);
|
||||
}
|
||||
return;
|
||||
@@ -526,6 +527,7 @@ pub const Jar = struct {
|
||||
|
||||
if (!is_expired) {
|
||||
try self.cookies.append(self.allocator, cookie);
|
||||
stored = true;
|
||||
self.dispatchChange(.changed, &self.cookies.items[self.cookies.items.len - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,34 +427,38 @@ fn storeCookie(exec: *const Execution, init: CookieInit) !void {
|
||||
if (!is_https) return error.InvalidPrefixedCookie;
|
||||
}
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(session.cookie_jar.allocator);
|
||||
errdefer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
// The errdefer only protects construction failures. Once we `break :blk`
|
||||
// with the Cookie value, `Jar.add` owns its lifetime.
|
||||
const cookie: Cookie = blk: {
|
||||
var arena = std.heap.ArenaAllocator.init(session.cookie_jar.allocator);
|
||||
errdefer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
|
||||
const owned_name = try aa.dupe(u8, init.name);
|
||||
const owned_value = try aa.dupe(u8, init.value);
|
||||
const owned_path = try Cookie.parsePath(aa, url, init.path);
|
||||
const owned_domain = try Cookie.parseDomain(aa, url, init.domain);
|
||||
const owned_name = try aa.dupe(u8, init.name);
|
||||
const owned_value = try aa.dupe(u8, init.value);
|
||||
const owned_path = try Cookie.parsePath(aa, url, init.path);
|
||||
const owned_domain = try Cookie.parseDomain(aa, url, init.domain);
|
||||
|
||||
const cookie: Cookie = .{
|
||||
.arena = arena,
|
||||
.name = owned_name,
|
||||
.value = owned_value,
|
||||
.path = owned_path,
|
||||
.domain = owned_domain,
|
||||
break :blk .{
|
||||
.arena = arena,
|
||||
.name = owned_name,
|
||||
.value = owned_value,
|
||||
.path = owned_path,
|
||||
.domain = owned_domain,
|
||||
|
||||
// CookieStore.expires is a unix timestamp in milliseconds; Cookie tracks
|
||||
// expiry in seconds. A timestamp at or before "now" deletes the cookie via
|
||||
// the Jar's expiry path.
|
||||
.expires = if (init.expires) |ms| ms / 1000.0 else null,
|
||||
// CookieStore.expires is a unix timestamp in milliseconds; Cookie tracks
|
||||
// expiry in seconds. A timestamp at or before "now" deletes the cookie via
|
||||
// the Jar's expiry path.
|
||||
.expires = if (init.expires) |ms| ms / 1000.0 else null,
|
||||
|
||||
.secure = secure,
|
||||
.http_only = false,
|
||||
.same_site = switch (init.sameSite) {
|
||||
.strict => .strict,
|
||||
.lax => .lax,
|
||||
.none => .none,
|
||||
},
|
||||
.secure = secure,
|
||||
.http_only = false,
|
||||
.same_site = switch (init.sameSite) {
|
||||
.strict => .strict,
|
||||
.lax => .lax,
|
||||
.none => .none,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// CookieStore is a script API, so is_http = false.
|
||||
|
||||
@@ -142,30 +142,34 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
|
||||
return error.NotImplemented;
|
||||
}
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
|
||||
errdefer arena.deinit();
|
||||
const a = arena.allocator();
|
||||
// The errdefer only protects construction failures. Once we `break :blk`
|
||||
// with the Cookie value, `Jar.add` owns its lifetime.
|
||||
const cookie = blk: {
|
||||
var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
|
||||
errdefer arena.deinit();
|
||||
const a = arena.allocator();
|
||||
|
||||
// NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.
|
||||
const domain = try Cookie.parseDomain(a, param.url, param.domain);
|
||||
const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
|
||||
// NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.
|
||||
const domain = try Cookie.parseDomain(a, param.url, param.domain);
|
||||
const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
|
||||
|
||||
const secure = if (param.secure) |s| s else if (param.url) |url| URL.isHTTPS(url) else false;
|
||||
const secure = if (param.secure) |s| s else if (param.url) |url| URL.isHTTPS(url) else false;
|
||||
|
||||
const cookie = Cookie{
|
||||
.arena = arena,
|
||||
.name = try a.dupe(u8, param.name),
|
||||
.value = try a.dupe(u8, param.value),
|
||||
.path = path,
|
||||
.domain = domain,
|
||||
.expires = param.expires,
|
||||
.secure = secure,
|
||||
.http_only = param.httpOnly,
|
||||
.same_site = switch (param.sameSite) {
|
||||
.Strict => .strict,
|
||||
.Lax => .lax,
|
||||
.None => .none,
|
||||
},
|
||||
break :blk Cookie{
|
||||
.arena = arena,
|
||||
.name = try a.dupe(u8, param.name),
|
||||
.value = try a.dupe(u8, param.value),
|
||||
.path = path,
|
||||
.domain = domain,
|
||||
.expires = param.expires,
|
||||
.secure = secure,
|
||||
.http_only = param.httpOnly,
|
||||
.same_site = switch (param.sameSite) {
|
||||
.Strict => .strict,
|
||||
.Lax => .lax,
|
||||
.None => .none,
|
||||
},
|
||||
};
|
||||
};
|
||||
try cookie_jar.add(cookie, std.time.timestamp(), true);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ fn _loadFromFile(session: *Session, path: []const u8) !void {
|
||||
};
|
||||
|
||||
jar.add(cookie, now, true) catch |err| {
|
||||
cookie.deinit();
|
||||
log.warn(.app, "invalid cookie", .{ .name = jc.name, .err = err });
|
||||
continue;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user