Merge "Remove not used private member variable mParserWarnings from OutputPage"
[mediawiki.git] / resources / src / mediawiki / api / upload.js
blob614c00119e945c951acac1295e6fadc95da012ac
1 /**
2  * Provides an interface for uploading files to MediaWiki.
3  *
4  * @class mw.Api.plugin.upload
5  * @singleton
6  */
7 ( function ( mw, $ ) {
8         var nonce = 0,
9                 fieldsAllowed = {
10                         stash: true,
11                         filekey: true,
12                         filename: true,
13                         comment: true,
14                         text: true,
15                         watchlist: true,
16                         ignorewarnings: true
17                 };
19         /**
20          * @private
21          * Get nonce for iframe IDs on the page.
22          *
23          * @return {number}
24          */
25         function getNonce() {
26                 return nonce++;
27         }
29         /**
30          * @private
31          * Given a non-empty object, return one of its keys.
32          *
33          * @param {Object} obj
34          * @return {string}
35          */
36         function getFirstKey( obj ) {
37                 for ( var key in obj ) {
38                         if ( obj.hasOwnProperty( key ) ) {
39                                 return key;
40                         }
41                 }
42         }
44         /**
45          * @private
46          * Get new iframe object for an upload.
47          *
48          * @return {HTMLIframeElement}
49          */
50         function getNewIframe( id ) {
51                 var frame = document.createElement( 'iframe' );
52                 frame.id = id;
53                 frame.name = id;
54                 return frame;
55         }
57         /**
58          * @private
59          * Shortcut for getting hidden inputs
60          *
61          * @return {jQuery}
62          */
63         function getHiddenInput( name, val ) {
64                 return $( '<input type="hidden" />' )
65                         .attr( 'name', name )
66                         .val( val );
67         }
69         /**
70          * Process the result of the form submission, returned to an iframe.
71          * This is the iframe's onload event.
72          *
73          * @param {HTMLIframeElement} iframe Iframe to extract result from
74          * @return {Object} Response from the server. The return value may or may
75          *   not be an XMLDocument, this code was copied from elsewhere, so if you
76          *   see an unexpected return type, please file a bug.
77          */
78         function processIframeResult( iframe ) {
79                 var json,
80                         doc = iframe.contentDocument || frames[ iframe.id ].document;
82                 if ( doc.XMLDocument ) {
83                         // The response is a document property in IE
84                         return doc.XMLDocument;
85                 }
87                 if ( doc.body ) {
88                         // Get the json string
89                         // We're actually searching through an HTML doc here --
90                         // according to mdale we need to do this
91                         // because IE does not load JSON properly in an iframe
92                         json = $( doc.body ).find( 'pre' ).text();
94                         return JSON.parse( json );
95                 }
97                 // Response is a xml document
98                 return doc;
99         }
101         function formDataAvailable() {
102                 return window.FormData !== undefined &&
103                         window.File !== undefined &&
104                         window.File.prototype.slice !== undefined;
105         }
107         $.extend( mw.Api.prototype, {
108                 /**
109                  * Upload a file to MediaWiki.
110                  *
111                  * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
112                  * iframe if it doesn't.
113                  *
114                  * Caveats of iframe upload:
115                  * - The returned jQuery.Promise will not receive `progress` notifications during the upload
116                  * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
117                  * - You must pass a HTMLInputElement and not a File for it to be possible
118                  *
119                  * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside
120                  *  of it, or a File object.
121                  * @param {Object} data Other upload options, see action=upload API docs for more
122                  * @return {jQuery.Promise}
123                  */
124                 upload: function ( file, data ) {
125                         var isFileInput, canUseFormData;
127                         isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
129                         if ( formDataAvailable() && isFileInput && file.files ) {
130                                 file = file.files[ 0 ];
131                         }
133                         if ( !file ) {
134                                 throw new Error( 'No file' );
135                         }
137                         canUseFormData = formDataAvailable() && file instanceof window.File;
139                         if ( !isFileInput && !canUseFormData ) {
140                                 throw new Error( 'Unsupported argument type passed to mw.Api.upload' );
141                         }
143                         if ( canUseFormData ) {
144                                 return this.uploadWithFormData( file, data );
145                         }
147                         return this.uploadWithIframe( file, data );
148                 },
150                 /**
151                  * Upload a file to MediaWiki with an iframe and a form.
152                  *
153                  * This method is necessary for browsers without the File/FormData
154                  * APIs, and continues to work in browsers with those APIs.
155                  *
156                  * The rough sketch of how this method works is as follows:
157                  * 1. An iframe is loaded with no content.
158                  * 2. A form is submitted with the passed-in file input and some extras.
159                  * 3. The MediaWiki API receives that form data, and sends back a response.
160                  * 4. The response is sent to the iframe, because we set target=(iframe id)
161                  * 5. The response is parsed out of the iframe's document, and passed back
162                  *    through the promise.
163                  *
164                  * @private
165                  * @param {HTMLInputElement} file The file input with a file in it.
166                  * @param {Object} data Other upload options, see action=upload API docs for more
167                  * @return {jQuery.Promise}
168                  */
169                 uploadWithIframe: function ( file, data ) {
170                         var key,
171                                 tokenPromise = $.Deferred(),
172                                 api = this,
173                                 deferred = $.Deferred(),
174                                 nonce = getNonce(),
175                                 id = 'uploadframe-' + nonce,
176                                 $form = $( '<form>' ),
177                                 iframe = getNewIframe( id ),
178                                 $iframe = $( iframe );
180                         for ( key in data ) {
181                                 if ( !fieldsAllowed[ key ] ) {
182                                         delete data[ key ];
183                                 }
184                         }
186                         data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
187                         $form.addClass( 'mw-api-upload-form' );
189                         $form.css( 'display', 'none' )
190                                 .attr( {
191                                         action: this.defaults.ajax.url,
192                                         method: 'POST',
193                                         target: id,
194                                         enctype: 'multipart/form-data'
195                                 } );
197                         $iframe.one( 'load', function () {
198                                 $iframe.one( 'load', function () {
199                                         var result = processIframeResult( iframe );
200                                         deferred.notify( 1 );
202                                         if ( !result ) {
203                                                 deferred.reject( 'ok-but-empty', 'No response from API on upload attempt.' );
204                                         } else if ( result.error ) {
205                                                 if ( result.error.code === 'badtoken' ) {
206                                                         api.badToken( 'edit' );
207                                                 }
209                                                 deferred.reject( result.error.code, result );
210                                         } else if ( result.upload && result.upload.warnings ) {
211                                                 deferred.reject( getFirstKey( result.upload.warnings ), result );
212                                         } else {
213                                                 deferred.resolve( result );
214                                         }
215                                 } );
216                                 tokenPromise.done( function () {
217                                         $form.submit();
218                                 } );
219                         } );
221                         $iframe.error( function ( error ) {
222                                 deferred.reject( 'http', error );
223                         } );
225                         $iframe.prop( 'src', 'about:blank' ).hide();
227                         file.name = 'file';
229                         $.each( data, function ( key, val ) {
230                                 $form.append( getHiddenInput( key, val ) );
231                         } );
233                         if ( !data.filename && !data.stash ) {
234                                 throw new Error( 'Filename not included in file data.' );
235                         }
237                         if ( this.needToken() ) {
238                                 this.getEditToken().then( function ( token ) {
239                                         $form.append( getHiddenInput( 'token', token ) );
240                                         tokenPromise.resolve();
241                                 }, tokenPromise.reject );
242                         } else {
243                                 tokenPromise.resolve();
244                         }
246                         $( 'body' ).append( $form, $iframe );
248                         deferred.always( function () {
249                                 $form.remove();
250                                 $iframe.remove();
251                         } );
253                         return deferred.promise();
254                 },
256                 /**
257                  * Uploads a file using the FormData API.
258                  *
259                  * @private
260                  * @param {File} file
261                  * @param {Object} data Other upload options, see action=upload API docs for more
262                  * @return {jQuery.Promise}
263                  */
264                 uploadWithFormData: function ( file, data ) {
265                         var key,
266                                 deferred = $.Deferred();
268                         for ( key in data ) {
269                                 if ( !fieldsAllowed[ key ] ) {
270                                         delete data[ key ];
271                                 }
272                         }
274                         data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
275                         data.file = file;
277                         if ( !data.filename && !data.stash ) {
278                                 throw new Error( 'Filename not included in file data.' );
279                         }
281                         // Use this.postWithEditToken() or this.post()
282                         this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
283                                 // Use FormData (if we got here, we know that it's available)
284                                 contentType: 'multipart/form-data',
285                                 // Provide upload progress notifications
286                                 xhr: function () {
287                                         var xhr = $.ajaxSettings.xhr();
288                                         if ( xhr.upload ) {
289                                                 // need to bind this event before we open the connection (see note at
290                                                 // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
291                                                 xhr.upload.addEventListener( 'progress', function ( ev ) {
292                                                         if ( ev.lengthComputable ) {
293                                                                 deferred.notify( ev.loaded / ev.total );
294                                                         }
295                                                 } );
296                                         }
297                                         return xhr;
298                                 }
299                         } )
300                                 .done( function ( result ) {
301                                         deferred.notify( 1 );
302                                         if ( result.upload && result.upload.warnings ) {
303                                                 deferred.reject( getFirstKey( result.upload.warnings ), result );
304                                         } else {
305                                                 deferred.resolve( result );
306                                         }
307                                 } )
308                                 .fail( function ( errorCode, result ) {
309                                         deferred.notify( 1 );
310                                         deferred.reject( errorCode, result );
311                                 } );
313                         return deferred.promise();
314                 },
316                 /**
317                  * Upload a file to the stash.
318                  *
319                  * This function will return a promise, which when resolved, will pass back a function
320                  * to finish the stash upload. You can call that function with an argument containing
321                  * more, or conflicting, data to pass to the server. For example:
322                  *
323                  *     // upload a file to the stash with a placeholder filename
324                  *     api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
325                  *         // finish is now the function we can use to finalize the upload
326                  *         // pass it a new filename from user input to override the initial value
327                  *         finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
328                  *             // the upload is complete, data holds the API response
329                  *         } );
330                  *     } );
331                  *
332                  * @param {File|HTMLInputElement} file
333                  * @param {Object} [data]
334                  * @return {jQuery.Promise}
335                  * @return {Function} return.finishStashUpload Call this function to finish the upload.
336                  * @return {Object} return.finishStashUpload.data Additional data for the upload.
337                  * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload
338                  * @return {Object} return.finishStashUpload.return.data API return value for the final upload
339                  */
340                 uploadToStash: function ( file, data ) {
341                         var filekey,
342                                 api = this;
344                         if ( !data.filename ) {
345                                 throw new Error( 'Filename not included in file data.' );
346                         }
348                         function finishUpload( moreData ) {
349                                 data = $.extend( data, moreData );
350                                 data.filekey = filekey;
351                                 data.action = 'upload';
352                                 data.format = 'json';
354                                 if ( !data.filename ) {
355                                         throw new Error( 'Filename not included in file data.' );
356                                 }
358                                 return api.postWithEditToken( data ).then( function ( result ) {
359                                         if ( result.upload && result.upload.warnings ) {
360                                                 return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
361                                         }
362                                         return result;
363                                 } );
364                         }
366                         return this.upload( file, { stash: true, filename: data.filename } ).then(
367                                 function ( result ) {
368                                         filekey = result.upload.filekey;
369                                         return finishUpload;
370                                 },
371                                 function ( errorCode, result ) {
372                                         if ( result && result.upload && result.upload.filekey ) {
373                                                 // Ignore any warnings if 'filekey' was returned, that's all we care about
374                                                 filekey = result.upload.filekey;
375                                                 return $.Deferred().resolve( finishUpload );
376                                         }
377                                         return $.Deferred().reject( errorCode, result );
378                                 }
379                         );
380                 },
382                 needToken: function () {
383                         return true;
384                 }
385         } );
387         /**
388          * @class mw.Api
389          * @mixins mw.Api.plugin.upload
390          */
391 }( mediaWiki, jQuery ) );