19d25423ffe2b4adde806d22f5d88055b8082b16
[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 * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside of it, or a File object.
92 * @param {Object} data Other upload options, see action=upload API docs for more
93 * @return {jQuery.Promise}
94 */
95 upload: function ( file, data ) {
96 var isFileInput, canUseFormData;
97
98 isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
99
100 if ( formDataAvailable() && isFileInput && file.files ) {
101 file = file.files[0];
102 }
103
104 if ( !file ) {
105 return $.Deferred().reject( 'No file' );
106 }
107
108 canUseFormData = formDataAvailable() && file instanceof window.File;
109
110 if ( !isFileInput && !canUseFormData ) {
111 return $.Deferred().reject( 'Unsupported argument type passed to mw.Api.upload' );
112 }
113
114 if ( canUseFormData ) {
115 return this.uploadWithFormData( file, data );
116 }
117
118 return this.uploadWithIframe( file, data );
119 },
120
121 /**
122 * Upload a file to MediaWiki with an iframe and a form.
123 *
124 * This method is necessary for browsers without the File/FormData
125 * APIs, and continues to work in browsers with those APIs.
126 *
127 * The rough sketch of how this method works is as follows:
128 * * An iframe is loaded with no content.
129 * * A form is submitted with the passed-in file input and some extras.
130 * * The MediaWiki API receives that form data, and sends back a response.
131 * * The response is sent to the iframe, because we set target=(iframe id)
132 * * The response is parsed out of the iframe's document, and passed back
133 * through the promise.
134 * @param {HTMLInputElement} file The file input with a file in it.
135 * @param {Object} data Other upload options, see action=upload API docs for more
136 * @return {jQuery.Promise}
137 */
138 uploadWithIframe: function ( file, data ) {
139 var key,
140 tokenPromise = $.Deferred(),
141 api = this,
142 deferred = $.Deferred(),
143 nonce = getNonce(),
144 id = 'uploadframe-' + nonce,
145 $form = $( '<form>' ),
146 iframe = getNewIframe( id ),
147 $iframe = $( iframe );
148
149 for ( key in data ) {
150 if ( !fieldsAllowed[key] ) {
151 delete data[key];
152 }
153 }
154
155 data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
156 $form.addClass( 'mw-api-upload-form' );
157
158 $form.css( 'display', 'none' )
159 .attr( {
160 action: this.defaults.ajax.url,
161 method: 'POST',
162 target: id,
163 enctype: 'multipart/form-data'
164 } );
165
166 $iframe.one( 'load', function () {
167 $iframe.one( 'load', function () {
168 var result = processIframeResult( iframe );
169
170 if ( !result ) {
171 deferred.reject( 'No response from API on upload attempt.' );
172 } else if ( result.error || result.warnings ) {
173 if ( result.error && result.error.code === 'badtoken' ) {
174 api.badToken( 'edit' );
175 }
176
177 deferred.reject( result.error || result.warnings );
178 } else {
179 deferred.notify( 1 );
180 deferred.resolve( result );
181 }
182 } );
183 tokenPromise.done( function () {
184 $form.submit();
185 } );
186 } );
187
188 $iframe.error( function ( error ) {
189 deferred.reject( 'iframe failed to load: ' + error );
190 } );
191
192 $iframe.prop( 'src', 'about:blank' ).hide();
193
194 file.name = 'file';
195
196 $.each( data, function ( key, val ) {
197 $form.append( getHiddenInput( key, val ) );
198 } );
199
200 if ( !data.filename && !data.stash ) {
201 return $.Deferred().reject( 'Filename not included in file data.' );
202 }
203
204 if ( this.needToken() ) {
205 this.getEditToken().then( function ( token ) {
206 $form.append( getHiddenInput( 'token', token ) );
207 tokenPromise.resolve();
208 }, tokenPromise.reject );
209 } else {
210 tokenPromise.resolve();
211 }
212
213 $( 'body' ).append( $form, $iframe );
214
215 deferred.always( function () {
216 $form.remove();
217 $iframe.remove();
218 } );
219
220 return deferred.promise();
221 },
222
223 /**
224 * Uploads a file using the FormData API.
225 * @param {File} file
226 * @param {Object} data
227 * @return {jQuery.Promise}
228 */
229 uploadWithFormData: function ( file, data ) {
230 var key,
231 deferred = $.Deferred();
232
233 for ( key in data ) {
234 if ( !fieldsAllowed[key] ) {
235 delete data[key];
236 }
237 }
238
239 data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
240 data.file = file;
241
242 if ( !data.filename && !data.stash ) {
243 return $.Deferred().reject( 'Filename not included in file data.' );
244 }
245
246 // Use this.postWithEditToken() or this.post()
247 this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
248 // Use FormData (if we got here, we know that it's available)
249 contentType: 'multipart/form-data',
250 // Provide upload progress notifications
251 xhr: function () {
252 var xhr = $.ajaxSettings.xhr();
253 if ( xhr.upload ) {
254 // need to bind this event before we open the connection (see note at
255 // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
256 xhr.upload.addEventListener( 'progress', function ( ev ) {
257 if ( ev.lengthComputable ) {
258 deferred.notify( ev.loaded / ev.total );
259 }
260 } );
261 }
262 return xhr;
263 }
264 } )
265 .done( function ( result ) {
266 if ( result.error || result.warnings ) {
267 deferred.reject( result.error || result.warnings );
268 } else {
269 deferred.notify( 1 );
270 deferred.resolve( result );
271 }
272 } )
273 .fail( function ( result ) {
274 deferred.reject( result );
275 } );
276
277 return deferred.promise();
278 },
279
280 /**
281 * Upload a file to the stash.
282 *
283 * This function will return a promise, which when resolved, will pass back a function
284 * to finish the stash upload. You can call that function with an argument containing
285 * more, or conflicting, data to pass to the server. For example:
286 * // upload a file to the stash with a placeholder filename
287 * api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
288 * // finish is now the function we can use to finalize the upload
289 * // pass it a new filename from user input to override the initial value
290 * finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
291 * // the upload is complete, data holds the API response
292 * } );
293 * } );
294 * @param {File|HTMLInputElement} file
295 * @param {Object} [data]
296 * @return {jQuery.Promise}
297 * @return {Function} return.finishStashUpload Call this function to finish the upload.
298 * @return {Object} return.finishStashUpload.data Additional data for the upload.
299 * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload
300 * @return {Object} return.finishStashUpload.return.data API return value for the final upload
301 */
302 uploadToStash: function ( file, data ) {
303 var filekey,
304 api = this;
305
306 if ( !data.filename ) {
307 return $.Deferred().reject( 'Filename not included in file data.' );
308 }
309
310 function finishUpload( moreData ) {
311 data = $.extend( data, moreData );
312 data.filekey = filekey;
313 data.action = 'upload';
314 data.format = 'json';
315
316 if ( !data.filename ) {
317 return $.Deferred().reject( 'Filename not included in file data.' );
318 }
319
320 return api.postWithEditToken( data );
321 }
322
323 return this.upload( file, { stash: true, filename: data.filename } ).then( function ( result ) {
324 if ( result && result.upload && result.upload.filekey ) {
325 filekey = result.upload.filekey;
326 } else if ( result && ( result.error || result.warning ) ) {
327 return $.Deferred().reject( result );
328 }
329
330 return finishUpload;
331 } );
332 },
333
334 needToken: function () {
335 return true;
336 }
337 } );
338
339 /**
340 * @class mw.Api
341 * @mixins mw.Api.plugin.upload
342 */
343 }( mediaWiki, jQuery ) );