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