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