wp-polyfill-formdata.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. /* formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
  2. /* global FormData self Blob File */
  3. /* eslint-disable no-inner-declarations */
  4. if (typeof Blob !== 'undefined' && (typeof FormData === 'undefined' || !FormData.prototype.keys)) {
  5. const global = typeof globalThis === 'object'
  6. ? globalThis
  7. : typeof window === 'object'
  8. ? window
  9. : typeof self === 'object' ? self : this
  10. // keep a reference to native implementation
  11. const _FormData = global.FormData
  12. // To be monkey patched
  13. const _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send
  14. const _fetch = global.Request && global.fetch
  15. const _sendBeacon = global.navigator && global.navigator.sendBeacon
  16. // Might be a worker thread...
  17. const _match = global.Element && global.Element.prototype
  18. // Unable to patch Request/Response constructor correctly #109
  19. // only way is to use ES6 class extend
  20. // https://github.com/babel/babel/issues/1966
  21. const stringTag = global.Symbol && Symbol.toStringTag
  22. // Add missing stringTags to blob and files
  23. if (stringTag) {
  24. if (!Blob.prototype[stringTag]) {
  25. Blob.prototype[stringTag] = 'Blob'
  26. }
  27. if ('File' in global && !File.prototype[stringTag]) {
  28. File.prototype[stringTag] = 'File'
  29. }
  30. }
  31. // Fix so you can construct your own File
  32. try {
  33. new File([], '') // eslint-disable-line
  34. } catch (a) {
  35. global.File = function File (b, d, c) {
  36. const blob = new Blob(b, c || {})
  37. const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date()
  38. Object.defineProperties(blob, {
  39. name: {
  40. value: d
  41. },
  42. lastModified: {
  43. value: +t
  44. },
  45. toString: {
  46. value () {
  47. return '[object File]'
  48. }
  49. }
  50. })
  51. if (stringTag) {
  52. Object.defineProperty(blob, stringTag, {
  53. value: 'File'
  54. })
  55. }
  56. return blob
  57. }
  58. }
  59. function ensureArgs (args, expected) {
  60. if (args.length < expected) {
  61. throw new TypeError(`${expected} argument required, but only ${args.length} present.`)
  62. }
  63. }
  64. /**
  65. * @param {string} name
  66. * @param {string | undefined} filename
  67. * @returns {[string, File|string]}
  68. */
  69. function normalizeArgs (name, value, filename) {
  70. if (value instanceof Blob) {
  71. filename = filename !== undefined
  72. ? String(filename + '')
  73. : typeof value.name === 'string'
  74. ? value.name
  75. : 'blob'
  76. if (value.name !== filename || Object.prototype.toString.call(value) === '[object Blob]') {
  77. value = new File([value], filename)
  78. }
  79. return [String(name), value]
  80. }
  81. return [String(name), String(value)]
  82. }
  83. // normalize line feeds for textarea
  84. // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-line-break-normalisation-transformation
  85. function normalizeLinefeeds (value) {
  86. return value.replace(/\r?\n|\r/g, '\r\n')
  87. }
  88. /**
  89. * @template T
  90. * @param {ArrayLike<T>} arr
  91. * @param {{ (elm: T): void; }} cb
  92. */
  93. function each (arr, cb) {
  94. for (let i = 0; i < arr.length; i++) {
  95. cb(arr[i])
  96. }
  97. }
  98. const escape = str => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
  99. /**
  100. * @implements {Iterable}
  101. */
  102. class FormDataPolyfill {
  103. /**
  104. * FormData class
  105. *
  106. * @param {HTMLFormElement=} form
  107. */
  108. constructor (form) {
  109. /** @type {[string, string|File][]} */
  110. this._data = []
  111. const self = this
  112. form && each(form.elements, (/** @type {HTMLInputElement} */ elm) => {
  113. if (
  114. !elm.name ||
  115. elm.disabled ||
  116. elm.type === 'submit' ||
  117. elm.type === 'button' ||
  118. elm.matches('form fieldset[disabled] *')
  119. ) return
  120. if (elm.type === 'file') {
  121. const files = elm.files && elm.files.length
  122. ? elm.files
  123. : [new File([], '', { type: 'application/octet-stream' })] // #78
  124. each(files, file => {
  125. self.append(elm.name, file)
  126. })
  127. } else if (elm.type === 'select-multiple' || elm.type === 'select-one') {
  128. each(elm.options, opt => {
  129. !opt.disabled && opt.selected && self.append(elm.name, opt.value)
  130. })
  131. } else if (elm.type === 'checkbox' || elm.type === 'radio') {
  132. if (elm.checked) self.append(elm.name, elm.value)
  133. } else {
  134. const value = elm.type === 'textarea' ? normalizeLinefeeds(elm.value) : elm.value
  135. self.append(elm.name, value)
  136. }
  137. })
  138. }
  139. /**
  140. * Append a field
  141. *
  142. * @param {string} name field name
  143. * @param {string|Blob|File} value string / blob / file
  144. * @param {string=} filename filename to use with blob
  145. * @return {undefined}
  146. */
  147. append (name, value, filename) {
  148. ensureArgs(arguments, 2)
  149. this._data.push(normalizeArgs(name, value, filename))
  150. }
  151. /**
  152. * Delete all fields values given name
  153. *
  154. * @param {string} name Field name
  155. * @return {undefined}
  156. */
  157. delete (name) {
  158. ensureArgs(arguments, 1)
  159. const result = []
  160. name = String(name)
  161. each(this._data, entry => {
  162. entry[0] !== name && result.push(entry)
  163. })
  164. this._data = result
  165. }
  166. /**
  167. * Iterate over all fields as [name, value]
  168. *
  169. * @return {Iterator}
  170. */
  171. * entries () {
  172. for (var i = 0; i < this._data.length; i++) {
  173. yield this._data[i]
  174. }
  175. }
  176. /**
  177. * Iterate over all fields
  178. *
  179. * @param {Function} callback Executed for each item with parameters (value, name, thisArg)
  180. * @param {Object=} thisArg `this` context for callback function
  181. */
  182. forEach (callback, thisArg) {
  183. ensureArgs(arguments, 1)
  184. for (const [name, value] of this) {
  185. callback.call(thisArg, value, name, this)
  186. }
  187. }
  188. /**
  189. * Return first field value given name
  190. * or null if non existent
  191. *
  192. * @param {string} name Field name
  193. * @return {string|File|null} value Fields value
  194. */
  195. get (name) {
  196. ensureArgs(arguments, 1)
  197. const entries = this._data
  198. name = String(name)
  199. for (let i = 0; i < entries.length; i++) {
  200. if (entries[i][0] === name) {
  201. return entries[i][1]
  202. }
  203. }
  204. return null
  205. }
  206. /**
  207. * Return all fields values given name
  208. *
  209. * @param {string} name Fields name
  210. * @return {Array} [{String|File}]
  211. */
  212. getAll (name) {
  213. ensureArgs(arguments, 1)
  214. const result = []
  215. name = String(name)
  216. each(this._data, data => {
  217. data[0] === name && result.push(data[1])
  218. })
  219. return result
  220. }
  221. /**
  222. * Check for field name existence
  223. *
  224. * @param {string} name Field name
  225. * @return {boolean}
  226. */
  227. has (name) {
  228. ensureArgs(arguments, 1)
  229. name = String(name)
  230. for (let i = 0; i < this._data.length; i++) {
  231. if (this._data[i][0] === name) {
  232. return true
  233. }
  234. }
  235. return false
  236. }
  237. /**
  238. * Iterate over all fields name
  239. *
  240. * @return {Iterator}
  241. */
  242. * keys () {
  243. for (const [name] of this) {
  244. yield name
  245. }
  246. }
  247. /**
  248. * Overwrite all values given name
  249. *
  250. * @param {string} name Filed name
  251. * @param {string} value Field value
  252. * @param {string=} filename Filename (optional)
  253. */
  254. set (name, value, filename) {
  255. ensureArgs(arguments, 2)
  256. name = String(name)
  257. /** @type {[string, string|File][]} */
  258. const result = []
  259. const args = normalizeArgs(name, value, filename)
  260. let replace = true
  261. // - replace the first occurrence with same name
  262. // - discards the remaining with same name
  263. // - while keeping the same order items where added
  264. each(this._data, data => {
  265. data[0] === name
  266. ? replace && (replace = !result.push(args))
  267. : result.push(data)
  268. })
  269. replace && result.push(args)
  270. this._data = result
  271. }
  272. /**
  273. * Iterate over all fields
  274. *
  275. * @return {Iterator}
  276. */
  277. * values () {
  278. for (const [, value] of this) {
  279. yield value
  280. }
  281. }
  282. /**
  283. * Return a native (perhaps degraded) FormData with only a `append` method
  284. * Can throw if it's not supported
  285. *
  286. * @return {FormData}
  287. */
  288. ['_asNative'] () {
  289. const fd = new _FormData()
  290. for (const [name, value] of this) {
  291. fd.append(name, value)
  292. }
  293. return fd
  294. }
  295. /**
  296. * [_blob description]
  297. *
  298. * @return {Blob} [description]
  299. */
  300. ['_blob'] () {
  301. const boundary = '----formdata-polyfill-' + Math.random(),
  302. chunks = [],
  303. p = `--${boundary}\r\nContent-Disposition: form-data; name="`
  304. this.forEach((value, name) => typeof value == 'string'
  305. ? chunks.push(p + escape(normalizeLinefeeds(name)) + `"\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
  306. : chunks.push(p + escape(normalizeLinefeeds(name)) + `"; filename="${escape(value.name)}"\r\nContent-Type: ${value.type||"application/octet-stream"}\r\n\r\n`, value, `\r\n`))
  307. chunks.push(`--${boundary}--`)
  308. return new Blob(chunks, {
  309. type: "multipart/form-data; boundary=" + boundary
  310. })
  311. }
  312. /**
  313. * The class itself is iterable
  314. * alias for formdata.entries()
  315. *
  316. * @return {Iterator}
  317. */
  318. [Symbol.iterator] () {
  319. return this.entries()
  320. }
  321. /**
  322. * Create the default string description.
  323. *
  324. * @return {string} [object FormData]
  325. */
  326. toString () {
  327. return '[object FormData]'
  328. }
  329. }
  330. if (_match && !_match.matches) {
  331. _match.matches =
  332. _match.matchesSelector ||
  333. _match.mozMatchesSelector ||
  334. _match.msMatchesSelector ||
  335. _match.oMatchesSelector ||
  336. _match.webkitMatchesSelector ||
  337. function (s) {
  338. var matches = (this.document || this.ownerDocument).querySelectorAll(s)
  339. var i = matches.length
  340. while (--i >= 0 && matches.item(i) !== this) {}
  341. return i > -1
  342. }
  343. }
  344. if (stringTag) {
  345. /**
  346. * Create the default string description.
  347. * It is accessed internally by the Object.prototype.toString().
  348. */
  349. FormDataPolyfill.prototype[stringTag] = 'FormData'
  350. }
  351. // Patch xhr's send method to call _blob transparently
  352. if (_send) {
  353. const setRequestHeader = global.XMLHttpRequest.prototype.setRequestHeader
  354. global.XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
  355. setRequestHeader.call(this, name, value)
  356. if (name.toLowerCase() === 'content-type') this._hasContentType = true
  357. }
  358. global.XMLHttpRequest.prototype.send = function (data) {
  359. // need to patch send b/c old IE don't send blob's type (#44)
  360. if (data instanceof FormDataPolyfill) {
  361. const blob = data['_blob']()
  362. if (!this._hasContentType) this.setRequestHeader('Content-Type', blob.type)
  363. _send.call(this, blob)
  364. } else {
  365. _send.call(this, data)
  366. }
  367. }
  368. }
  369. // Patch fetch's function to call _blob transparently
  370. if (_fetch) {
  371. global.fetch = function (input, init) {
  372. if (init && init.body && init.body instanceof FormDataPolyfill) {
  373. init.body = init.body['_blob']()
  374. }
  375. return _fetch.call(this, input, init)
  376. }
  377. }
  378. // Patch navigator.sendBeacon to use native FormData
  379. if (_sendBeacon) {
  380. global.navigator.sendBeacon = function (url, data) {
  381. if (data instanceof FormDataPolyfill) {
  382. data = data['_asNative']()
  383. }
  384. return _sendBeacon.call(this, url, data)
  385. }
  386. }
  387. global['FormData'] = FormDataPolyfill
  388. }