formdata.js 7.6 KB

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