Skip to content

Commit 74b36d6

Browse files
committed
support form.submit()
Only supports application/x-www-form-urlencoded
1 parent 58215a4 commit 74b36d6

File tree

9 files changed

+231
-52
lines changed

9 files changed

+231
-52
lines changed

src/browser/html/document.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ pub const HTMLDocument = struct {
175175
}
176176

177177
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
178-
return page.navigateFromWebAPI(url);
178+
return page.navigateFromWebAPI(url, .{ .reason = .script });
179179
}
180180

181181
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {

src/browser/html/form.zig

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const std = @import("std");
1919
const Allocator = std.mem.Allocator;
2020

2121
const parser = @import("../netsurf.zig");
22+
const Page = @import("../page.zig").Page;
2223
const HTMLElement = @import("elements.zig").HTMLElement;
2324
const FormData = @import("../xhr/form_data.zig").FormData;
2425

@@ -27,6 +28,10 @@ pub const HTMLFormElement = struct {
2728
pub const prototype = *HTMLElement;
2829
pub const subtype = .node;
2930

31+
pub fn _submit(self: *parser.Form, page: *Page) !void {
32+
return page.submitForm(self, null);
33+
}
34+
3035
pub fn _requestSubmit(self: *parser.Form) !void {
3136
try parser.formElementSubmit(self);
3237
}
@@ -35,20 +40,3 @@ pub const HTMLFormElement = struct {
3540
try parser.formElementReset(self);
3641
}
3742
};
38-
39-
pub const Submission = struct {
40-
method: ?[]const u8,
41-
form_data: FormData,
42-
};
43-
44-
pub fn processSubmission(arena: Allocator, form: *parser.Form) !?Submission {
45-
const form_element: *parser.Element = @ptrCast(form);
46-
const method = try parser.elementGetAttribute(form_element, "method");
47-
48-
return .{
49-
.method = method,
50-
.form_data = try FormData.fromForm(arena, form),
51-
};
52-
}
53-
54-
// Check xhr/form_data.zig for tests

src/browser/html/location.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,15 @@ pub const Location = struct {
7070
}
7171

7272
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
73-
return page.navigateFromWebAPI(url);
73+
return page.navigateFromWebAPI(url, .{ .reason = .script });
7474
}
7575

7676
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
77-
return page.navigateFromWebAPI(url);
77+
return page.navigateFromWebAPI(url, .{ .reason = .script });
7878
}
7979

8080
pub fn _reload(_: *const Location, page: *Page) !void {
81-
return page.navigateFromWebAPI(page.url.raw);
81+
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
8282
}
8383

