2 * Provides an interface for uploading files to MediaWiki.
3 * @class mw.Api.plugin.upload
20 * Get nonce for iframe IDs on the page.
29 * Get new iframe object for an upload.
30 * @return {HTMLIframeElement}
32 function getNewIframe( id
) {
33 var frame
= document
.createElement( 'iframe' );
41 * Shortcut for getting hidden inputs
44 function getHiddenInput( name
, val
) {
45 return $( '<input type="hidden" />')
51 * Parse response from an XHR to the server.
56 function parseXHRResponse( e
) {
60 response
= $.parseJSON( e
.target
.responseText
);
65 info
: e
.target
.responseText
74 * Process the result of the form submission, returned to an iframe.
75 * This is the iframe's onload event.
77 * @param {HTMLIframeElement} iframe Iframe to extract result from
78 * @return {Object} Response from the server. The return value may or may
79 * not be an XMLDocument, this code was copied from elsewhere, so if you
80 * see an unexpected return type, please file a bug.
82 function processIframeResult( iframe
) {
84 doc
= iframe
.contentDocument
|| frames
[iframe
.id
].document
;
86 if ( doc
.XMLDocument
) {
87 // The response is a document property in IE
88 return doc
.XMLDocument
;
92 // Get the json string
93 // We're actually searching through an HTML doc here --
94 // according to mdale we need to do this
95 // because IE does not load JSON properly in an iframe
96 json
= $( doc
.body
).find( 'pre' ).text();
98 return JSON
.parse( json
);
101 // Response is a xml document
105 function formDataAvailable() {
106 return window
.FormData
!== undefined &&
107 window
.File
!== undefined &&
108 window
.File
.prototype.slice
!== undefined;
111 $.extend( mw
.Api
.prototype, {
113 * Upload a file to MediaWiki.
114 * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside of it, or a File object.
115 * @param {Object} data Other upload options, see action=upload API docs for more
116 * @return {jQuery.Promise}
118 upload: function ( file
, data
) {
119 var iframe
, formData
;
122 return $.Deferred().reject( 'No file' );
125 iframe
= file
.nodeType
&& file
.nodeType
=== Node
.ELEMENT_NODE
;
126 formData
= formDataAvailable() && file
instanceof window
.File
;
128 if ( !iframe
&& !formData
) {
129 return $.Deferred().reject( 'Unsupported argument type passed to mw.Api.upload' );
133 return this.uploadWithFormData( file
, data
);
136 return this.uploadWithIframe( file
, data
);
140 * Upload a file to MediaWiki with an iframe and a form.
142 * This method is necessary for browsers without the File/FormData
143 * APIs, and continues to work in browsers with those APIs.
145 * The rough sketch of how this method works is as follows:
146 * * An iframe is loaded with no content.
147 * * A form is submitted with the passed-in file input and some extras.
148 * * The MediaWiki API receives that form data, and sends back a response.
149 * * The response is sent to the iframe, because we set target=(iframe id)
150 * * The response is parsed out of the iframe's document, and passed back
151 * through the promise.
152 * @param {HTMLInputElement} file The file input with a file in it.
153 * @param {Object} data Other upload options, see action=upload API docs for more
154 * @return {jQuery.Promise}
156 uploadWithIframe: function ( file
, data
) {
158 tokenPromise
= $.Deferred(),
160 deferred
= $.Deferred(),
162 id
= 'uploadframe-' + nonce
,
163 $form
= $( '<form>' ),
164 iframe
= getNewIframe( id
),
165 $iframe
= $( iframe
);
167 for ( key
in data
) {
168 if ( !fieldsAllowed
[key
] ) {
173 data
= $.extend( {}, this.defaults
.parameters
, { action
: 'upload' }, data
);
174 $form
.addClass( 'mw-api-upload-form' );
176 $form
.css( 'display', 'none' )
178 action
: this.defaults
.ajax
.url
,
181 enctype
: 'multipart/form-data'
184 $iframe
.one( 'load', function () {
185 $iframe
.one( 'load', function () {
186 var result
= processIframeResult( iframe
);
189 deferred
.reject( 'No response from API on upload attempt.' );
190 } else if ( result
.error
|| result
.warnings
) {
191 if ( result
.error
&& result
.error
.code
=== 'badtoken' ) {
192 api
.badToken( 'edit' );
195 deferred
.reject( result
.error
|| result
.warnings
);
197 deferred
.notify( 1 );
198 deferred
.resolve( result
);
201 tokenPromise
.done( function () {
206 $iframe
.error( function ( error
) {
207 deferred
.reject( 'iframe failed to load: ' + error
);
210 $iframe
.prop( 'src', 'about:blank' ).hide();
214 $.each( data
, function ( key
, val
) {
215 $form
.append( getHiddenInput( key
, val
) );
218 if ( !data
.filename
&& !data
.stash
) {
219 return $.Deferred().reject( 'Filename not included in file data.' );
222 if ( this.needToken() ) {
223 this.getEditToken().then( function ( token
) {
224 $form
.append( getHiddenInput( 'token', token
) );
225 tokenPromise
.resolve();
226 }, tokenPromise
.reject
);
228 tokenPromise
.resolve();
231 $( 'body' ).append( $form
, $iframe
);
233 deferred
.always( function () {
238 return deferred
.promise();
242 * Uploads a file using the FormData API.
244 * @param {Object} data
246 uploadWithFormData: function ( file
, data
) {
249 formData
= new FormData(),
250 deferred
= $.Deferred();
252 for ( key
in data
) {
253 if ( !fieldsAllowed
[key
] ) {
258 data
= $.extend( {}, this.defaults
.parameters
, { action
: 'upload' }, data
);
260 $.each( data
, function ( key
, val
) {
261 formData
.append( key
, val
);
264 if ( !data
.filename
&& !data
.stash
) {
265 return $.Deferred().reject( 'Filename not included in file data.' );
268 formData
.append( 'file', file
);
270 xhr
= new XMLHttpRequest();
272 xhr
.upload
.addEventListener( 'progress', function ( e
) {
273 if ( e
.lengthComputable
) {
274 deferred
.notify( e
.loaded
/ e
.total
);
278 xhr
.addEventListener( 'abort', function ( e
) {
279 deferred
.reject( parseXHRResponse( e
) );
282 xhr
.addEventListener( 'load', function ( e
) {
283 var result
= parseXHRResponse( e
);
285 if ( result
.error
|| result
.warnings
) {
286 if ( result
.error
&& result
.error
.code
=== 'badtoken' ) {
287 api
.badToken( 'edit' );
290 deferred
.reject( result
.error
|| result
.warnings
);
292 deferred
.notify( 1 );
293 deferred
.resolve( result
);
297 xhr
.addEventListener( 'error', function ( e
) {
298 deferred
.reject( parseXHRResponse( e
) );
301 xhr
.open( 'POST', this.defaults
.ajax
.url
, true );
303 if ( this.needToken() ) {
304 this.getEditToken().then( function ( token
) {
305 formData
.append( 'token', token
);
306 xhr
.send( formData
);
309 xhr
.send( formData
);
312 return deferred
.promise();
316 * Upload a file to the stash.
318 * This function will return a promise, which when resolved, will pass back a function
319 * to finish the stash upload. You can call that function with an argument containing
320 * more, or conflicting, data to pass to the server. For example:
321 * // upload a file to the stash with a placeholder filename
322 * api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
323 * // finish is now the function we can use to finalize the upload
324 * // pass it a new filename from user input to override the initial value
325 * finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
326 * // the upload is complete, data holds the API response
329 * @param {File|HTMLInputElement} file
330 * @param {Object} [data]
331 * @return {jQuery.Promise}
332 * @return {Function} return.finishStashUpload Call this function to finish the upload.
333 * @return {Object} return.finishStashUpload.data Additional data for the upload.
334 * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload
335 * @return {Object} return.finishStashUpload.return.data API return value for the final upload
337 uploadToStash: function ( file
, data
) {
341 if ( !data
.filename
) {
342 return $.Deferred().reject( 'Filename not included in file data.' );
345 function finishUpload( moreData
) {
346 data
= $.extend( data
, moreData
);
347 data
.filekey
= filekey
;
348 data
.action
= 'upload';
349 data
.format
= 'json';
351 if ( !data
.filename
) {
352 return $.Deferred().reject( 'Filename not included in file data.' );
355 return api
.postWithEditToken( data
);
358 return this.upload( file
, { stash
: true, filename
: data
.filename
} ).then( function ( result
) {
359 if ( result
&& result
.upload
&& result
.upload
.filekey
) {
360 filekey
= result
.upload
.filekey
;
361 } else if ( result
&& ( result
.error
|| result
.warning
) ) {
362 return $.Deferred().reject( result
);
369 needToken: function () {
376 * @mixins mw.Api.plugin.upload
378 }( mediaWiki
, jQuery
) );