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