1'use strict' 2 3const { isBlobLike, toUSVString, makeIterator } = require('./util') 4const { kState } = require('./symbols') 5const { File: UndiciFile, FileLike, isFileLike } = require('./file') 6const { webidl } = require('./webidl') 7const { Blob, File: NativeFile } = require('buffer') 8 9/** @type {globalThis['File']} */ 10const File = NativeFile ?? UndiciFile 11 12// https://xhr.spec.whatwg.org/#formdata 13class FormData { 14 constructor (form) { 15 if (form !== undefined) { 16 throw webidl.errors.conversionFailed({ 17 prefix: 'FormData constructor', 18 argument: 'Argument 1', 19 types: ['undefined'] 20 }) 21 } 22 23 this[kState] = [] 24 } 25 26 append (name, value, filename = undefined) { 27 webidl.brandCheck(this, FormData) 28 29 webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' }) 30 31 if (arguments.length === 3 && !isBlobLike(value)) { 32 throw new TypeError( 33 "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" 34 ) 35 } 36 37 // 1. Let value be value if given; otherwise blobValue. 38 39 name = webidl.converters.USVString(name) 40 value = isBlobLike(value) 41 ? webidl.converters.Blob(value, { strict: false }) 42 : webidl.converters.USVString(value) 43 filename = arguments.length === 3 44 ? webidl.converters.USVString(filename) 45 : undefined 46 47 // 2. Let entry be the result of creating an entry with 48 // name, value, and filename if given. 49 const entry = makeEntry(name, value, filename) 50 51 // 3. Append entry to this’s entry list. 52 this[kState].push(entry) 53 } 54 55 delete (name) { 56 webidl.brandCheck(this, FormData) 57 58 webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' }) 59 60 name = webidl.converters.USVString(name) 61 62 // The delete(name) method steps are to remove all entries whose name 63 // is name from this’s entry list. 64 this[kState] = this[kState].filter(entry => entry.name !== name) 65 } 66 67 get (name) { 68 webidl.brandCheck(this, FormData) 69 70 webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' }) 71 72 name = webidl.converters.USVString(name) 73 74 // 1. If there is no entry whose name is name in this’s entry list, 75 // then return null. 76 const idx = this[kState].findIndex((entry) => entry.name === name) 77 if (idx === -1) { 78 return null 79 } 80 81 // 2. Return the value of the first entry whose name is name from 82 // this’s entry list. 83 return this[kState][idx].value 84 } 85 86 getAll (name) { 87 webidl.brandCheck(this, FormData) 88 89 webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' }) 90 91 name = webidl.converters.USVString(name) 92 93 // 1. If there is no entry whose name is name in this’s entry list, 94 // then return the empty list. 95 // 2. Return the values of all entries whose name is name, in order, 96 // from this’s entry list. 97 return this[kState] 98 .filter((entry) => entry.name === name) 99 .map((entry) => entry.value) 100 } 101 102 has (name) { 103 webidl.brandCheck(this, FormData) 104 105 webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' }) 106 107 name = webidl.converters.USVString(name) 108 109 // The has(name) method steps are to return true if there is an entry 110 // whose name is name in this’s entry list; otherwise false. 111 return this[kState].findIndex((entry) => entry.name === name) !== -1 112 } 113 114 set (name, value, filename = undefined) { 115 webidl.brandCheck(this, FormData) 116 117 webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' }) 118 119 if (arguments.length === 3 && !isBlobLike(value)) { 120 throw new TypeError( 121 "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" 122 ) 123 } 124 125 // The set(name, value) and set(name, blobValue, filename) method steps 126 // are: 127 128 // 1. Let value be value if given; otherwise blobValue. 129 130 name = webidl.converters.USVString(name) 131 value = isBlobLike(value) 132 ? webidl.converters.Blob(value, { strict: false }) 133 : webidl.converters.USVString(value) 134 filename = arguments.length === 3 135 ? toUSVString(filename) 136 : undefined 137 138 // 2. Let entry be the result of creating an entry with name, value, and 139 // filename if given. 140 const entry = makeEntry(name, value, filename) 141 142 // 3. If there are entries in this’s entry list whose name is name, then 143 // replace the first such entry with entry and remove the others. 144 const idx = this[kState].findIndex((entry) => entry.name === name) 145 if (idx !== -1) { 146 this[kState] = [ 147 ...this[kState].slice(0, idx), 148 entry, 149 ...this[kState].slice(idx + 1).filter((entry) => entry.name !== name) 150 ] 151 } else { 152 // 4. Otherwise, append entry to this’s entry list. 153 this[kState].push(entry) 154 } 155 } 156 157 entries () { 158 webidl.brandCheck(this, FormData) 159 160 return makeIterator( 161 () => this[kState].map(pair => [pair.name, pair.value]), 162 'FormData', 163 'key+value' 164 ) 165 } 166 167 keys () { 168 webidl.brandCheck(this, FormData) 169 170 return makeIterator( 171 () => this[kState].map(pair => [pair.name, pair.value]), 172 'FormData', 173 'key' 174 ) 175 } 176 177 values () { 178 webidl.brandCheck(this, FormData) 179 180 return makeIterator( 181 () => this[kState].map(pair => [pair.name, pair.value]), 182 'FormData', 183 'value' 184 ) 185 } 186 187 /** 188 * @param {(value: string, key: string, self: FormData) => void} callbackFn 189 * @param {unknown} thisArg 190 */ 191 forEach (callbackFn, thisArg = globalThis) { 192 webidl.brandCheck(this, FormData) 193 194 webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' }) 195 196 if (typeof callbackFn !== 'function') { 197 throw new TypeError( 198 "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'." 199 ) 200 } 201 202 for (const [key, value] of this) { 203 callbackFn.apply(thisArg, [value, key, this]) 204 } 205 } 206} 207 208FormData.prototype[Symbol.iterator] = FormData.prototype.entries 209 210Object.defineProperties(FormData.prototype, { 211 [Symbol.toStringTag]: { 212 value: 'FormData', 213 configurable: true 214 } 215}) 216 217/** 218 * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry 219 * @param {string} name 220 * @param {string|Blob} value 221 * @param {?string} filename 222 * @returns 223 */ 224function makeEntry (name, value, filename) { 225 // 1. Set name to the result of converting name into a scalar value string. 226 // "To convert a string into a scalar value string, replace any surrogates 227 // with U+FFFD." 228 // see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end 229 name = Buffer.from(name).toString('utf8') 230 231 // 2. If value is a string, then set value to the result of converting 232 // value into a scalar value string. 233 if (typeof value === 'string') { 234 value = Buffer.from(value).toString('utf8') 235 } else { 236 // 3. Otherwise: 237 238 // 1. If value is not a File object, then set value to a new File object, 239 // representing the same bytes, whose name attribute value is "blob" 240 if (!isFileLike(value)) { 241 value = value instanceof Blob 242 ? new File([value], 'blob', { type: value.type }) 243 : new FileLike(value, 'blob', { type: value.type }) 244 } 245 246 // 2. If filename is given, then set value to a new File object, 247 // representing the same bytes, whose name attribute is filename. 248 if (filename !== undefined) { 249 /** @type {FilePropertyBag} */ 250 const options = { 251 type: value.type, 252 lastModified: value.lastModified 253 } 254 255 value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile 256 ? new File([value], filename, options) 257 : new FileLike(value, filename, options) 258 } 259 } 260 261 // 4. Return an entry whose name is name and whose value is value. 262 return { name, value } 263} 264 265module.exports = { FormData } 266