Merge "API: Use message-per-value for apihelp-query+iwlinks-param-prop"
[lhc/web/wiklou.git] / resources / src / mediawiki.api / mediawiki.api.js
1 ( function ( mw, $ ) {
2
3 /**
4 * @class mw.Api
5 */
6
7 /**
8 * @property {Object} defaultOptions Default options for #ajax calls. Can be overridden by passing
9 * `options` to mw.Api constructor.
10 * @property {Object} defaultOptions.parameters Default query parameters for API requests.
11 * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
12 * @private
13 */
14 var defaultOptions = {
15 parameters: {
16 action: 'query',
17 format: 'json'
18 },
19 ajax: {
20 url: mw.util.wikiScript( 'api' ),
21 timeout: 30 * 1000, // 30 seconds
22 dataType: 'json'
23 }
24 },
25
26 // Keyed by ajax url and symbolic name for the individual request
27 promises = {};
28
29 // Pre-populate with fake ajax promises to save http requests for tokens
30 // we already have on the page via the user.tokens module (bug 34733).
31 promises[ defaultOptions.ajax.url ] = {};
32 $.each( mw.user.tokens.get(), function ( key, value ) {
33 // This requires #getToken to use the same key as user.tokens.
34 // Format: token-type + "Token" (eg. editToken, patrolToken, watchToken).
35 promises[ defaultOptions.ajax.url ][ key ] = $.Deferred()
36 .resolve( value )
37 .promise( { abort: function () {} } );
38 } );
39
40 /**
41 * Constructor to create an object to interact with the API of a particular MediaWiki server.
42 * mw.Api objects represent the API of a particular MediaWiki server.
43 *
44 * var api = new mw.Api();
45 * api.get( {
46 * action: 'query',
47 * meta: 'userinfo'
48 * } ).done( function ( data ) {
49 * console.log( data );
50 * } );
51 *
52 * Multiple values for a parameter can be specified using an array (since MW 1.25):
53 *
54 * var api = new mw.Api();
55 * api.get( {
56 * action: 'query',
57 * meta: [ 'userinfo', 'siteinfo' ] // same effect as 'userinfo|siteinfo'
58 * } ).done( function ( data ) {
59 * console.log( data );
60 * } );
61 *
62 * @constructor
63 * @param {Object} [options] See #defaultOptions documentation above. Can also be overridden for
64 * each individual request by passing them to #get or #post (or directly #ajax) later on.
65 */
66 mw.Api = function ( options ) {
67 // TODO: Share API objects with exact same config.
68 options = options || {};
69
70 // Force a string if we got a mw.Uri object
71 if ( options.ajax && options.ajax.url !== undefined ) {
72 options.ajax.url = String( options.ajax.url );
73 }
74
75 options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
76 options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
77
78 this.defaults = options;
79 };
80
81 mw.Api.prototype = {
82
83 /**
84 * Perform API get request
85 *
86 * @param {Object} parameters
87 * @param {Object} [ajaxOptions]
88 * @return {jQuery.Promise}
89 */
90 get: function ( parameters, ajaxOptions ) {
91 ajaxOptions = ajaxOptions || {};
92 ajaxOptions.type = 'GET';
93 return this.ajax( parameters, ajaxOptions );
94 },
95
96 /**
97 * Perform API post request
98 *
99 * TODO: Post actions for non-local hostnames will need proxy.
100 *
101 * @param {Object} parameters
102 * @param {Object} [ajaxOptions]
103 * @return {jQuery.Promise}
104 */
105 post: function ( parameters, ajaxOptions ) {
106 ajaxOptions = ajaxOptions || {};
107 ajaxOptions.type = 'POST';
108 return this.ajax( parameters, ajaxOptions );
109 },
110
111 /**
112 * Perform the API call.
113 *
114 * @param {Object} parameters
115 * @param {Object} [ajaxOptions]
116 * @return {jQuery.Promise} Done: API response data and the jqXHR object.
117 * Fail: Error code
118 */
119 ajax: function ( parameters, ajaxOptions ) {
120 var token,
121 apiDeferred = $.Deferred(),
122 xhr, key, formData;
123
124 parameters = $.extend( {}, this.defaults.parameters, parameters );
125 ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
126
127 // Ensure that token parameter is last (per [[mw:API:Edit#Token]]).
128 if ( parameters.token ) {
129 token = parameters.token;
130 delete parameters.token;
131 }
132
133 for ( key in parameters ) {
134 if ( $.isArray( parameters[key] ) ) {
135 parameters[key] = parameters[key].join( '|' );
136 }
137 }
138
139 // If multipart/form-data has been requested and emulation is possible, emulate it
140 if (
141 ajaxOptions.type === 'POST' &&
142 window.FormData &&
143 ajaxOptions.contentType === 'multipart/form-data'
144 ) {
145
146 formData = new FormData();
147
148 for ( key in parameters ) {
149 formData.append( key, parameters[key] );
150 }
151 // If we extracted a token parameter, add it back in.
152 if ( token ) {
153 formData.append( 'token', token );
154 }
155
156 ajaxOptions.data = formData;
157
158 // Prevent jQuery from mangling our FormData object
159 ajaxOptions.processData = false;
160 // Prevent jQuery from overriding the Content-Type header
161 ajaxOptions.contentType = false;
162 } else {
163 // Some deployed MediaWiki >= 1.17 forbid periods in URLs, due to an IE XSS bug
164 // So let's escape them here. See bug #28235
165 // This works because jQuery accepts data as a query string or as an Object
166 ajaxOptions.data = $.param( parameters ).replace( /\./g, '%2E' );
167
168 // If we extracted a token parameter, add it back in.
169 if ( token ) {
170 ajaxOptions.data += '&token=' + encodeURIComponent( token );
171 }
172
173 if ( ajaxOptions.contentType === 'multipart/form-data' ) {
174 // We were asked to emulate but can't, so drop the Content-Type header, otherwise
175 // it'll be wrong and the server will fail to decode the POST body
176 delete ajaxOptions.contentType;
177 }
178 }
179
180 // Make the AJAX request
181 xhr = $.ajax( ajaxOptions )
182 // If AJAX fails, reject API call with error code 'http'
183 // and details in second argument.
184 .fail( function ( xhr, textStatus, exception ) {
185 apiDeferred.reject( 'http', {
186 xhr: xhr,
187 textStatus: textStatus,
188 exception: exception
189 } );
190 } )
191 // AJAX success just means "200 OK" response, also check API error codes
192 .done( function ( result, textStatus, jqXHR ) {
193 if ( result === undefined || result === null || result === '' ) {
194 apiDeferred.reject( 'ok-but-empty',
195 'OK response but empty result (check HTTP headers?)'
196 );
197 } else if ( result.error ) {
198 var code = result.error.code === undefined ? 'unknown' : result.error.code;
199 apiDeferred.reject( code, result );
200 } else {
201 apiDeferred.resolve( result, jqXHR );
202 }
203 } );
204
205 // Return the Promise
206 return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
207 if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
208 mw.log( 'mw.Api error: ', code, details );
209 }
210 } );
211 },
212
213 /**
214 * Post to API with specified type of token. If we have no token, get one and try to post.
215 * If we have a cached token try using that, and if it fails, blank out the
216 * cached token and start over. For example to change an user option you could do:
217 *
218 * new mw.Api().postWithToken( 'options', {
219 * action: 'options',
220 * optionname: 'gender',
221 * optionvalue: 'female'
222 * } );
223 *
224 * @param {string} tokenType The name of the token, like options or edit.
225 * @param {Object} params API parameters
226 * @param {Object} [ajaxOptions]
227 * @return {jQuery.Promise} See #post
228 * @since 1.22
229 */
230 postWithToken: function ( tokenType, params, ajaxOptions ) {
231 var api = this;
232
233 return api.getToken( tokenType, params.assert ).then( function ( token ) {
234 params.token = token;
235 return api.post( params, ajaxOptions ).then(
236 // If no error, return to caller as-is
237 null,
238 // Error handler
239 function ( code ) {
240 if ( code === 'badtoken' ) {
241 api.badToken( tokenType );
242 // Try again, once
243 params.token = undefined;
244 return api.getToken( tokenType, params.assert ).then( function ( token ) {
245 params.token = token;
246 return api.post( params, ajaxOptions );
247 } );
248 }
249
250 // Different error, pass on to let caller handle the error code
251 return this;
252 }
253 );
254 } );
255 },
256
257 /**
258 * Get a token for a certain action from the API.
259 *
260 * The assert parameter is only for internal use by postWithToken.
261 *
262 * @param {string} type Token type
263 * @return {jQuery.Promise}
264 * @return {Function} return.done
265 * @return {string} return.done.token Received token.
266 * @since 1.22
267 */
268 getToken: function ( type, assert ) {
269 var apiPromise,
270 promiseGroup = promises[ this.defaults.ajax.url ],
271 d = promiseGroup && promiseGroup[ type + 'Token' ];
272
273 if ( !d ) {
274 apiPromise = this.get( { action: 'tokens', type: type, assert: assert } );
275
276 d = apiPromise
277 .then( function ( data ) {
278 if ( data.tokens && data.tokens[type + 'token'] ) {
279 return data.tokens[type + 'token'];
280 }
281
282 // If token type is not available for this user,
283 // key '...token' is either missing or set to boolean false
284 return $.Deferred().reject( 'token-missing', data );
285 }, function () {
286 // Clear promise. Do not cache errors.
287 delete promiseGroup[ type + 'Token' ];
288 // Pass on to allow the caller to handle the error
289 return this;
290 } )
291 // Attach abort handler
292 .promise( { abort: apiPromise.abort } );
293
294 // Store deferred now so that we can use it again even if it isn't ready yet
295 if ( !promiseGroup ) {
296 promiseGroup = promises[ this.defaults.ajax.url ] = {};
297 }
298 promiseGroup[ type + 'Token' ] = d;
299 }
300
301 return d;
302 },
303
304 /**
305 * Indicate that the cached token for a certain action of the API is bad.
306 *
307 * Call this if you get a 'badtoken' error when using the token returned by #getToken.
308 * You may also want to use #postWithToken instead, which invalidates bad cached tokens
309 * automatically.
310 *
311 * @param {string} type Token type
312 * @since 1.26
313 */
314 badToken: function ( type ) {
315 var promiseGroup = promises[ this.defaults.ajax.url ];
316 if ( promiseGroup ) {
317 delete promiseGroup[ type + 'Token' ];
318 }
319 }
320 };
321
322 /**
323 * @static
324 * @property {Array}
325 * List of errors we might receive from the API.
326 * For now, this just documents our expectation that there should be similar messages
327 * available.
328 */
329 mw.Api.errors = [
330 // occurs when POST aborted
331 // jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result
332 'ok-but-empty',
333
334 // timeout
335 'timeout',
336
337 // really a warning, but we treat it like an error
338 'duplicate',
339 'duplicate-archive',
340
341 // upload succeeded, but no image info.
342 // this is probably impossible, but might as well check for it
343 'noimageinfo',
344 // remote errors, defined in API
345 'uploaddisabled',
346 'nomodule',
347 'mustbeposted',
348 'badaccess-groups',
349 'missingresult',
350 'missingparam',
351 'invalid-file-key',
352 'copyuploaddisabled',
353 'mustbeloggedin',
354 'empty-file',
355 'file-too-large',
356 'filetype-missing',
357 'filetype-banned',
358 'filetype-banned-type',
359 'filename-tooshort',
360 'illegal-filename',
361 'verification-error',
362 'hookaborted',
363 'unknown-error',
364 'internal-error',
365 'overwrite',
366 'badtoken',
367 'fetchfileerror',
368 'fileexists-shared-forbidden',
369 'invalidtitle',
370 'notloggedin',
371
372 // Stash-specific errors - expanded
373 'stashfailed',
374 'stasherror',
375 'stashedfilenotfound',
376 'stashpathinvalid',
377 'stashfilestorage',
378 'stashzerolength',
379 'stashnotloggedin',
380 'stashwrongowner',
381 'stashnosuchfilekey'
382 ];
383
384 /**
385 * @static
386 * @property {Array}
387 * List of warnings we might receive from the API.
388 * For now, this just documents our expectation that there should be similar messages
389 * available.
390 */
391 mw.Api.warnings = [
392 'duplicate',
393 'exists'
394 ];
395
396 }( mediaWiki, jQuery ) );