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