diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 49d82d07..23adbe99 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -4046,6 +4046,11 @@ const SubmitFormOpts = struct { pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.Form, submit_opts: SubmitFormOpts) !void { const form = form_ orelse return; + // see the `_constructing_entry_list` field documentation + if (form._constructing_entry_list) { + return; + } + if (submitter_) |submitter| { if (submitter.getAttributeSafe(comptime .wrap("disabled")) != null) { return; @@ -4083,6 +4088,14 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For }; if (submit_opts.fire_event) { + // Prevent a submit on the form from firing while we're submit the form. + // This is both spec-correct AND prevents infinite recursion. + if (form._firing_submission_events) { + return; + } + form._firing_submission_events = true; + defer form._firing_submission_events = false; + // Per HTML spec "submit a form element" algorithm: SubmitEvent.submitter // must be null when the submitter is the form itself, which is what // Form.requestSubmit() passes when called with no submitter argument. diff --git a/src/browser/tests/element/html/form.html b/src/browser/tests/element/html/form.html index 4eec5e2f..e3ed84a9 100644 --- a/src/browser/tests/element/html/form.html +++ b/src/browser/tests/element/html/form.html @@ -561,3 +561,65 @@ testing.expectEqual('big5', form.getAttribute('accept-charset')); } + + +
+ +
+ + + + + + + + diff --git a/src/browser/webapi/element/html/Form.zig b/src/browser/webapi/element/html/Form.zig index 3e3d3585..d43d719a 100644 --- a/src/browser/webapi/element/html/Form.zig +++ b/src/browser/webapi/element/html/Form.zig @@ -33,6 +33,14 @@ pub const TextArea = @import("TextArea.zig"); const Form = @This(); _proto: *HtmlElement, +// Prevents submission of the form while we're in the process of submitting +// the form. You can imagine an onsubmit = () => form.submit() endless loop. +_firing_submission_events: bool = false, + +// Prevents submission of the form while we're building the entry list for the +// form. You can imagine an formdata = () => form.submit() endless loop. +_constructing_entry_list: bool = false, + pub fn asHtmlElement(self: *Form) *HtmlElement { return self._proto; } diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig index 82436a24..12f0ecf6 100644 --- a/src/browser/webapi/net/FormData.zig +++ b/src/browser/webapi/net/FormData.zig @@ -73,6 +73,14 @@ pub fn init(form_: ?*Form, submitter: ?*Element, exec: *const Execution) !*FormD .worker => lp.assert(false, "FormData worker form", .{}), }; + if (form._constructing_entry_list) { + // see the `_constructing_entry_list` field documentation + return error.InvalidStateError; + } + + form._constructing_entry_list = true; + defer form._constructing_entry_list = false; + const form_data = try exec._factory.create(FormData{ ._arena = exec.arena, ._entries = try collectForm(frame.arena, form, submitter, frame), @@ -397,7 +405,7 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const constructor = bridge.constructor(FormData.init, .{}); + pub const constructor = bridge.constructor(FormData.init, .{ .dom_exception = true }); pub const has = bridge.function(FormData.has, .{}); pub const get = bridge.function(FormData.get, .{}); pub const set = bridge.function(FormData.set, .{});