mediawiki.api.uploadWithFormData: Implement in terms of existing mw.Api functionality
authorBartosz Dziewoński <matma.rex@gmail.com>
Tue, 18 Aug 2015 22:50:30 +0000 (00:50 +0200)
committerJforrester <jforrester@wikimedia.org>
Mon, 24 Aug 2015 18:26:37 +0000 (18:26 +0000)
* mw.Api#ajax can already handle FormData, if instructed to, since
  d19432a332c21935d42087db706e50c5259063ea (which seems to have been a
  part of mobile uploads experiments).
* MobileFrontend's api.js already had code to provide upload progress
  events while using mw.Api, lifted it from there.

With this change, we should be able to just use mw.ForeignApi (being
added in Ic20b9682d28633baa87d22e6e9fb71ce507da58d) to upload to a
different wiki. (Assuming that the browser supports FormData.)

Additionally:

* Improve detection of whether we can use FormData: if we are given a
  HTMLInputElement, try to get a File from it before we fall back to
  iframe form upload.
* mediawiki.api.edit: In #postWithEditToken, pass through the
  ajaxOptions parameter to #postWithToken.

Change-Id: Ib9abe32ee3320c67ac0a4544c942b844a5550562

resources/src/mediawiki.api/mediawiki.api.edit.js
resources/src/mediawiki.api/mediawiki.api.upload.js

index dbe45bf..e6161e4 100644 (file)
                 * cached token and start over.
                 *
                 * @param {Object} params API parameters
+                * @param {Object} [ajaxOptions]
                 * @return {jQuery.Promise} See #post
                 */
-               postWithEditToken: function ( params ) {
-                       return this.postWithToken( 'edit', params );
+               postWithEditToken: function ( params, ajaxOptions ) {
+                       return this.postWithToken( 'edit', params, ajaxOptions );
                },
 
                /**
index cec352a..19d2542 100644 (file)
                        .val( val );
        }
 
-       /**
-        * Parse response from an XHR to the server.
-        * @private
-        * @param {Event} e
-        * @return {Object}
-        */
-       function parseXHRResponse( e ) {
-               var response;
-
-               try {
-                       response = $.parseJSON( e.target.responseText );
-               } catch ( error ) {
-                       response = {
-                               error: {
-                                       code: e.target.code,
-                                       info: e.target.responseText
-                               }
-                       };
-               }
-
-               return response;
-       }
-
        /**
         * Process the result of the form submission, returned to an iframe.
         * This is the iframe's onload event.
                 * @return {jQuery.Promise}
                 */
                upload: function ( file, data ) {
-                       var iframe, formData;
+                       var isFileInput, canUseFormData;
+
+                       isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
+
+                       if ( formDataAvailable() && isFileInput && file.files ) {
+                               file = file.files[0];
+                       }
 
                        if ( !file ) {
                                return $.Deferred().reject( 'No file' );
                        }
 
-                       iframe = file.nodeType && file.nodeType === Node.ELEMENT_NODE;
-                       formData = formDataAvailable() && file instanceof window.File;
+                       canUseFormData = formDataAvailable() && file instanceof window.File;
 
-                       if ( !iframe && !formData ) {
+                       if ( !isFileInput && !canUseFormData ) {
                                return $.Deferred().reject( 'Unsupported argument type passed to mw.Api.upload' );
                        }
 
-                       if ( formData ) {
+                       if ( canUseFormData ) {
                                return this.uploadWithFormData( file, data );
                        }
 
                 * Uploads a file using the FormData API.
                 * @param {File} file
                 * @param {Object} data
+                * @return {jQuery.Promise}
                 */
                uploadWithFormData: function ( file, data ) {
-                       var key, xhr,
-                               api = this,
-                               formData = new FormData(),
+                       var key,
                                deferred = $.Deferred();
 
                        for ( key in data ) {
                        }
 
                        data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
-
-                       $.each( data, function ( key, val ) {
-                               formData.append( key, val );
-                       } );
+                       data.file = file;
 
                        if ( !data.filename && !data.stash ) {
                                return $.Deferred().reject( 'Filename not included in file data.' );
                        }
 
-                       formData.append( 'file', file );
-
-                       xhr = new XMLHttpRequest();
-
-                       xhr.upload.addEventListener( 'progress', function ( e ) {
-                               if ( e.lengthComputable ) {
-                                       deferred.notify( e.loaded / e.total );
-                               }
-                       }, false );
-
-                       xhr.addEventListener( 'abort', function ( e ) {
-                               deferred.reject( parseXHRResponse( e ) );
-                       }, false );
-
-                       xhr.addEventListener( 'load', function ( e ) {
-                               var result = parseXHRResponse( e );
-
-                               if ( result.error || result.warnings ) {
-                                       if ( result.error && result.error.code === 'badtoken' ) {
-                                               api.badToken( 'edit' );
+                       // Use this.postWithEditToken() or this.post()
+                       this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
+                               // Use FormData (if we got here, we know that it's available)
+                               contentType: 'multipart/form-data',
+                               // Provide upload progress notifications
+                               xhr: function () {
+                                       var xhr = $.ajaxSettings.xhr();
+                                       if ( xhr.upload ) {
+                                               // need to bind this event before we open the connection (see note at
+                                               // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
+                                               xhr.upload.addEventListener( 'progress', function ( ev ) {
+                                                       if ( ev.lengthComputable ) {
+                                                               deferred.notify( ev.loaded / ev.total );
+                                                       }
+                                               } );
                                        }
-
-                                       deferred.reject( result.error || result.warnings );
-                               } else {
-                                       deferred.notify( 1 );
-                                       deferred.resolve( result );
+                                       return xhr;
                                }
-                       }, false );
-
-                       xhr.addEventListener( 'error', function ( e ) {
-                               deferred.reject( parseXHRResponse( e ) );
-                       }, false );
-
-                       xhr.open( 'POST', this.defaults.ajax.url, true );
-
-                       if ( this.needToken() ) {
-                               this.getEditToken().then( function ( token ) {
-                                       formData.append( 'token', token );
-                                       xhr.send( formData );
+                       } )
+                               .done( function ( result ) {
+                                       if ( result.error || result.warnings ) {
+                                               deferred.reject( result.error || result.warnings );
+                                       } else {
+                                               deferred.notify( 1 );
+                                               deferred.resolve( result );
+                                       }
+                               } )
+                               .fail( function ( result ) {
+                                       deferred.reject( result );
                                } );
-                       } else {
-                               xhr.send( formData );
-                       }
 
                        return deferred.promise();
                },