Merge "Drop zh-tw message "saveprefs""
[lhc/web/wiklou.git] / resources / src / mediawiki.api / mediawiki.api.upload.js
index cec352a..4abff28 100644 (file)
@@ -1,5 +1,6 @@
 /**
  * Provides an interface for uploading files to MediaWiki.
+ *
  * @class mw.Api.plugin.upload
  * @singleton
  */
@@ -18,6 +19,7 @@
        /**
         * @private
         * Get nonce for iframe IDs on the page.
+        *
         * @return {number}
         */
        function getNonce() {
@@ -27,6 +29,7 @@
        /**
         * @private
         * Get new iframe object for an upload.
+        *
         * @return {HTMLIframeElement}
         */
        function getNewIframe( id ) {
        /**
         * @private
         * Shortcut for getting hidden inputs
+        *
         * @return {jQuery}
         */
        function getHiddenInput( name, val ) {
-               return $( '<input type="hidden" />')
+               return $( '<input type="hidden" />' )
                        .attr( 'name', name )
                        .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.
@@ -81,7 +62,7 @@
         */
        function processIframeResult( iframe ) {
                var json,
-                       doc = iframe.contentDocument || frames[iframe.id].document;
+                       doc = iframe.contentDocument || frames[ iframe.id ].document;
 
                if ( doc.XMLDocument ) {
                        // The response is a document property in IE
        $.extend( mw.Api.prototype, {
                /**
                 * Upload a file to MediaWiki.
-                * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside of it, or a File object.
+                *
+                * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
+                * iframe if it doesn't.
+                *
+                * Caveats of iframe upload:
+                * - The returned jQuery.Promise will not receive `progress` notifications during the upload
+                * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
+                * - You must pass a HTMLInputElement and not a File for it to be possible
+                *
+                * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside
+                *     of it, or a File object.
                 * @param {Object} data Other upload options, see action=upload API docs for more
                 * @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 );
                        }
 
                 * APIs, and continues to work in browsers with those APIs.
                 *
                 * The rough sketch of how this method works is as follows:
-                * * An iframe is loaded with no content.
-                * * A form is submitted with the passed-in file input and some extras.
-                * * The MediaWiki API receives that form data, and sends back a response.
-                * * The response is sent to the iframe, because we set target=(iframe id)
-                * * The response is parsed out of the iframe's document, and passed back
-                *   through the promise.
+                * 1. An iframe is loaded with no content.
+                * 2. A form is submitted with the passed-in file input and some extras.
+                * 3. The MediaWiki API receives that form data, and sends back a response.
+                * 4. The response is sent to the iframe, because we set target=(iframe id)
+                * 5. The response is parsed out of the iframe's document, and passed back
+                *    through the promise.
+                *
+                * @private
                 * @param {HTMLInputElement} file The file input with a file in it.
                 * @param {Object} data Other upload options, see action=upload API docs for more
                 * @return {jQuery.Promise}
                                $iframe = $( iframe );
 
                        for ( key in data ) {
-                               if ( !fieldsAllowed[key] ) {
-                                       delete data[key];
+                               if ( !fieldsAllowed[ key ] ) {
+                                       delete data[ key ];
                                }
                        }
 
 
                /**
                 * Uploads a file using the FormData API.
+                *
+                * @private
                 * @param {File} file
-                * @param {Object} data
+                * @param {Object} data Other upload options, see action=upload API docs for more
+                * @return {jQuery.Promise}
                 */
                uploadWithFormData: function ( file, data ) {
-                       var key, xhr,
-                               api = this,
-                               formData = new FormData(),
+                       var key,
                                deferred = $.Deferred();
 
                        for ( key in data ) {
-                               if ( !fieldsAllowed[key] ) {
-                                       delete data[key];
+                               if ( !fieldsAllowed[ key ] ) {
+                                       delete data[ key ];
                                }
                        }
 
                        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();
                },
                 * This function will return a promise, which when resolved, will pass back a function
                 * to finish the stash upload. You can call that function with an argument containing
                 * more, or conflicting, data to pass to the server. For example:
+                *
                 *     // upload a file to the stash with a placeholder filename
                 *     api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
                 *         // finish is now the function we can use to finalize the upload
                 *             // the upload is complete, data holds the API response
                 *         } );
                 *     } );
+                *
                 * @param {File|HTMLInputElement} file
                 * @param {Object} [data]
                 * @return {jQuery.Promise}
                                        return $.Deferred().reject( 'Filename not included in file data.' );
                                }
 
-                               return api.postWithEditToken( data );
+                               return api.postWithEditToken( data ).then( function ( result ) {
+                                       if ( result.upload && ( result.upload.error || result.upload.warnings ) ) {
+                                               return $.Deferred().reject( result.upload.error || result.upload.warnings ).promise();
+                                       }
+                                       return result;
+                               } );
                        }
 
                        return this.upload( file, { stash: true, filename: data.filename } ).then( function ( result ) {