wp-polyfill-formdata.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. if (typeof FormData === 'undefined' || !FormData.prototype.keys) {
  2. const global = typeof window === 'object'
  3. ? window : typeof self === 'object'
  4. ? self : this
  5. // keep a reference to native implementation
  6. const _FormData = global.FormData
  7. // To be monkey patched
  8. const _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send
  9. const _fetch = global.Request && global.fetch
  10. // Unable to patch Request constructor correctly
  11. // const _Request = global.Request
  12. // only way is to use ES6 class extend
  13. // https://github.com/babel/babel/issues/1966
  14. const stringTag = global.Symbol && Symbol.toStringTag
  15. const map = new WeakMap
  16. const wm = o => map.get(o)
  17. const arrayFrom = Array.from || (obj => [].slice.call(obj))
  18. // Add missing stringTags to blob and files
  19. if (stringTag) {
  20. if (!Blob.prototype[stringTag]) {
  21. Blob.prototype[stringTag] = 'Blob'
  22. }
  23. if ('File' in global && !File.prototype[stringTag]) {
  24. File.prototype[stringTag] = 'File'
  25. }
  26. }
  27. // Fix so you can construct your own File
  28. try {
  29. new File([], '')
  30. } catch (a) {
  31. global.File = function(b, d, c) {
  32. const blob = new Blob(b, c)
  33. const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date
  34. Object.defineProperties(blob, {
  35. name: {
  36. value: d
  37. },
  38. lastModifiedDate: {
  39. value: t
  40. },
  41. lastModified: {
  42. value: +t
  43. },
  44. toString: {
  45. value() {
  46. return '[object File]'
  47. }
  48. }
  49. })
  50. if (stringTag) {
  51. Object.defineProperty(blob, stringTag, {
  52. value: 'File'
  53. })
  54. }
  55. return blob
  56. }
  57. }
  58. function normalizeValue([value, filename]) {
  59. if (value instanceof Blob)
  60. // Should always returns a new File instance
  61. // console.assert(fd.get(x) !== fd.get(x))
  62. value = new File([value], filename, {
  63. type: value.type,
  64. lastModified: value.lastModified
  65. })
  66. return value
  67. }
  68. function stringify(name) {
  69. if (!arguments.length)
  70. throw new TypeError('1 argument required, but only 0 present.')
  71. return [name + '']
  72. }
  73. function normalizeArgs(name, value, filename) {
  74. if (arguments.length < 2)
  75. throw new TypeError(
  76. `2 arguments required, but only ${arguments.length} present.`
  77. )
  78. return value instanceof Blob
  79. // normalize name and filename if adding an attachment
  80. ? [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. : [name + '', value + '']
  87. }
  88. function each (arr, cb) {
  89. for (let i = 0; i < arr.length; i++) {
  90. cb(arr[i])
  91. }
  92. }
  93. /**
  94. * @implements {Iterable}
  95. */
  96. class FormDataPolyfill {
  97. /**
  98. * FormData class
  99. *
  100. * @param {HTMLElement=} form
  101. */
  102. constructor(form) {
  103. map.set(this, Object.create(null))
  104. if (!form)
  105. return this
  106. const self = this
  107. each(form.elements, elm => {
  108. if (!elm.name || elm.disabled || elm.type === 'submit' || elm.type === 'button') return
  109. if (elm.type === 'file') {
  110. each(elm.files || [], file => {
  111. self.append(elm.name, file)
  112. })
  113. } else if (elm.type === 'select-multiple' || elm.type === 'select-one') {
  114. each(elm.options, opt => {
  115. !opt.disabled && opt.selected && self.append(elm.name, opt.value)
  116. })
  117. } else if (elm.type === 'checkbox' || elm.type === 'radio') {
  118. if (elm.checked) self.append(elm.name, elm.value)
  119. } else {
  120. self.append(elm.name, elm.value)
  121. }
  122. })
  123. }
  124. /**
  125. * Append a field
  126. *
  127. * @param {String} name field name
  128. * @param {String|Blob|File} value string / blob / file
  129. * @param {String=} filename filename to use with blob
  130. * @return {Undefined}
  131. */
  132. append(name, value, filename) {
  133. const map = wm(this)
  134. if (!map[name])
  135. map[name] = []
  136. map[name].push([value, filename])
  137. }
  138. /**
  139. * Delete all fields values given name
  140. *
  141. * @param {String} name Field name
  142. * @return {Undefined}
  143. */
  144. delete(name) {
  145. delete wm(this)[name]
  146. }
  147. /**
  148. * Iterate over all fields as [name, value]
  149. *
  150. * @return {Iterator}
  151. */
  152. *entries() {
  153. const map = wm(this)
  154. for (let name in map)
  155. for (let value of map[name])
  156. yield [name, normalizeValue(value)]
  157. }
  158. /**
  159. * Iterate over all fields
  160. *
  161. * @param {Function} callback Executed for each item with parameters (value, name, thisArg)
  162. * @param {Object=} thisArg `this` context for callback function
  163. * @return {Undefined}
  164. */
  165. forEach(callback, thisArg) {
  166. for (let [name, value] of this)
  167. callback.call(thisArg, value, name, this)
  168. }
  169. /**
  170. * Return first field value given name
  171. * or null if non existen
  172. *
  173. * @param {String} name Field name
  174. * @return {String|File|null} value Fields value
  175. */
  176. get(name) {
  177. const map = wm(this)
  178. return map[name] ? normalizeValue(map[name][0]) : null
  179. }
  180. /**
  181. * Return all fields values given name
  182. *
  183. * @param {String} name Fields name
  184. * @return {Array} [{String|File}]
  185. */
  186. getAll(name) {
  187. return (wm(this)[name] || []).map(normalizeValue)
  188. }
  189. /**
  190. * Check for field name existence
  191. *
  192. * @param {String} name Field name
  193. * @return {boolean}
  194. */
  195. has(name) {
  196. return name in wm(this)
  197. }
  198. /**
  199. * Iterate over all fields name
  200. *
  201. * @return {Iterator}
  202. */
  203. *keys() {
  204. for (let [name] of this)
  205. yield name
  206. }
  207. /**
  208. * Overwrite all values given name
  209. *
  210. * @param {String} name Filed name
  211. * @param {String} value Field value
  212. * @param {String=} filename Filename (optional)
  213. * @return {Undefined}
  214. */
  215. set(name, value, filename) {
  216. wm(this)[name] = [[value, filename]]
  217. }
  218. /**
  219. * Iterate over all fields
  220. *
  221. * @return {Iterator}
  222. */
  223. *values() {
  224. for (let [name, value] of this)
  225. yield value
  226. }
  227. /**
  228. * Return a native (perhaps degraded) FormData with only a `append` method
  229. * Can throw if it's not supported
  230. *
  231. * @return {FormData}
  232. */
  233. ['_asNative']() {
  234. const fd = new _FormData
  235. for (let [name, value] of this)
  236. fd.append(name, value)
  237. return fd
  238. }
  239. /**
  240. * [_blob description]
  241. *
  242. * @return {Blob} [description]
  243. */
  244. ['_blob']() {
  245. const boundary = '----formdata-polyfill-' + Math.random()
  246. const chunks = []
  247. for (let [name, value] of this) {
  248. chunks.push(`--${boundary}\r\n`)
  249. if (value instanceof Blob) {
  250. chunks.push(
  251. `Content-Disposition: form-data; name="${name}"; filename="${value.name}"\r\n`,
  252. `Content-Type: ${value.type || 'application/octet-stream'}\r\n\r\n`,
  253. value,
  254. '\r\n'
  255. )
  256. } else {
  257. chunks.push(
  258. `Content-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`
  259. )
  260. }
  261. }
  262. chunks.push(`--${boundary}--`)
  263. return new Blob(chunks, {type: 'multipart/form-data; boundary=' + boundary})
  264. }
  265. /**
  266. * The class itself is iterable
  267. * alias for formdata.entries()
  268. *
  269. * @return {Iterator}
  270. */
  271. [Symbol.iterator]() {
  272. return this.entries()
  273. }
  274. /**
  275. * Create the default string description.
  276. *
  277. * @return {String} [object FormData]
  278. */
  279. toString() {
  280. return '[object FormData]'
  281. }
  282. }
  283. if (stringTag) {
  284. /**
  285. * Create the default string description.
  286. * It is accessed internally by the Object.prototype.toString().
  287. *
  288. * @return {String} FormData
  289. */
  290. FormDataPolyfill.prototype[stringTag] = 'FormData'
  291. }
  292. const decorations = [
  293. ['append', normalizeArgs],
  294. ['delete', stringify],
  295. ['get', stringify],
  296. ['getAll', stringify],
  297. ['has', stringify],
  298. ['set', normalizeArgs]
  299. ]
  300. decorations.forEach(arr => {
  301. const orig = FormDataPolyfill.prototype[arr[0]]
  302. FormDataPolyfill.prototype[arr[0]] = function() {
  303. return orig.apply(this, arr[1].apply(this, arrayFrom(arguments)))
  304. }
  305. })
  306. // Patch xhr's send method to call _blob transparently
  307. if (_send) {
  308. XMLHttpRequest.prototype.send = function(data) {
  309. // I would check if Content-Type isn't already set
  310. // But xhr lacks getRequestHeaders functionallity
  311. // https://github.com/jimmywarting/FormData/issues/44
  312. if (data instanceof FormDataPolyfill) {
  313. const blob = data['_blob']()
  314. this.setRequestHeader('Content-Type', blob.type)
  315. _send.call(this, blob)
  316. } else {
  317. _send.call(this, data)
  318. }
  319. }
  320. }
  321. // Patch fetch's function to call _blob transparently
  322. if (_fetch) {
  323. const _fetch = global.fetch
  324. global.fetch = function(input, init) {
  325. if (init && init.body && init.body instanceof FormDataPolyfill) {
  326. init.body = init.body['_blob']()
  327. }
  328. return _fetch(input, init)
  329. }
  330. }
  331. global['FormData'] = FormDataPolyfill
  332. }