From df69f9e60c031e5ae9cb85da6558a67d03edd60d Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Fri, 30 Aug 2024 01:39:54 +0800 Subject: [PATCH] fix: [#1484] Throw error in FormData.append when value parameter type is incorrect (#1484) * fix: throw error when append parameter type incorrect * fix: lint * chore: [#1484] Some additional fixes * chore: [#1484] Some additional fixes --------- Co-authored-by: David Ortner --- packages/happy-dom/src/fetch/Request.ts | 2 +- packages/happy-dom/src/fetch/Response.ts | 2 +- .../src/fetch/multipart/MultipartReader.ts | 3 ++- packages/happy-dom/src/file/Blob.ts | 4 ++-- packages/happy-dom/src/file/File.ts | 8 +++++++ packages/happy-dom/src/form-data/FormData.ts | 21 +++++++++++++------ .../html-form-element/HTMLFormElement.ts | 3 +-- packages/happy-dom/test/fetch/Fetch.test.ts | 2 +- packages/happy-dom/test/fetch/Request.test.ts | 8 +++---- .../happy-dom/test/fetch/Response.test.ts | 13 +++++++----- .../happy-dom/test/fetch/SyncFetch.test.ts | 2 +- .../happy-dom/test/form-data/FormData.test.ts | 14 +++++++++++++ 12 files changed, 58 insertions(+), 24 deletions(-) diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index e8749930c..d56b40aca 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -363,7 +363,7 @@ export default class Request implements Request { if (contentType?.startsWith('application/x-www-form-urlencoded')) { const parameters = new URLSearchParams(await this.text()); - const formData = new FormData(); + const formData = new window.FormData(); for (const [key, value] of parameters) { formData.append(key, value); diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index fa8021008..4cc2882ca 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -289,7 +289,7 @@ export default class Response implements Response { if (contentType?.startsWith('application/x-www-form-urlencoded')) { const parameters = new URLSearchParams(await this.text()); - const formData = new FormData(); + const formData = new window.FormData(); for (const [key, value] of parameters) { formData.append(key, value); diff --git a/packages/happy-dom/src/fetch/multipart/MultipartReader.ts b/packages/happy-dom/src/fetch/multipart/MultipartReader.ts index 671573eae..6e0f9a718 100644 --- a/packages/happy-dom/src/fetch/multipart/MultipartReader.ts +++ b/packages/happy-dom/src/fetch/multipart/MultipartReader.ts @@ -22,7 +22,7 @@ const CHARACTER_CODE = { * https://github.com/node-fetch/node-fetch/blob/main/src/utils/multipart-parser.js (MIT) */ export default class MultipartReader { - private formData = new FormData(); + private formData: FormData; private boundary: Uint8Array; private boundaryIndex = 0; private state = MultiparParserStateEnum.boundary; @@ -50,6 +50,7 @@ export default class MultipartReader { const boundaryHeader = `--${boundary}`; this.window = window; this.boundary = new Uint8Array(boundaryHeader.length); + this.formData = new window.FormData(); for (let i = 0, max = boundaryHeader.length; i < max; i++) { this.boundary[i] = boundaryHeader.charCodeAt(i); diff --git a/packages/happy-dom/src/file/Blob.ts b/packages/happy-dom/src/file/Blob.ts index 2c85da1df..c694d8f65 100644 --- a/packages/happy-dom/src/file/Blob.ts +++ b/packages/happy-dom/src/file/Blob.ts @@ -15,12 +15,12 @@ export default class Blob { /** * Constructor. * - * @param bits Bits. + * @param [bits] Bits. * @param [options] Options. * @param [options.type] MIME type. */ constructor( - bits: (ArrayBuffer | ArrayBufferView | Blob | Buffer | string)[], + bits?: (ArrayBuffer | ArrayBufferView | Blob | Buffer | string)[], options?: { type?: string } ) { const buffers = []; diff --git a/packages/happy-dom/src/file/File.ts b/packages/happy-dom/src/file/File.ts index aedfff9f8..4164590de 100644 --- a/packages/happy-dom/src/file/File.ts +++ b/packages/happy-dom/src/file/File.ts @@ -27,6 +27,14 @@ export default class File extends Blob { name: string, options?: { type?: string; lastModified?: number } ) { + if (arguments.length < 2) { + throw new TypeError( + "Failed to construct 'File': 2 arguments required, but only " + + arguments.length + + ' present.' + ); + } + super(bits, options); this.name = name.replace(/\//g, ':'); diff --git a/packages/happy-dom/src/form-data/FormData.ts b/packages/happy-dom/src/form-data/FormData.ts index 759691e20..22a289ef8 100644 --- a/packages/happy-dom/src/form-data/FormData.ts +++ b/packages/happy-dom/src/form-data/FormData.ts @@ -3,6 +3,7 @@ import * as PropertySymbol from '../PropertySymbol.js'; import File from '../file/File.js'; import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; +import BrowserWindow from '../window/BrowserWindow.js'; type FormDataEntry = { name: string; @@ -15,6 +16,9 @@ type FormDataEntry = { * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData */ export default class FormData implements Iterable<[string, string | File]> { + // Injected by WindowClassExtender + protected declare [PropertySymbol.window]: BrowserWindow; + #entries: FormDataEntry[] = []; /** @@ -96,6 +100,11 @@ export default class FormData implements Iterable<[string, string | File]> { * @param [filename] Filename. */ public append(name: string, value: string | Blob | File, filename?: string): void { + if (filename && !(value instanceof Blob)) { + throw new this[PropertySymbol.window].TypeError( + 'Failed to execute "append" on "FormData": parameter 2 is not of type "Blob".' + ); + } this.#entries.push({ name, value: this.#parseValue(value, filename) @@ -232,12 +241,6 @@ export default class FormData implements Iterable<[string, string | File]> { * @returns Parsed value. */ #parseValue(value: string | Blob | File, filename?: string): string | File { - if (value instanceof Blob && !(value instanceof File)) { - const file = new File([], 'blob', { type: value.type }); - file[PropertySymbol.buffer] = value[PropertySymbol.buffer]; - return file; - } - if (value instanceof File) { if (filename) { const file = new File([], filename, { type: value.type, lastModified: value.lastModified }); @@ -247,6 +250,12 @@ export default class FormData implements Iterable<[string, string | File]> { return value; } + if (value instanceof Blob) { + const file = new File([], 'blob', { type: value.type }); + file[PropertySymbol.buffer] = value[PropertySymbol.buffer]; + return file; + } + return String(value); } } diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index f48743b57..62af9b401 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -8,7 +8,6 @@ import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import HTMLButtonElement from '../html-button-element/HTMLButtonElement.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import BrowserFrameNavigator from '../../browser/utilities/BrowserFrameNavigator.js'; -import FormData from '../../form-data/FormData.js'; import BrowserWindow from '../../window/BrowserWindow.js'; import THTMLFormControlElement from './THTMLFormControlElement.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; @@ -584,7 +583,7 @@ export default class HTMLFormElement extends HTMLElement { } const method = submitter?.formMethod || this.method; - const formData = new FormData(this); + const formData = new this[PropertySymbol.window].FormData(this); let targetFrame: IBrowserFrame; switch (submitter?.formTarget || this.target) { diff --git a/packages/happy-dom/test/fetch/Fetch.test.ts b/packages/happy-dom/test/fetch/Fetch.test.ts index 5078fe75e..894bcb2e3 100644 --- a/packages/happy-dom/test/fetch/Fetch.test.ts +++ b/packages/happy-dom/test/fetch/Fetch.test.ts @@ -3285,7 +3285,7 @@ describe('Fetch', () => { it('Supports POST request with body as FormData.', async () => { const window = new Window({ url: 'https://localhost:8080/' }); - const formData = new FormData(); + const formData = new window.FormData(); vi.spyOn(Math, 'random').mockImplementation(() => 0.8); diff --git a/packages/happy-dom/test/fetch/Request.test.ts b/packages/happy-dom/test/fetch/Request.test.ts index d7c5e91f6..e33f182c5 100644 --- a/packages/happy-dom/test/fetch/Request.test.ts +++ b/packages/happy-dom/test/fetch/Request.test.ts @@ -670,7 +670,7 @@ describe('Request', () => { describe('formData()', () => { it('Returns FormData for FormData object (multipart)', async () => { - const formData = new FormData(); + const formData = new window.FormData(); formData.append('some', 'test'); const request = new window.Request(TEST_URL, { method: 'POST', body: formData }); const formDataResponse = await request.formData(); @@ -710,7 +710,7 @@ describe('Request', () => { }); it('Returns FormData for multipart text fields.', async () => { - const formData = new FormData(); + const formData = new window.FormData(); vi.spyOn(Math, 'random').mockImplementation(() => 0.8); @@ -733,7 +733,7 @@ describe('Request', () => { }); it('Returns FormData for multipart files.', async () => { - const formData = new FormData(); + const formData = new window.FormData(); const imageBuffer = await FS.promises.readFile( Path.join(__dirname, 'data', 'test-image.jpg') ); @@ -773,7 +773,7 @@ describe('Request', () => { it('Supports window.happyDOM?.waitUntilComplete().', async () => { await new Promise((resolve) => { - const formData = new FormData(); + const formData = new window.FormData(); formData.append('some', 'test'); const request = new window.Request(TEST_URL, { method: 'POST', body: formData }); let isAsyncComplete = false; diff --git a/packages/happy-dom/test/fetch/Response.test.ts b/packages/happy-dom/test/fetch/Response.test.ts index 9c8e523a3..5584c325a 100644 --- a/packages/happy-dom/test/fetch/Response.test.ts +++ b/packages/happy-dom/test/fetch/Response.test.ts @@ -322,7 +322,7 @@ describe('Response', () => { describe('formData()', () => { it('Returns FormData for FormData object (multipart)', async () => { - const formData = new FormData(); + const formData = new window.FormData(); formData.append('some', 'test'); const response = new window.Response(formData); const formDataResponse = await response.formData(); @@ -362,7 +362,7 @@ describe('Response', () => { }); it('Returns FormData for multipart text fields.', async () => { - const formData = new FormData(); + const formData = new window.FormData(); vi.spyOn(Math, 'random').mockImplementation(() => 0.8); @@ -385,7 +385,7 @@ describe('Response', () => { }); it('Returns FormData for multipart files.', async () => { - const formData = new FormData(); + const formData = new window.FormData(); const imageBuffer = await FS.promises.readFile( Path.join(__dirname, 'data', 'test-image.jpg') ); @@ -461,13 +461,16 @@ describe('Response', () => { it('Supports window.happyDOM?.waitUntilComplete() for multipart content.', async () => { await new Promise((resolve) => { - const response = new window.Response(new FormData()); + const response = new window.Response(new window.FormData()); let isAsyncComplete = false; vi.spyOn(MultipartFormDataParser, 'streamToFormData').mockImplementation( (): Promise<{ formData: FormData; buffer: Buffer }> => new Promise((resolve) => - setTimeout(() => resolve({ formData: new FormData(), buffer: Buffer.from([]) }), 10) + setTimeout( + () => resolve({ formData: new window.FormData(), buffer: Buffer.from([]) }), + 10 + ) ) ); diff --git a/packages/happy-dom/test/fetch/SyncFetch.test.ts b/packages/happy-dom/test/fetch/SyncFetch.test.ts index 03cdf7cc7..aeffd3ddd 100644 --- a/packages/happy-dom/test/fetch/SyncFetch.test.ts +++ b/packages/happy-dom/test/fetch/SyncFetch.test.ts @@ -2029,7 +2029,7 @@ describe('SyncFetch', () => { const body = '------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n'; - const formData = new FormData(); + const formData = new window.FormData(); let requestArgs: string | null = null; vi.spyOn(Math, 'random').mockImplementation(() => 0.8); diff --git a/packages/happy-dom/test/form-data/FormData.test.ts b/packages/happy-dom/test/form-data/FormData.test.ts index b7ea7ddde..c41f83889 100644 --- a/packages/happy-dom/test/form-data/FormData.test.ts +++ b/packages/happy-dom/test/form-data/FormData.test.ts @@ -2,6 +2,7 @@ import Window from '../../src/window/Window.js'; import Document from '../../src/nodes/document/Document.js'; import File from '../../src/file/File.js'; import { beforeEach, describe, it, expect } from 'vitest'; +import Blob from '../../src/file/Blob.js'; describe('FormData', () => { let window: Window; @@ -140,11 +141,24 @@ describe('FormData', () => { describe('append()', () => { it('Appends a value.', () => { const formData = new window.FormData(); + const blob = new Blob(); + const file = new File([], 'filename'); formData.append('key1', 'value1'); formData.append('key1', 'value2'); + formData.append('key2', blob); + formData.append('key3', file); expect(formData.getAll('key1')).toEqual(['value1', 'value2']); + expect(formData.getAll('key2')).toEqual([new File([], 'blob')]); + expect(formData.getAll('key3')).toEqual([file]); + }); + + it('Throws an error if a filename is provided and the value is not an instance of a Blob.', () => { + const formData = new window.FormData(); + expect(() => formData.append('key1', 'value1', 'filename')).toThrow( + 'Failed to execute "append" on "FormData": parameter 2 is not of type "Blob".' + ); }); });