/* formdata-polyfill. MIT License. Jimmy Wärting */ /* global FormData self Blob File */ /* eslint-disable no-inner-declarations */ if (typeof Blob !== 'undefined' && (typeof FormData === 'undefined' || !FormData.prototype.keys)) { const global = typeof globalThis === 'object' ? globalThis : typeof window === 'object' ? window : typeof self === 'object' ? self : this // keep a reference to native implementation const _FormData = global.FormData // To be monkey patched const _send = global.XMLHttpRequest && global.XMLHttpRequest.prototype.send const _fetch = global.Request && global.fetch const _sendBeacon = global.navigator && global.navigator.sendBeacon // Might be a worker thread... const _match = global.Element && global.Element.prototype // Unable to patch Request/Response constructor correctly #109 // only way is to use ES6 class extend // https://github.com/babel/babel/issues/1966 const stringTag = global.Symbol && Symbol.toStringTag // Add missing stringTags to blob and files if (stringTag) { if (!Blob.prototype[stringTag]) { Blob.prototype[stringTag] = 'Blob' } if ('File' in global && !File.prototype[stringTag]) { File.prototype[stringTag] = 'File' } } // Fix so you can construct your own File try { new File([], '') // eslint-disable-line } catch (a) { global.File = function File (b, d, c) { const blob = new Blob(b, c || {}) const t = c && void 0 !== c.lastModified ? new Date(c.lastModified) : new Date() Object.defineProperties(blob, { name: { value: d }, lastModified: { value: +t }, toString: { value () { return '[object File]' } } }) if (stringTag) { Object.defineProperty(blob, stringTag, { value: 'File' }) } return blob } } function ensureArgs (args, expected) { if (args.length < expected) { throw new TypeError(`${expected} argument required, but only ${args.length} present.`) } } /** * @param {string} name * @param {string | undefined} filename * @returns {[string, File|string]} */ function normalizeArgs (name, value, filename) { if (value instanceof Blob) { filename = filename !== undefined ? String(filename + '') : typeof value.name === 'string' ? value.name : 'blob' if (value.name !== filename || Object.prototype.toString.call(value) === '[object Blob]') { value = new File([value], filename) } return [String(name), value] } return [String(name), String(value)] } // normalize line feeds for textarea // https://html.spec.whatwg.org/multipage/form-elements.html#textarea-line-break-normalisation-transformation function normalizeLinefeeds (value) { return value.replace(/\r?\n|\r/g, '\r\n') } /** * @template T * @param {ArrayLike} arr * @param {{ (elm: T): void; }} cb */ function each (arr, cb) { for (let i = 0; i < arr.length; i++) { cb(arr[i]) } } const escape = str => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22') /** * @implements {Iterable} */ class FormDataPolyfill { /** * FormData class * * @param {HTMLFormElement=} form */ constructor (form) { /** @type {[string, string|File][]} */ this._data = [] const self = this form && each(form.elements, (/** @type {HTMLInputElement} */ elm) => { if ( !elm.name || elm.disabled || elm.type === 'submit' || elm.type === 'button' || elm.matches('form fieldset[disabled] *') ) return if (elm.type === 'file') { const files = elm.files && elm.files.length ? elm.files : [new File([], '', { type: 'application/octet-stream' })] // #78 each(files, file => { self.append(elm.name, file) }) } else if (elm.type === 'select-multiple' || elm.type === 'select-one') { each(elm.options, opt => { !opt.disabled && opt.selected && self.append(elm.name, opt.value) }) } else if (elm.type === 'checkbox' || elm.type === 'radio') { if (elm.checked) self.append(elm.name, elm.value) } else { const value = elm.type === 'textarea' ? normalizeLinefeeds(elm.value) : elm.value self.append(elm.name, value) } }) } /** * Append a field * * @param {string} name field name * @param {string|Blob|File} value string / blob / file * @param {string=} filename filename to use with blob * @return {undefined} */ append (name, value, filename) { ensureArgs(arguments, 2) this._data.push(normalizeArgs(name, value, filename)) } /** * Delete all fields values given name * * @param {string} name Field name * @return {undefined} */ delete (name) { ensureArgs(arguments, 1) const result = [] name = String(name) each(this._data, entry => { entry[0] !== name && result.push(entry) }) this._data = result } /** * Iterate over all fields as [name, value] * * @return {Iterator} */ * entries () { for (var i = 0; i < this._data.length; i++) { yield this._data[i] } } /** * Iterate over all fields * * @param {Function} callback Executed for each item with parameters (value, name, thisArg) * @param {Object=} thisArg `this` context for callback function */ forEach (callback, thisArg) { ensureArgs(arguments, 1) for (const [name, value] of this) { callback.call(thisArg, value, name, this) } } /** * Return first field value given name * or null if non existent * * @param {string} name Field name * @return {string|File|null} value Fields value */ get (name) { ensureArgs(arguments, 1) const entries = this._data name = String(name) for (let i = 0; i < entries.length; i++) { if (entries[i][0] === name) { return entries[i][1] } } return null } /** * Return all fields values given name * * @param {string} name Fields name * @return {Array} [{String|File}] */ getAll (name) { ensureArgs(arguments, 1) const result = [] name = String(name) each(this._data, data => { data[0] === name && result.push(data[1]) }) return result } /** * Check for field name existence * * @param {string} name Field name * @return {boolean} */ has (name) { ensureArgs(arguments, 1) name = String(name) for (let i = 0; i < this._data.length; i++) { if (this._data[i][0] === name) { return true } } return false } /** * Iterate over all fields name * * @return {Iterator} */ * keys () { for (const [name] of this) { yield name } } /** * Overwrite all values given name * * @param {string} name Filed name * @param {string} value Field value * @param {string=} filename Filename (optional) */ set (name, value, filename) { ensureArgs(arguments, 2) name = String(name) /** @type {[string, string|File][]} */ const result = [] const args = normalizeArgs(name, value, filename) let replace = true // - replace the first occurrence with same name // - discards the remaining with same name // - while keeping the same order items where added each(this._data, data => { data[0] === name ? replace && (replace = !result.push(args)) : result.push(data) }) replace && result.push(args) this._data = result } /** * Iterate over all fields * * @return {Iterator} */ * values () { for (const [, value] of this) { yield value } } /** * Return a native (perhaps degraded) FormData with only a `append` method * Can throw if it's not supported * * @return {FormData} */ ['_asNative'] () { const fd = new _FormData() for (const [name, value] of this) { fd.append(name, value) } return fd } /** * [_blob description] * * @return {Blob} [description] */ ['_blob'] () { const boundary = '----formdata-polyfill-' + Math.random(), chunks = [], p = `--${boundary}\r\nContent-Disposition: form-data; name="` this.forEach((value, name) => typeof value == 'string' ? chunks.push(p + escape(normalizeLinefeeds(name)) + `"\r\n\r\n${normalizeLinefeeds(value)}\r\n`) : 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`)) chunks.push(`--${boundary}--`) return new Blob(chunks, { type: "multipart/form-data; boundary=" + boundary }) } /** * The class itself is iterable * alias for formdata.entries() * * @return {Iterator} */ [Symbol.iterator] () { return this.entries() } /** * Create the default string description. * * @return {string} [object FormData] */ toString () { return '[object FormData]' } } if (_match && !_match.matches) { _match.matches = _match.matchesSelector || _match.mozMatchesSelector || _match.msMatchesSelector || _match.oMatchesSelector || _match.webkitMatchesSelector || function (s) { var matches = (this.document || this.ownerDocument).querySelectorAll(s) var i = matches.length while (--i >= 0 && matches.item(i) !== this) {} return i > -1 } } if (stringTag) { /** * Create the default string description. * It is accessed internally by the Object.prototype.toString(). */ FormDataPolyfill.prototype[stringTag] = 'FormData' } // Patch xhr's send method to call _blob transparently if (_send) { const setRequestHeader = global.XMLHttpRequest.prototype.setRequestHeader global.XMLHttpRequest.prototype.setRequestHeader = function (name, value) { setRequestHeader.call(this, name, value) if (name.toLowerCase() === 'content-type') this._hasContentType = true } global.XMLHttpRequest.prototype.send = function (data) { // need to patch send b/c old IE don't send blob's type (#44) if (data instanceof FormDataPolyfill) { const blob = data['_blob']() if (!this._hasContentType) this.setRequestHeader('Content-Type', blob.type) _send.call(this, blob) } else { _send.call(this, data) } } } // Patch fetch's function to call _blob transparently if (_fetch) { global.fetch = function (input, init) { if (init && init.body && init.body instanceof FormDataPolyfill) { init.body = init.body['_blob']() } return _fetch.call(this, input, init) } } // Patch navigator.sendBeacon to use native FormData if (_sendBeacon) { global.navigator.sendBeacon = function (url, data) { if (data instanceof FormDataPolyfill) { data = data['_asNative']() } return _sendBeacon.call(this, url, data) } } global['FormData'] = FormDataPolyfill }