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
=== file
.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
) {
157 var tokenPromise
= $.Deferred(),
159 filenameFound
= false,
160 deferred
= $.Deferred(),
162 id
= 'uploadframe-' + nonce
,
163 $form
= $( '<form>' ),
164 iframe
= getNewIframe( id
),
165 $iframe
= $( iframe
);
167 $form
.addClass( 'mw-api-upload-form' );
170 getHiddenInput( 'action', 'upload' ),
171 getHiddenInput( 'format', 'json' ),
175 $form
.css( 'display', 'none' )
177 action
: this.defaults
.ajax
.url
,
180 enctype
: 'multipart/form-data'
183 $iframe
.one( 'load', function () {
184 $iframe
.one( 'load', function () {
185 var result
= processIframeResult( iframe
);
188 deferred
.reject( 'No response from API on upload attempt.' );
189 } else if ( result
.error
|| result
.warnings
) {
190 if ( result
.error
&& result
.error
.code
=== 'badtoken' ) {
191 api
.badToken( 'edit' );
194 deferred
.reject( result
.error
|| result
.warnings
);
196 deferred
.notify( 1 );
197 deferred
.resolve( result
);
200 tokenPromise
.done( function () {
205 $iframe
.error( function ( error
) {
206 deferred
.reject( 'iframe failed to load: ' + error
);
209 $iframe
.prop( 'src', 'about:blank' ).hide();
213 $.each( data
, function ( key
, val
) {
214 if ( key
=== 'filename' ) {
215 filenameFound
= true;
218 if ( fieldsAllowed
[key
] === true ) {
219 $form
.append( getHiddenInput( key
, val
) );
223 if ( !filenameFound
&& !data
.stash
) {
224 return $.Deferred().reject( 'Filename not included in file data.' );
227 if ( this.needToken() ) {
228 this.getEditToken().then( function ( token
) {
229 $form
.append( getHiddenInput( 'token', token
) );
230 tokenPromise
.resolve();
231 }, tokenPromise
.reject
);
233 tokenPromise
.resolve();
236 $( 'body' ).append( $form
, $iframe
);
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(),
251 filenameFound
= false;
253 formData
.append( 'action', 'upload' );
254 formData
.append( 'format', 'json' );
256 $.each( data
, function ( key
, val
) {
257 if ( key
=== 'filename' ) {
258 filenameFound
= true;
261 if ( fieldsAllowed
[key
] === true ) {
262 formData
.append( key
, val
);
266 if ( !filenameFound
&& !data
.stash
) {
267 return $.Deferred().reject( 'Filename not included in file data.' );
270 formData
.append( 'file', file
);
272 xhr
= new XMLHttpRequest();
274 xhr
.upload
.addEventListener( 'progress', function ( e
) {
275 if ( e
.lengthComputable
) {
276 deferred
.notify( e
.loaded
/ e
.total
);
280 xhr
.addEventListener( 'abort', function ( e
) {
281 deferred
.reject( parseXHRResponse( e
) );
284 xhr
.addEventListener( 'load', function ( e
) {
285 deferred
.resolve( parseXHRResponse( e
) );
288 xhr
.addEventListener( 'error', function ( e
) {
289 deferred
.reject( parseXHRResponse( e
) );
292 xhr
.open( 'POST', this.defaults
.ajax
.url
, true );
294 if ( this.needToken() ) {
295 this.getEditToken().then( function ( token
) {
296 formData
.append( 'token', token
);
297 xhr
.send( formData
);
299 // Mark the edit token as bad, it's been used.
300 api
.badToken( 'edit' );
303 xhr
.send( formData
);
306 return deferred
.promise();
310 * Upload a file to the stash.
312 * This function will return a promise, which when resolved, will pass back a function
313 * to finish the stash upload. You can call that function with an argument containing
314 * more, or conflicting, data to pass to the server. For example:
315 * // upload a file to the stash with a placeholder filename
316 * api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
317 * // finish is now the function we can use to finalize the upload
318 * // pass it a new filename from user input to override the initial value
319 * finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
320 * // the upload is complete, data holds the API response
323 * @param {File|HTMLInputElement} file
324 * @param {Object} [data]
325 * @return {jQuery.Promise}
326 * @return {Function} return.finishStashUpload Call this function to finish the upload.
327 * @return {Object} return.finishStashUpload.data Additional data for the upload.
328 * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload
329 * @return {Object} return.finishStashUpload.return.data API return value for the final upload
331 uploadToStash: function ( file
, data
) {
335 if ( !data
.filename
) {
336 return $.Deferred().reject( 'Filename not included in file data.' );
339 function finishUpload( moreData
) {
340 data
= $.extend( data
, moreData
);
341 data
.filekey
= filekey
;
342 data
.action
= 'upload';
343 data
.format
= 'json';
345 if ( !data
.filename
) {
346 return $.Deferred().reject( 'Filename not included in file data.' );
349 return api
.postWithEditToken( data
);
352 return this.upload( file
, { stash
: true, filename
: data
.filename
} ).then( function ( result
) {
353 if ( result
&& result
.upload
&& result
.upload
.filekey
) {
354 filekey
= result
.upload
.filekey
;
355 } else if ( result
&& ( result
.error
|| result
.warning
) ) {
356 return $.Deferred().reject( result
);
363 needToken: function () {
370 * @mixins mw.Api.plugin.upload
372 }( mediaWiki
, jQuery
) );