Sin descripción

wp-polyfill-formdata.js 12KB

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