Spaces:
Runtime error
Runtime error
const { isBlobLike, toUSVString, makeIterator } = require('./util') | |
const { kState } = require('./symbols') | |
const { File: UndiciFile, FileLike, isFileLike } = require('./file') | |
const { webidl } = require('./webidl') | |
const { Blob, File: NativeFile } = require('buffer') | |
/** @type {globalThis['File']} */ | |
const File = NativeFile ?? UndiciFile | |
// https://xhr.spec.whatwg.org/#formdata | |
class FormData { | |
constructor (form) { | |
if (form !== undefined) { | |
throw webidl.errors.conversionFailed({ | |
prefix: 'FormData constructor', | |
argument: 'Argument 1', | |
types: ['undefined'] | |
}) | |
} | |
this[kState] = [] | |
} | |
append (name, value, filename = undefined) { | |
webidl.brandCheck(this, FormData) | |
webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' }) | |
if (arguments.length === 3 && !isBlobLike(value)) { | |
throw new TypeError( | |
"Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" | |
) | |
} | |
// 1. Let value be value if given; otherwise blobValue. | |
name = webidl.converters.USVString(name) | |
value = isBlobLike(value) | |
? webidl.converters.Blob(value, { strict: false }) | |
: webidl.converters.USVString(value) | |
filename = arguments.length === 3 | |
? webidl.converters.USVString(filename) | |
: undefined | |
// 2. Let entry be the result of creating an entry with | |
// name, value, and filename if given. | |
const entry = makeEntry(name, value, filename) | |
// 3. Append entry to this’s entry list. | |
this[kState].push(entry) | |
} | |
delete (name) { | |
webidl.brandCheck(this, FormData) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' }) | |
name = webidl.converters.USVString(name) | |
// The delete(name) method steps are to remove all entries whose name | |
// is name from this’s entry list. | |
this[kState] = this[kState].filter(entry => entry.name !== name) | |
} | |
get (name) { | |
webidl.brandCheck(this, FormData) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' }) | |
name = webidl.converters.USVString(name) | |
// 1. If there is no entry whose name is name in this’s entry list, | |
// then return null. | |
const idx = this[kState].findIndex((entry) => entry.name === name) | |
if (idx === -1) { | |
return null | |
} | |
// 2. Return the value of the first entry whose name is name from | |
// this’s entry list. | |
return this[kState][idx].value | |
} | |
getAll (name) { | |
webidl.brandCheck(this, FormData) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' }) | |
name = webidl.converters.USVString(name) | |
// 1. If there is no entry whose name is name in this’s entry list, | |
// then return the empty list. | |
// 2. Return the values of all entries whose name is name, in order, | |
// from this’s entry list. | |
return this[kState] | |
.filter((entry) => entry.name === name) | |
.map((entry) => entry.value) | |
} | |
has (name) { | |
webidl.brandCheck(this, FormData) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' }) | |
name = webidl.converters.USVString(name) | |
// The has(name) method steps are to return true if there is an entry | |
// whose name is name in this’s entry list; otherwise false. | |
return this[kState].findIndex((entry) => entry.name === name) !== -1 | |
} | |
set (name, value, filename = undefined) { | |
webidl.brandCheck(this, FormData) | |
webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' }) | |
if (arguments.length === 3 && !isBlobLike(value)) { | |
throw new TypeError( | |
"Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" | |
) | |
} | |
// The set(name, value) and set(name, blobValue, filename) method steps | |
// are: | |
// 1. Let value be value if given; otherwise blobValue. | |
name = webidl.converters.USVString(name) | |
value = isBlobLike(value) | |
? webidl.converters.Blob(value, { strict: false }) | |
: webidl.converters.USVString(value) | |
filename = arguments.length === 3 | |
? toUSVString(filename) | |
: undefined | |
// 2. Let entry be the result of creating an entry with name, value, and | |
// filename if given. | |
const entry = makeEntry(name, value, filename) | |
// 3. If there are entries in this’s entry list whose name is name, then | |
// replace the first such entry with entry and remove the others. | |
const idx = this[kState].findIndex((entry) => entry.name === name) | |
if (idx !== -1) { | |
this[kState] = [ | |
...this[kState].slice(0, idx), | |
entry, | |
...this[kState].slice(idx + 1).filter((entry) => entry.name !== name) | |
] | |
} else { | |
// 4. Otherwise, append entry to this’s entry list. | |
this[kState].push(entry) | |
} | |
} | |
entries () { | |
webidl.brandCheck(this, FormData) | |
return makeIterator( | |
() => this[kState].map(pair => [pair.name, pair.value]), | |
'FormData', | |
'key+value' | |
) | |
} | |
keys () { | |
webidl.brandCheck(this, FormData) | |
return makeIterator( | |
() => this[kState].map(pair => [pair.name, pair.value]), | |
'FormData', | |
'key' | |
) | |
} | |
values () { | |
webidl.brandCheck(this, FormData) | |
return makeIterator( | |
() => this[kState].map(pair => [pair.name, pair.value]), | |
'FormData', | |
'value' | |
) | |
} | |
/** | |
* @param {(value: string, key: string, self: FormData) => void} callbackFn | |
* @param {unknown} thisArg | |
*/ | |
forEach (callbackFn, thisArg = globalThis) { | |
webidl.brandCheck(this, FormData) | |
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' }) | |
if (typeof callbackFn !== 'function') { | |
throw new TypeError( | |
"Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'." | |
) | |
} | |
for (const [key, value] of this) { | |
callbackFn.apply(thisArg, [value, key, this]) | |
} | |
} | |
} | |
FormData.prototype[Symbol.iterator] = FormData.prototype.entries | |
Object.defineProperties(FormData.prototype, { | |
[Symbol.toStringTag]: { | |
value: 'FormData', | |
configurable: true | |
} | |
}) | |
/** | |
* @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry | |
* @param {string} name | |
* @param {string|Blob} value | |
* @param {?string} filename | |
* @returns | |
*/ | |
function makeEntry (name, value, filename) { | |
// 1. Set name to the result of converting name into a scalar value string. | |
// "To convert a string into a scalar value string, replace any surrogates | |
// with U+FFFD." | |
// see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end | |
name = Buffer.from(name).toString('utf8') | |
// 2. If value is a string, then set value to the result of converting | |
// value into a scalar value string. | |
if (typeof value === 'string') { | |
value = Buffer.from(value).toString('utf8') | |
} else { | |
// 3. Otherwise: | |
// 1. If value is not a File object, then set value to a new File object, | |
// representing the same bytes, whose name attribute value is "blob" | |
if (!isFileLike(value)) { | |
value = value instanceof Blob | |
? new File([value], 'blob', { type: value.type }) | |
: new FileLike(value, 'blob', { type: value.type }) | |
} | |
// 2. If filename is given, then set value to a new File object, | |
// representing the same bytes, whose name attribute is filename. | |
if (filename !== undefined) { | |
/** @type {FilePropertyBag} */ | |
const options = { | |
type: value.type, | |
lastModified: value.lastModified | |
} | |
value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile | |
? new File([value], filename, options) | |
: new FileLike(value, filename, options) | |
} | |
} | |
// 4. Return an entry whose name is name and whose value is value. | |
return { name, value } | |
} | |
module.exports = { FormData } | |