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