Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/validate-form-submitter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"superformdata": patch
---

Validate `encode(form, { submitter })` submitters before encoding so non-submit controls and controls owned by another form throw like native `FormData(form, submitter)`.
23 changes: 23 additions & 0 deletions src/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,27 @@ function isSubmitButton(el: Element): boolean {
return false;
}

function validateSubmitter(form: HTMLFormElement, submitter?: HTMLElement | null): void {
if (submitter == null) return;

if (!isSubmitButton(submitter)) {
throw new TypeError("The specified element is not a submit button");
}

if (submitter instanceof HTMLButtonElement || submitter instanceof HTMLInputElement) {
if (submitter.form === form) return;
}

if (typeof DOMException !== "undefined") {
throw new DOMException(
"The specified element is not owned by this form element",
"NotFoundError",
);
}

throw new Error("The specified element is not owned by this form element");
}

function isSubmittableFormControl(
element: Element,
): element is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement {
Expand Down Expand Up @@ -117,6 +138,8 @@ function encodeForm(
explicitTypes?: Record<string, string>,
submitter?: HTMLElement | null,
): EncodedEntry[] | PreservedFileEntry[] {
validateSubmitter(form, submitter);

const entries: PreservedFileEntry[] = [];
const types: Record<string, string> = { ...explicitTypes };

Expand Down
42 changes: 35 additions & 7 deletions test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,18 +453,44 @@ describe("encode(form)", () => {
const button = form.querySelector<HTMLButtonElement>("button")!;
const entries = encode(form, { submitter: button });

expect(entries).toContainEqual(["title", "hello"]);
expect(entries).toContainEqual(["action", "save"]);
expect(entries).toEqual([
["title", "hello"],
["action", "save"],
]);
});

test("always excludes button[type=button]", () => {
test("rejects button[type=button] passed as submitter", () => {
const form = createForm(
'<input name="title" value="hello" /><button name="action" type="button" value="click">Click</button>',
);
const button = form.querySelector<HTMLButtonElement>("button")!;
const entries = encode(form, { submitter: button });

expect(entries).toEqual([["title", "hello"]]);
expect(() => encode(form, { submitter: button })).toThrow(TypeError);
});

test("rejects non-submit input passed as submitter", () => {
const form = createForm(
'<input name="title" value="hello" /><input name="notSubmit" value="x" />',
);
const input = form.querySelector<HTMLInputElement>('input[name="notSubmit"]')!;

expect(() => encode(form, { submitter: input })).toThrow(TypeError);
});

test("rejects submit buttons not owned by the form", () => {
document.body.innerHTML =
'<form id="target"><input name="title" value="hello" /></form>' +
'<form id="other"><button name="action" value="save">Save</button></form>';
const form = document.querySelector<HTMLFormElement>("#target")!;
const button = document.querySelector<HTMLButtonElement>("#other button")!;

try {
encode(form, { submitter: button });
throw new Error("Expected encode to reject submitter owned by another form");
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect((error as DOMException).name).toBe("NotFoundError");
}
});

test("excludes input[type=submit] without submitter", () => {
Expand All @@ -483,8 +509,10 @@ describe("encode(form)", () => {
const submitInput = form.querySelector<HTMLInputElement>('input[type="submit"]')!;
const entries = encode(form, { submitter: submitInput });

expect(entries).toContainEqual(["title", "hello"]);
expect(entries).toContainEqual(["sub", "Send"]);
expect(entries).toEqual([
["title", "hello"],
["sub", "Send"],
]);
});

test("includes input[type=image] when passed as submitter (encodes name+value, not coordinates)", () => {
Expand Down
Loading