2 * Provides an interface for uploading files to MediaWiki.
4 * @class mw.Api.plugin.upload
21 * Get nonce for iframe IDs on the page.
31 * Get new iframe object for an upload.
33 * @return {HTMLIframeElement}
35 function getNewIframe( id
) {
36 var frame
= document
.createElement( 'iframe' );
44 * Shortcut for getting hidden inputs
48 function getHiddenInput( name
, val
) {
49 return $( '<input type="hidden" />' )
55 * Process the result of the form submission, returned to an iframe.
56 * This is the iframe's onload event.
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.
63 function processIframeResult( iframe
) {
65 doc
= iframe
.contentDocument
|| frames
[ iframe
.id
].document
;
67 if ( doc
.XMLDocument
) {
68 // The response is a document property in IE
69 return doc
.XMLDocument
;
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();
79 return JSON
.parse( json
);
82 // Response is a xml document
86 function formDataAvailable() {
87 return window
.FormData
!== undefined &&
88 window
.File
!== undefined &&
89 window
.File
.prototype.slice
!== undefined;
92 $.extend( mw
.Api
.prototype, {
94 * Upload a file to MediaWiki.
96 * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
97 * iframe if it doesn't.
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
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}
109 upload: function ( file
, data
) {
110 var isFileInput
, canUseFormData
;
112 isFileInput
= file
&& file
.nodeType
=== Node
.ELEMENT_NODE
;
114 if ( formDataAvailable() && isFileInput
&& file
.files
) {
115 file
= file
.files
[ 0 ];
119 return $.Deferred().reject( 'No file' );
122 canUseFormData
= formDataAvailable() && file
instanceof window
.File
;
124 if ( !isFileInput
&& !canUseFormData
) {
125 return $.Deferred().reject( 'Unsupported argument type passed to mw.Api.upload' );
128 if ( canUseFormData
) {
129 return this.uploadWithFormData( file
, data
);
132 return this.uploadWithIframe( file
, data
);
136 * Upload a file to MediaWiki with an iframe and a form.
138 * This method is necessary for browsers without the File/FormData
139 * APIs, and continues to work in browsers with those APIs.
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.
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}
154 uploadWithIframe: function ( file
, data
) {
156 tokenPromise
= $.Deferred(),
158 deferred
= $.Deferred(),
160 id
= 'uploadframe-' + nonce
,
161 $form
= $( '<form>' ),
162 iframe
= getNewIframe( id
),
163 $iframe
= $( iframe
);
165 for ( key
in data
) {
166 if ( !fieldsAllowed
[ key
] ) {
171 data
= $.extend( {}, this.defaults
.parameters
, { action
: 'upload' }, data
);
172 $form
.addClass( 'mw-api-upload-form' );
174 $form
.css( 'display', 'none' )
176 action
: this.defaults
.ajax
.url
,
179 enctype
: 'multipart/form-data'
182 $iframe
.one( 'load', function () {
183 $iframe
.one( 'load', function () {
184 var result
= processIframeResult( iframe
);
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' );
193 deferred
.reject( result
.error
|| result
.warnings
);
195 deferred
.notify( 1 );
196 deferred
.resolve( result
);
199 tokenPromise
.done( function () {
204 $iframe
.error( function ( error
) {
205 deferred
.reject( 'iframe failed to load: ' + error
);
208 $iframe
.prop( 'src', 'about:blank' ).hide();
212 $.each( data
, function ( key
, val
) {
213 $form
.append( getHiddenInput( key
, val
) );
216 if ( !data
.filename
&& !data
.stash
) {
217 return $.Deferred().reject( 'Filename not included in file data.' );
220 if ( this.needToken() ) {
221 this.getEditToken().then( function ( token
) {
222 $form
.append( getHiddenInput( 'token', token
) );
223 tokenPromise
.resolve();
224 }, tokenPromise
.reject
);
226 tokenPromise
.resolve();
229 $( 'body' ).append( $form
, $iframe
);
231 deferred
.always( function () {
236 return deferred
.promise();
240 * Uploads a file using the FormData API.
244 * @param {Object} data Other upload options, see action=upload API docs for more
245 * @return {jQuery.Promise}
247 uploadWithFormData: function ( file
, data
) {
249 deferred
= $.Deferred();
251 for ( key
in data
) {
252 if ( !fieldsAllowed
[ key
] ) {
257 data
= $.extend( {}, this.defaults
.parameters
, { action
: 'upload' }, data
);
260 if ( !data
.filename
&& !data
.stash
) {
261 return $.Deferred().reject( 'Filename not included in file data.' );
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
270 var xhr
= $.ajaxSettings
.xhr();
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
);
283 .done( function ( result
) {
284 if ( result
.error
|| result
.warnings
) {
285 deferred
.reject( result
.error
|| result
.warnings
);
287 deferred
.notify( 1 );
288 deferred
.resolve( result
);
291 .fail( function ( result
) {
292 deferred
.reject( result
);
295 return deferred
.promise();
299 * Upload a file to the stash.
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:
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
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
322 uploadToStash: function ( file
, data
) {
326 if ( !data
.filename
) {
327 return $.Deferred().reject( 'Filename not included in file data.' );
330 function finishUpload( moreData
) {
331 data
= $.extend( data
, moreData
);
332 data
.filekey
= filekey
;
333 data
.action
= 'upload';
334 data
.format
= 'json';
336 if ( !data
.filename
) {
337 return $.Deferred().reject( 'Filename not included in file data.' );
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();
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
);
359 needToken: function () {
366 * @mixins mw.Api.plugin.upload
368 }( mediaWiki
, jQuery
) );