formdata.js 7.4 KB

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