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