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