8484
pub fn _toString(self: *Location, page: *Page) ![]const u8 {

src/browser/html/window.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub const Window = struct {
101101
}
102102

103103
pub fn set_location(_: *const Window, url: []const u8, page: *Page) !void {
104-
return page.navigateFromWebAPI(url);
104+
return page.navigateFromWebAPI(url, .{ .reason = .script });
105105
}
106106

107107
pub fn get_console(self: *Window) *Console {

src/browser/page.zig

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ pub const Page = struct {
199199
self.url = request_url;
200200

201201
// load the data
202-
var request = try self.newHTTPRequest(.GET, &self.url, .{ .navigation = true });
202+
var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true });
203203
defer request.deinit();
204+
request.body = opts.body;
204205
request.notification = notification;
205206

206207
notification.dispatch(.page_navigate, &.{
@@ -541,7 +542,7 @@ pub const Page = struct {
541542
.a => {
542543
const element: *parser.Element = @ptrCast(node);
543544
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
544-
try self.navigateFromWebAPI(href);
545+
try self.navigateFromWebAPI(href, .{});
545546
},
546547
else => {},
547548
}
@@ -550,10 +551,11 @@ pub const Page = struct {
550551
// As such we schedule the function to be called as soon as possible.
551552
// The page.arena is safe to use here, but the transfer_arena exists
552553
// specifically for this type of lifetime.
553-
pub fn navigateFromWebAPI(self: *Page, url: []const u8) !void {
554+
pub fn navigateFromWebAPI(self: *Page, url: []const u8, opts: NavigateOpts) !void {
554555
const arena = self.session.transfer_arena;
555556
const navi = try arena.create(DelayedNavigation);
556557
navi.* = .{
558+
.opts = opts,
557559
.session = self.session,
558560
.url = try arena.dupe(u8, url),
559561
};
@@ -578,17 +580,45 @@ pub const Page = struct {
578580
}
579581
return null;
580582
}
583+
584+
pub fn submitForm(self: *Page, form: *parser.Form, submitter: ?*parser.ElementHTML) !void {
585+
const FormData = @import("xhr/form_data.zig").FormData;
586+
587+
const transfer_arena = self.session.transfer_arena;
588+
var form_data = try FormData.fromForm(form, submitter, self);
589+
590+
const encoding = try parser.elementGetAttribute(@ptrCast(form), "enctype");
591+
592+
var buf: std.ArrayListUnmanaged(u8) = .empty;
593+
try form_data.write(encoding, buf.writer(transfer_arena));
594+
595+
const method = try parser.elementGetAttribute(@ptrCast(form), "method") orelse "";
596+
var action = try parser.elementGetAttribute(@ptrCast(form), "action") orelse self.url.raw;
597+
598+
var opts = NavigateOpts{
599+
.reason = .form,
600+
};
601+
if (std.ascii.eqlIgnoreCase(method, "post")) {
602+
opts.method = .POST;
603+
opts.body = buf.items;
604+
} else {
605+
action = try URL.concatQueryString(transfer_arena, action, buf.items);
606+
}
607+
608+
try self.navigateFromWebAPI(action, opts);
609+
}
581610
};
582611

583612
const DelayedNavigation = struct {
584613
url: []const u8,
585614
session: *Session,
615+
opts: NavigateOpts,
586616
navigate_node: Loop.CallbackNode = .{ .func = delayNavigate },
587617

588618
fn delayNavigate(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
589619
_ = repeat_delay;
590620
const self: *DelayedNavigation = @fieldParentPtr("navigate_node", node);
591-
self.session.pageNavigate(self.url) catch |err| {
621+
self.session.pageNavigate(self.url, self.opts) catch |err| {
592622
log.err(.page, "delayed navigation error", .{ .err = err, .url = self.url });
593623
};
594624
}
@@ -697,11 +727,15 @@ const Script = struct {
697727
pub const NavigateReason = enum {
698728
anchor,
699729
address_bar,
730+
form,
731+
script,
700732
};
701733

702734
pub const NavigateOpts = struct {
703735
cdp_id: ?i64 = null,
704736
reason: NavigateReason = .address_bar,
737+
method: http.Request.Method = .GET,
738+
body: ?[]const u8 = null,
705739
};
706740

707741
fn timestamp() u32 {

src/browser/session.zig

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const Allocator = std.mem.Allocator;
2323
const Env = @import("env.zig").Env;
2424
const Page = @import("page.zig").Page;
2525
const Browser = @import("browser.zig").Browser;
26+
const NavigateOpts = @import("page.zig").NavigateOpts;
2627

2728
const log = @import("../log.zig");
2829
const parser = @import("netsurf.zig");
@@ -122,7 +123,7 @@ pub const Session = struct {
122123
return &(self.page orelse return null);
123124
}
124125

125-
pub fn pageNavigate(self: *Session, url_string: []const u8) !void {
126+
pub fn pageNavigate(self: *Session, url_string: []const u8, opts: NavigateOpts) !void {
126127
// currently, this is only called from the page, so let's hope
127128
// it isn't null!
128129
std.debug.assert(self.page != null);
@@ -136,8 +137,6 @@ pub const Session = struct {
136137

137138
self.removePage();
138139
var page = try self.createPage();
139-
return page.navigate(url, .{
140-
.reason = .anchor,
141-
});
140+
return page.navigate(url, opts);
142141
}
143142
};

src/browser/xhr/form_data.zig

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,15 @@ pub const Interfaces = .{
5151

5252
// https://xhr.spec.whatwg.org/#interface-formdata
5353
pub const FormData = struct {
54-
entries: std.ArrayListUnmanaged(Entry),
54+
entries: Entry.List,
5555

5656
pub fn constructor(form_: ?*parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !FormData {
5757
const form = form_ orelse return .{ .entries = .empty };
58-
return fromForm(form, submitter_, page, .{});
58+
return fromForm(form, submitter_, page);
5959
}
6060

61-
const FromFormOpts = struct {
62-
// Uses the page.arena if null. This is needed for when we're handling
63-
// form submission from the Page, and we want to capture the form within
64-
// the session's transfer_arena.
65-
allocator: ?Allocator = null,
66-
};
67-
pub fn fromForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page, opts: FromFormOpts) !FormData {
68-
const entries = try collectForm(opts.allocator orelse page.arena, form, submitter_, page);
61+
pub fn fromForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !FormData {
62+
const entries = try collectForm(form, submitter_, page);
6963
return .{ .entries = entries };
7064
}
7165

@@ -144,11 +138,78 @@ pub const FormData = struct {
144138
}
145139
return null;
146140
}
141+
142+
pub fn write(self: *const FormData, encoding_: ?[]const u8, writer: anytype) !void {
143+
const encoding = encoding_ orelse {
144+
return urlEncode(self, writer);
145+
};
146+
147+
if (std.ascii.eqlIgnoreCase(encoding, "application/x-www-form-urlencoded")) {
148+
return urlEncode(self, writer);
149+
}
150+
151+
log.warn(.form_data, "encoding not supported", .{ .encoding = encoding });
152+
return error.EncodingNotSupported;
153+
}
147154
};
148155

156+
fn urlEncode(data: *const FormData, writer: anytype) !void {
157+
const entries = data.entries.items;
158+
if (entries.len == 0) {
159+
return;
160+
}
161+
162+
try urlEncodeEntry(entries[0], writer);
163+
for (entries[1..]) |entry| {
164+
try writer.writeByte('&');
165+
try urlEncodeEntry(entry, writer);
166+
}
167+
}
168+
169+
fn urlEncodeEntry(entry: Entry, writer: anytype) !void {
170+
try urlEncodeValue(entry.key, writer);
171+
try writer.writeByte('=');
172+
try urlEncodeValue(entry.value, writer);
173+
}
174+
175+
fn urlEncodeValue(value: []const u8, writer: anytype) !void {
176+
if (!urlEncodeShouldEscape(value)) {
177+
return writer.writeAll(value);
178+
}
179+
180+
for (value) |b| {
181+
if (urlEncodeUnreserved(b)) {
182+
try writer.writeByte(b);
183+
} else if (b == ' ') {
184+
// for form submission, space should be encoded as '+', not '%20'
185+
try writer.writeByte('+');
186+
} else {
187+
try writer.print("%{X:0>2}", .{b});
188+
}
189+
}
190+
}
191+
192+
fn urlEncodeShouldEscape(value: []const u8) bool {
193+
for (value) |b| {
194+
if (!urlEncodeUnreserved(b)) {
195+
return true;
196+
}
197+
}
198+
return false;
199+
}
200+
201+
fn urlEncodeUnreserved(b: u8) bool {
202+
return switch (b) {
203+
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
204+
else => false,
205+
};
206+
}
207+
149208
const Entry = struct {
150209
key: []const u8,
151210
value: []const u8,
211+
212+
pub const List = std.ArrayListUnmanaged(Entry);
152213
};
153214

154215
const KeyIterable = iterator.Iterable(KeyIterator, "FormDataKeyIterator");
@@ -157,7 +218,7 @@ const EntryIterable = iterator.Iterable(EntryIterator, "FormDataEntryIterator");
157218

158219
const KeyIterator = struct {
159220
index: usize = 0,
160-
entries: *const std.ArrayListUnmanaged(Entry),
221+
entries: *const Entry.List,
161222

162223
pub fn _next(self: *KeyIterator) ?[]const u8 {
163224
const index = self.index;
@@ -171,7 +232,7 @@ const KeyIterator = struct {
171232

172233
const ValueIterator = struct {
173234
index: usize = 0,
174-
entries: *const std.ArrayListUnmanaged(Entry),
235+
entries: *const Entry.List,
175236

176237
pub fn _next(self: *ValueIterator) ?[]const u8 {
177238
const index = self.index;
@@ -185,7 +246,7 @@ const ValueIterator = struct {
185246

186247
const EntryIterator = struct {
187248
index: usize = 0,
188-
entries: *const std.ArrayListUnmanaged(Entry),
249+
entries: *const Entry.List,
189250

190251
pub fn _next(self: *EntryIterator) ?struct { []const u8, []const u8 } {
191252
const index = self.index;
@@ -198,11 +259,13 @@ const EntryIterator = struct {
198259
}
199260
};
200261

201-
fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !std.ArrayListUnmanaged(Entry) {
262+
// TODO: handle disabled fieldsets
263+
fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page) !Entry.List {
264+
const arena = page.arena;
202265
const collection = try parser.formGetCollection(form);
203266
const len = try parser.htmlCollectionGetLength(collection);
204267

205-
var entries: std.ArrayListUnmanaged(Entry) = .empty;
268+
var entries: Entry.List = .empty;
206269
try entries.ensureTotalCapacity(arena, len);
207270

208271
const submitter_name_ = try getSubmitterName(submitter_);
@@ -275,7 +338,7 @@ fn collectForm(arena: Allocator, form: *parser.Form, submitter_: ?*parser.Elemen
275338
return entries;
276339
}
277340

278-
fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *std.ArrayListUnmanaged(Entry), page: *Page) !void {
341+
fn collectSelectValues(arena: Allocator, select: *parser.Select, name: []const u8, entries: *Entry.List, page: *Page) !void {
279342
const HTMLSelectElement = @import("../html/select.zig").HTMLSelectElement;
280343

281344
// Go through the HTMLSelectElement because it has specific logic for handling
@@ -456,3 +519,34 @@ test "Browser.FormData" {
456519
},
457520
}, .{});
458521
}
522+
523+
test "Browser.FormData: urlEncode" {
524+
var arr: std.ArrayListUnmanaged(u8) = .empty;
525+
defer arr.deinit(testing.allocator);
526+
527+
{
528+
var fd = FormData{ .entries = .empty };
529+
try testing.expectError(error.EncodingNotSupported, fd.write("unknown", arr.writer(testing.allocator)));
530+
531+
try fd.write(null, arr.writer(testing.allocator));
532+
try testing.expectEqual("", arr.items);
533+
534+
try fd.write("application/x-www-form-urlencoded", arr.writer(testing.allocator));
535+
try testing.expectEqual("", arr.items);
536+
}
537+
538+
{
539+
var fd = FormData{ .entries = Entry.List.fromOwnedSlice(@constCast(&[_]Entry{
540+
.{ .key = "a", .value = "1" },
541+
.{ .key = "it's over", .value = "9000 !!!" },
542+
.{ .key = "emot", .value = "ok: ☺" },
543+
})) };
544+
const expected = "a=1&it%27s+over=9000+%21%21%21&emot=ok%3A+%E2%98%BA";
545+
try fd.write(null, arr.writer(testing.allocator));
546+
try testing.expectEqual(expected, arr.items);
547+
548+
arr.clearRetainingCapacity();
549+
try fd.write("application/x-www-form-urlencoded", arr.writer(testing.allocator));
550+
try testing.expectEqual(expected, arr.items);
551+
}
552+
}

0 commit comments

Comments
 (0)