Files
browser/src/xhr/xhr.zig
2024-02-15 16:42:54 +01:00

338 lines
11 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const Callback = jsruntime.Callback;
const DOMError = @import("../netsurf.zig").DOMError;
const Loop = jsruntime.Loop;
const YieldImpl = Loop.Yield(XMLHttpRequest);
const Client = @import("../async/Client.zig");
// XHR interfaces
// https://xhr.spec.whatwg.org/#interface-xmlhttprequest
pub const Interfaces = generate.Tuple(.{
XMLHttpRequestEventTarget,
XMLHttpRequestUpload,
XMLHttpRequest,
});
pub const XMLHttpRequestEventTarget = struct {
pub const prototype = *EventTarget;
pub const mem_guarantied = true;
onloadstart_cbk: ?Callback = null,
onprogress_cbk: ?Callback = null,
onabort_cbk: ?Callback = null,
onload_cbk: ?Callback = null,
ontimeout_cbk: ?Callback = null,
onloadend_cbk: ?Callback = null,
pub fn constructor() !XMLHttpRequestEventTarget {
return .{};
}
pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback) void {
self.onloadstart_cbk = handler;
}
pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback) void {
self.onprogress_cbk = handler;
}
pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback) void {
self.onabort_cbk = handler;
}
// TODO remove-me, test func du to an issue w/ the setter.
// see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989
pub fn _setOnload(self: *XMLHttpRequestEventTarget, handler: Callback) void {
self.set_onload(handler);
}
pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback) void {
self.onload_cbk = handler;
}
pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback) void {
self.ontimeout_cbk = handler;
}
pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback) void {
self.onloadend_cbk = handler;
}
pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void {
if (self.onloadstart_cbk) |cbk| cbk.deinit(alloc);
if (self.onprogress_cbk) |cbk| cbk.deinit(alloc);
if (self.onabort_cbk) |cbk| cbk.deinit(alloc);
if (self.onload_cbk) |cbk| cbk.deinit(alloc);
if (self.ontimeout_cbk) |cbk| cbk.deinit(alloc);
if (self.onloadend_cbk) |cbk| cbk.deinit(alloc);
}
};
pub const XMLHttpRequestUpload = struct {
pub const prototype = *XMLHttpRequestEventTarget;
pub const mem_guarantied = true;
proto: XMLHttpRequestEventTarget,
};
pub const XMLHttpRequest = struct {
pub const prototype = *XMLHttpRequestEventTarget;
pub const mem_guarantied = true;
pub const UNSENT: u16 = 0;
pub const OPENED: u16 = 1;
pub const HEADERS_RECEIVED: u16 = 2;
pub const LOADING: u16 = 3;
pub const DONE: u16 = 4;
// https://xhr.spec.whatwg.org/#response-type
const ResponseType = enum {
Empty,
Text,
ArrayBuffer,
Blob,
Document,
JSON,
};
proto: XMLHttpRequestEventTarget,
cli: Client,
impl: YieldImpl,
method: std.http.Method,
state: u16,
url: ?[]const u8,
uri: std.Uri,
headers: std.http.Headers,
sync: bool = true,
err: ?anyerror = null,
upload: ?XMLHttpRequestUpload = null,
timeout: u32 = 0,
withCredentials: bool = false,
// TODO: response readonly attribute any response;
response_bytes: ?[]const u8 = null,
response_type: ResponseType = .Empty,
response_headers: std.http.Headers,
send_flag: bool = false,
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest {
return .{
.proto = try XMLHttpRequestEventTarget.constructor(),
.headers = .{ .allocator = alloc, .owned = true },
.response_headers = .{ .allocator = alloc, .owned = true },
.impl = YieldImpl.init(loop),
.method = undefined,
.url = null,
.uri = undefined,
.state = UNSENT,
// TODO retrieve the HTTP client globally to reuse existing connections.
.cli = .{ .allocator = alloc, .loop = loop },
};
}
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
self.proto.deinit(alloc);
self.headers.deinit();
self.response_headers.deinit();
if (self.url) |v| alloc.free(v);
if (self.response_bytes) |v| alloc.free(v);
if (self.response_headers) |v| alloc.free(v);
// TODO the client must be shared between requests.
self.cli.deinit();
}
pub fn get_readyState(self: *XMLHttpRequest) u16 {
return self.state;
}
pub fn get_timeout(self: *XMLHttpRequest) u32 {
return self.timeout;
}
pub fn set_timeout(self: *XMLHttpRequest, timeout: u32) !void {
// TODO If the current global object is a Window object and thiss
// synchronous flag is set, then throw an "InvalidAccessError"
// DOMException.
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-timeout
self.timeout = timeout;
}
pub fn get_withCredentials(self: *XMLHttpRequest) bool {
return self.withCredentials;
}
pub fn set_withCredentials(self: *XMLHttpRequest, withCredentials: bool) !void {
if (self.state != OPENED and self.state != UNSENT) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
self.withCredentials = withCredentials;
}
pub fn _open(
self: *XMLHttpRequest,
alloc: std.mem.Allocator,
method: []const u8,
url: []const u8,
asyn: ?bool,
username: ?[]const u8,
password: ?[]const u8,
) !void {
_ = username;
_ = password;
// TODO If thiss relevant global object is a Window object and its
// associated Document is not fully active, then throw an
// "InvalidStateError" DOMException.
self.method = try validMethod(method);
self.url = try alloc.dupe(u8, url);
self.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax;
self.sync = if (asyn) |b| !b else false;
self.send_flag = false;
// TODO should we clearRetainingCapacity instead?
self.headers.clearAndFree();
self.response_headers.clearAndFree();
self.response_type = .Empty;
if (self.response_bytes) |v| alloc.free(v);
self.state = OPENED;
}
const methods = [_]struct {
tag: std.http.Method,
name: []const u8,
}{
.{ .tag = .DELETE, .name = "DELETE" },
.{ .tag = .GET, .name = "GET" },
.{ .tag = .HEAD, .name = "HEAD" },
.{ .tag = .OPTIONS, .name = "OPTIONS" },
.{ .tag = .POST, .name = "POST" },
.{ .tag = .PUT, .name = "PUT" },
};
const methods_forbidden = [_][]const u8{ "CONNECT", "TRACE", "TRACK" };
pub fn validMethod(m: []const u8) DOMError!std.http.Method {
for (methods) |method| {
if (std.ascii.eqlIgnoreCase(method.name, m)) {
return method.tag;
}
}
// If method is a forbidden method, then throw a "SecurityError" DOMException.
for (methods_forbidden) |method| {
if (std.ascii.eqlIgnoreCase(method, m)) {
return DOMError.Security;
}
}
// If method is not a method, then throw a "SyntaxError" DOMException.
return DOMError.Syntax;
}
pub fn _setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8) !void {
if (self.state != OPENED) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
return try self.headers.append(name, value);
}
// TODO body can be either a string or a document
pub fn _send(self: *XMLHttpRequest, body: ?[]const u8) !void {
if (self.state != OPENED) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
// The body argument provides the request body, if any, and is ignored
// if the request method is GET or HEAD.
// https://xhr.spec.whatwg.org/#the-send()-method
_ = body;
// TODO set Content-Type header according to the given body.
self.send_flag = true;
self.impl.yield(self);
}
pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void {
if (err) |e| return self.onerr(e);
var req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onerr(e);
defer req.deinit();
req.send(.{}) catch |e| return self.onerr(e);
req.finish() catch |e| return self.onerr(e);
req.wait() catch |e| return self.onerr(e);
self.response_headers = req.response.headers.clone(self.response_headers.allocator) catch |e| return self.onerr(e);
self.state = HEADERS_RECEIVED;
self.state = LOADING;
self.state = DONE;
// TODO use events instead
if (self.proto.onload_cbk) |cbk| {
// TODO pass an EventProgress
cbk.call(null) catch |e| {
std.debug.print("--- CALLBACK ERROR: {any}\n", .{e});
}; // TODO handle error
}
}
fn onerr(self: *XMLHttpRequest, err: anyerror) void {
self.err = err;
self.state = DONE;
}
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {
if (self.state != LOADING and self.state != DONE) return DOMError.InvalidState;
if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState;
return if (self.response_bytes) |v| v else "";
}
// the caller owns the string.
pub fn _getAllResponseHeaders(self: *XMLHttpRequest, alloc: std.mem.Allocator) ![]const u8 {
self.response_headers.sort();
var buf: std.ArrayListUnmanaged(u8) = .{};
const w = buf.writer(alloc);
for (self.response_headers.list.items) |entry| {
if (entry.value.len == 0) continue;
try w.writeAll(entry.name);
try w.writeAll(": ");
try w.writeAll(entry.value);
try w.writeAll("\r\n");
}
return buf.items;
}
};
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var send = [_]Case{
.{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" },
.{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" },
// TODO remove-me, test func du to an issue w/ the setter.
// see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989
.{ .src = "req.setOnload(cbk)", .ex = "undefined" },
// .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" },
.{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" },
.{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" },
.{ .src = "req.send(); nb", .ex = "0" },
// Each case executed waits for all loop callaback calls.
// So the url has been retrieved.
.{ .src = "nb", .ex = "1" },
.{ .src = "req.getAllResponseHeaders()", .ex = "undefined" },
};
try checkCases(js_env, &send);
}