forms: honor formaction / formmethod / formenctype on submit button

When a form is submitted via a click on a submit button (or
form.requestSubmit(button)), the HTML form-submission algorithm requires
the submitter's formaction / formmethod / formenctype attributes to
override the form's corresponding attributes when present. Frame.submitForm
was reading action / method / enctype only from the form element, so the
button-side overrides were silently ignored. The symmetrical lookup for
formtarget already exists upstream — this commit applies the same pattern
to the other three attributes.

Closes #2278
This commit is contained in:
Navid EMAD
2026-04-28 00:12:22 +02:00
parent ef3305a713
commit 46f1646cf0
2 changed files with 92 additions and 3 deletions

View File

@@ -3735,7 +3735,16 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
const arena = try self._session.getArena(.medium, "submitForm");
errdefer self._session.releaseArena(arena);
const enctype = form_element.getAttributeSafe(comptime .wrap("enctype"));
// Per HTML spec form-submission algorithm, when the submitter is a submit
// button, its formaction/formmethod/formenctype attributes override the
// form's corresponding attributes (matching how formtarget is honored above).
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-submit
const enctype = blk: {
if (submitter_) |s| {
if (s.getAttributeSafe(comptime .wrap("formenctype"))) |fe| break :blk fe;
}
break :blk form_element.getAttributeSafe(comptime .wrap("enctype"));
};
// Get charset from accept-charset attribute or fall back to document charset
const charset: []const u8 = blk: {
@@ -3752,8 +3761,18 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
var buf = std.Io.Writer.Allocating.init(arena);
try form_data.write(.{ .enctype = enctype, .charset = charset, .allocator = arena }, &buf.writer);
const method = form_element.getAttributeSafe(comptime .wrap("method")) orelse "";
var action = form_element.getAttributeSafe(comptime .wrap("action")) orelse self.url;
const method = blk: {
if (submitter_) |s| {
if (s.getAttributeSafe(comptime .wrap("formmethod"))) |fm| break :blk fm;
}
break :blk form_element.getAttributeSafe(comptime .wrap("method")) orelse "";
};
var action = blk: {
if (submitter_) |s| {
if (s.getAttributeSafe(comptime .wrap("formaction"))) |fa| break :blk fa;
}
break :blk form_element.getAttributeSafe(comptime .wrap("action")) orelse self.url;
};
var opts = NavigateOpts{
.reason = .form,

View File

@@ -56,3 +56,73 @@
});
}
</script>
<!-- Per HTML spec, formaction on the submit button overrides the form's action.
https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-submit -->
<iframe name=frame4 id=f4></iframe>
<form target="frame4" action="support/should-not-load.html">
<input type=submit id=submit_formaction formaction="support/page.html">
</form>
<script id=formaction type=module>
{
const state = await testing.async();
$('#submit_formaction').click();
$('#f4').onload = () => {
state.resolve();
};
await state.done(() => {
testing.expectEqual('a-page\n', $('#f4').contentDocument.body.textContent);
});
}
</script>
<!-- Per HTML spec, formmethod on the submit button overrides the form's method.
A form with method=GET would append the FormData as ?x=probe to the action;
formmethod=POST should suppress that and send the values in the request body.
We verify the override by asserting the loaded iframe URL has no query string. -->
<iframe name=frame5 id=f5></iframe>
<form target="frame5" method="GET" action="support/page.html">
<input name="x" value="probe">
<input type=submit id=submit_formmethod formmethod="POST">
</form>
<script id=formmethod type=module>
{
const state = await testing.async();
$('#submit_formmethod').click();
$('#f5').onload = () => {
state.resolve();
};
await state.done(() => {
// formmethod=POST overrode form's GET, so the FormData entry was NOT
// appended to the action URL as a query string.
testing.expectEqual(false, $('#f5').contentDocument.URL.includes('?'));
testing.expectEqual('a-page\n', $('#f5').contentDocument.body.textContent);
});
}
</script>
<!-- Combined: formaction + formmethod on the same submit button. -->
<iframe name=frame6 id=f6></iframe>
<form target="frame6" method="GET" action="support/should-not-load.html">
<input name="x" value="probe">
<input type=submit id=submit_form_both formaction="support/page.html" formmethod="POST">
</form>
<script id=formaction_and_formmethod type=module>
{
const state = await testing.async();
$('#submit_form_both').click();
$('#f6').onload = () => {
state.resolve();
};
await state.done(() => {
testing.expectEqual(false, $('#f6').contentDocument.URL.includes('?'));
testing.expectEqual('a-page\n', $('#f6').contentDocument.body.textContent);
});
}
</script>