formdata.js 7.2 KB

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