b03e52c9bd66a7cc946235bc4fd1cc0b5f122dec
[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 filename: true,
10 comment: true,
11 text: true,
12 watchlist: true,
13 ignorewarnings: true
14 };
15
16 /**
17 * @private
18 * Get nonce for iframe IDs on the page.
19 * @return {number}
20 */
21 function getNonce() {
22 return nonce++;
23 }
24
25 /**
26 * @private
27 * Get new iframe object for an upload.
28 * @return {HTMLIframeElement}
29 */
30 function getNewIframe( id ) {
31 var frame = document.createElement( 'iframe' );
32 frame.id = id;
33 frame.name = id;
34 return frame;
35 }
36
37 /**
38 * @private
39 * Shortcut for getting hidden inputs
40 * @return {jQuery}
41 */
42 function getHiddenInput( name, val ) {
43 return $( '<input type="hidden" />')
44 .attr( 'name', name )
45 .val( val );
46 }
47
48 /**
49 * Parse response from an XHR to the server.
50 * @private
51 * @param {Event} e
52 * @return {Object}
53 */
54 function parseXHRResponse( e ) {
55 var response;
56
57 try {
58 response = $.parseJSON( e.target.responseText );
59 } catch ( error ) {
60 response = {
61 error: {
62 code: e.target.code,
63 info: e.target.responseText
64 }
65 };
66 }
67
68 return response;
69 }
70
71 /**
72 * Process the result of the form submission, returned to an iframe.
73 * This is the iframe's onload event.
74 *
75 * @param {HTMLIframeElement} iframe Iframe to extract result from
76 * @return {Object} Response from the server. The return value may or may
77 * not be an XMLDocument, this code was copied from elsewhere, so if you
78 * see an unexpected return type, please file a bug.
79 */
80 function processIframeResult( iframe ) {
81 var json,
82 doc = iframe.contentDocument || frames[iframe.id].document;
83
84 if ( doc.XMLDocument ) {
85 // The response is a document property in IE
86 return doc.XMLDocument;
87 }
88
89 if ( doc.body ) {
90 // Get the json string
91 // We're actually searching through an HTML doc here --
92 // according to mdale we need to do this
93 // because IE does not load JSON properly in an iframe
94 json = $( doc.body ).find( 'pre' ).text();
95
96 return JSON.parse( json );
97 }
98
99 // Response is a xml document
100 return doc;
101 }
102
103 function formDataAvailable() {
104 return window.FormData !== undefined &&
105 window.File !== undefined &&
106 window.File.prototype.slice !== undefined;
107 }
108
109 $.extend( mw.Api.prototype, {
110 /**
111 * Upload a file to MediaWiki.
112 * @param {HTMLInputElement|File} file HTML input type=file element with a file already inside of it, or a File object.
113 * @param {Object} data Other upload options, see action=upload API docs for more
114 * @return {jQuery.Promise}
115 */
116 upload: function ( file, data ) {
117 var iframe, formData;
118
119 if ( !file ) {
120 return $.Deferred().reject( 'No file' );
121 }
122
123 iframe = file.nodeType && file.nodeType === file.ELEMENT_NODE;
124 formData = formDataAvailable() && file instanceof window.File;
125
126 if ( !iframe && !formData ) {
127 return $.Deferred().reject( 'Unsupported argument type passed to mw.Api.upload' );
128 }
129
130 if ( formData ) {
131 return this.uploadWithFormData( file, data );
132 }
133
134 return this.uploadWithIframe( file, data );
135 },
136
137 /**
138 * Upload a file to MediaWiki with an iframe and a form.
139 *
140 * This method is necessary for browsers without the File/FormData
141 * APIs, and continues to work in browsers with those APIs.
142 *
143 * The rough sketch of how this method works is as follows:
144 * * An iframe is loaded with no content.
145 * * A form is submitted with the passed-in file input and some extras.
146 * * The MediaWiki API receives that form data, and sends back a response.
147 * * The response is sent to the iframe, because we set target=(iframe id)
148 * * The response is parsed out of the iframe's document, and passed back
149 * 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}
153 */
154 uploadWithIframe: function ( file, data ) {
155 var tokenPromise,
156 api = this,
157 filenameFound = false,
158 deferred = $.Deferred(),
159 nonce = getNonce(),
160 id = 'uploadframe-' + nonce,
161 $form = $( '<form>' ),
162 iframe = getNewIframe( id ),
163 $iframe = $( iframe );
164
165 $form.addClass( 'mw-api-upload-form' );
166
167 $form.append(
168 getHiddenInput( 'action', 'upload' ),
169 getHiddenInput( 'format', 'json' ),
170 file
171 );
172
173 $form.css( 'display', 'none' )
174 .attr( {
175 action: this.defaults.ajax.url,
176 method: 'POST',
177 target: id,
178 enctype: 'multipart/form-data'
179 } );
180
181 $iframe.one( 'load', function () {
182 $iframe.one( 'load', function () {
183 var result = processIframeResult( iframe );
184
185 if ( !result ) {
186 deferred.reject( 'No response from API on upload attempt.' );
187 } else if ( result.error || result.warnings ) {
188 if ( result.error && result.error.code === 'badtoken' ) {
189 api.badToken( 'edit' );
190 }
191
192 deferred.reject( result.error || result.warnings );
193 } else {
194 deferred.notify( 1 );
195 deferred.resolve( result );
196 }
197 } );
198 tokenPromise.done( function () {
199 $form.submit();
200 } );
201 } );
202
203 $iframe.error( function ( error ) {
204 deferred.reject( 'iframe failed to load: ' + error );
205 } );
206
207 $iframe.prop( 'src', 'about:blank' ).hide();
208
209 file.name = 'file';
210
211 $.each( data, function ( key, val ) {
212 if ( key === 'filename' ) {
213 filenameFound = true;
214 }
215
216 if ( fieldsAllowed[key] === true ) {
217 $form.append( getHiddenInput( key, val ) );
218 }
219 } );
220
221 if ( !filenameFound ) {
222 return $.Deferred().reject( 'Filename not included in file data.' );
223 }
224
225 tokenPromise = this.getEditToken().then( function ( token ) {
226 $form.append( getHiddenInput( 'token', token ) );
227 } );
228
229 $( 'body' ).append( $form, $iframe );
230
231 return deferred.promise();
232 },
233
234 /**
235 * Uploads a file using the FormData API.
236 * @param {File} file
237 * @param {Object} data
238 */
239 uploadWithFormData: function ( file, data ) {
240 var xhr, tokenPromise,
241 api = this,
242 formData = new FormData(),
243 deferred = $.Deferred(),
244 filenameFound = false;
245
246 formData.append( 'action', 'upload' );
247 formData.append( 'format', 'json' );
248
249 $.each( data, function ( key, val ) {
250 if ( key === 'filename' ) {
251 filenameFound = true;
252 }
253
254 if ( fieldsAllowed[key] === true ) {
255 formData.append( key, val );
256 }
257 } );
258
259 if ( !filenameFound ) {
260 return $.Deferred().reject( 'Filename not included in file data.' );
261 }
262
263 formData.append( 'file', file );
264
265 xhr = new XMLHttpRequest();
266
267 xhr.upload.addEventListener( 'progress', function ( e ) {
268 if ( e.lengthComputable ) {
269 deferred.notify( e.loaded / e.total );
270 }
271 }, false );
272
273 xhr.addEventListener( 'abort', function ( e ) {
274 deferred.reject( parseXHRResponse( e ) );
275 }, false );
276
277 xhr.addEventListener( 'load', function ( e ) {
278 deferred.resolve( parseXHRResponse( e ) );
279 }, false );
280
281 xhr.addEventListener( 'error', function ( e ) {
282 deferred.reject( parseXHRResponse( e ) );
283 }, false );
284
285 xhr.open( 'POST', this.defaults.ajax.url, true );
286
287 tokenPromise = this.getEditToken().then( function ( token ) {
288 formData.append( 'token', token );
289 xhr.send( formData );
290 }, function () {
291 // Mark the edit token as bad, it's been used.
292 api.badToken( 'edit' );
293 } );
294
295 return deferred.promise();
296 }
297 } );
298
299 /**
300 * @class mw.Api
301 * @mixins mw.Api.plugin.upload
302 */
303 }( mediaWiki, jQuery ) );