formdata.js 7.7 KB

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