( function () { /** * @typedef {Object} mw.Api.Options * @property {Object} [parameters = { action: 'query', format: 'json' }] Default query * parameters for API requests * @property {Object} [ajax = { url: mw.util.wikiScript( 'api' ), timeout: 30 * 1000, dataType: 'json' }] * Default options for jQuery#ajax * @property {boolean} [useUS] Whether to use U+001F when joining multi-valued * parameters (since 1.28). Default is true if ajax.url is not set, false otherwise for * compatibility. * @property {string} [userAgent] User agent string to use for API requests (since 1.44). * This should identify what component (extension, gadget, user script) is making the request. */ /** * @private * @type {mw.Api.Options} */ const defaultOptions = { parameters: { action: 'query', format: 'json' }, ajax: { url: mw.util.wikiScript( 'api' ), timeout: 30 * 1000, // 30 seconds dataType: 'json' } }; /** * @classdesc Interact with the MediaWiki API. `mw.Api` is a client library for * the [action API](https://www.mediawiki.org/wiki/Special:MyLanguage/API:Main_page). * An `mw.Api` object represents the API of a MediaWiki site. For the REST API, * see {@link mw.Rest}. * * ``` * var api = new mw.Api(); * api.get( { * action: 'query', * meta: 'userinfo' * } ).then( ( data ) => { * console.log( data ); * } ); * ``` * * Since MW 1.25, multiple values for a parameter can be specified using an array: * * ``` * var api = new mw.Api(); * api.get( { * action: 'query', * meta: [ 'userinfo', 'siteinfo' ] // same effect as 'userinfo|siteinfo' * } ).then( ( data ) => { * console.log( data ); * } ); * ``` * * Since MW 1.26, boolean values for API parameters can be specified natively. Parameter * values set to `false` or `undefined` will be omitted from the request, as required by * the API. * * @class mw.Api * @constructor * @description Create an instance of `mw.Api`. * @param {mw.Api.Options} [options] See {@link mw.Api.Options}. This can also be overridden for * each request by passing them to [get()]{@link mw.Api#get} or [post()]{@link mw.Api#post} (or directly to * [ajax()]{@link mw.Api#ajax}) later on. */ mw.Api = function ( options ) { const defaults = Object.assign( {}, options ), setsUrl = options && options.ajax && options.ajax.url !== undefined; defaults.parameters = Object.assign( {}, defaultOptions.parameters, defaults.parameters ); defaults.ajax = Object.assign( {}, defaultOptions.ajax, defaults.ajax ); defaults.userAgent = defaults.userAgent || ( 'MediaWiki-JS/' + mw.config.get( 'wgVersion' ) ); // Force a string if we got a mw.Uri object if ( setsUrl ) { defaults.ajax.url = String( defaults.ajax.url ); } if ( defaults.useUS === undefined ) { defaults.useUS = !setsUrl; } this.defaults = defaults; this.requests = []; }; function normalizeTokenType( type ) { // Aliases for types that mw.Api has always supported, // based on how action=tokens worked previously (T280806). const csrfActions = [ 'edit', 'delete', 'protect', 'move', 'block', 'unblock', 'email', 'import', 'options' ]; if ( csrfActions.indexOf( type ) !== -1 ) { return 'csrf'; } return type; } function createTokenCache() { const tokenPromises = {}; // Pre-populate with fake ajax promises to avoid HTTP requests for tokens that // we already have on the page from the embedded user.options module (T36733). tokenPromises[ defaultOptions.ajax.url ] = {}; const tokens = mw.user.tokens.get(); for ( const tokenKey in tokens ) { const value = tokens[ tokenKey ]; // This requires #getToken to use the same key as mw.user.tokens. // Format: token-type + "Token" (eg. csrfToken, patrolToken, watchToken). tokenPromises[ defaultOptions.ajax.url ][ tokenKey ] = $.Deferred() .resolve( value ) .promise( { abort: function () {} } ); } return tokenPromises; } // Keyed by ajax url and symbolic name for the individual request let promises = createTokenCache(); // Unique private object for use by makeAbortablePromise() const ABORTED_BY_ABORTABLE_PROMISE = new Error( 'ABORTED_BY_ABORTABLE_PROMISE' ); mw.Api.prototype = { /** * Abort all unfinished requests issued by this Api object. * * @method */ abort: function () { this.requests.forEach( ( request ) => { if ( request ) { request.abort(); } } ); }, /** * Perform API get request. See [ajax()]{@link mw.Api#ajax} for details. * * @param {Object} parameters * @param {Object} [ajaxOptions] * @return {mw.Api~AbortablePromise} */ get: function ( parameters, ajaxOptions ) { ajaxOptions = ajaxOptions || {}; ajaxOptions.type = 'GET'; return this.ajax( parameters, ajaxOptions ); }, /** * Perform API post request. See [ajax()]{@link mw.Api#ajax} for details. * * @param {Object} parameters * @param {Object} [ajaxOptions] * @return {mw.Api~AbortablePromise} */ post: function ( parameters, ajaxOptions ) { ajaxOptions = ajaxOptions || {}; ajaxOptions.type = 'POST'; return this.ajax( parameters, ajaxOptions ); }, /** * Massage parameters from the nice format we accept into a format suitable for the API. * * NOTE: A value of undefined/null in an array will be represented by Array#join() * as the empty string. Should we filter silently? Warn? Leave as-is? * * @private * @param {Object} parameters (modified in-place) * @param {boolean} useUS Whether to use U+001F when joining multivalued parameters. */ preprocessParameters: function ( parameters, useUS ) { let key; // Handle common MediaWiki API idioms for passing parameters for ( key in parameters ) { // Multiple values are pipe-separated if ( Array.isArray( parameters[ key ] ) ) { if ( !useUS || parameters[ key ].join( '' ).indexOf( '|' ) === -1 ) { parameters[ key ] = parameters[ key ].join( '|' ); } else { parameters[ key ] = '\x1f' + parameters[ key ].join( '\x1f' ); } } else if ( parameters[ key ] === false || parameters[ key ] === undefined ) { // Boolean values are only false when not given at all delete parameters[ key ]; } } }, /** * Perform the API call. * * @param {Object} parameters Parameters to the API. See also {@link mw.Api.Options} * @param {Object} [ajaxOptions] Parameters to pass to jQuery.ajax. See also * {@link mw.Api.Options} * @param {AbortSignal} [ajaxOptions.signal] Signal which can be used to abort the request. * See {@link mw.Api~AbortController} for an example. (since 1.44) * @return {mw.Api~AbortablePromise} A promise that settles when the API response is processed. * Has an 'abort' method which can be used to abort the request. * See {@link mw.Api~AbortablePromise} for an example. * * - On success, resolves to `( result, jqXHR )` where `result` is the parsed API response. * - On an API error, rejects with `( code, result, result, jqXHR )` where `code` is the * [API error code](https://www.mediawiki.org/wiki/API:Errors_and_warnings), and `result` * is as above. When there are multiple errors, the code from the first one will be used. * If there is no error code, "unknown" is used. * - On other types of errors, rejects with `( 'http', details )` where `details` is an object * with three fields: `xhr` (the jqXHR object), `textStatus`, and `exception`. * The meaning of the last two fields is as follows: * - When the request is aborted (the abort method of the promise is called), textStatus * and exception are both set to "abort". * - On a network timeout, textStatus and exception are both set to "timeout". * - On a network error, textStatus is "error" and exception is the empty string. * - When the HTTP response code is anything other than 2xx or 304 (the API does not * use such response codes but some intermediate layer might), textStatus is "error" * and exception is the HTTP status text (the text following the status code in the * first line of the server response). For HTTP/2, `exception` is always an empty string. * - When the response is not valid JSON but the previous error conditions aren't met, * textStatus is "parsererror" and exception is the exception object thrown by * {@link JSON.parse}. */ ajax: function ( parameters, ajaxOptions ) { const apiDeferred = $.Deferred(); parameters = Object.assign( {}, this.defaults.parameters, parameters ); ajaxOptions = Object.assign( {}, this.defaults.ajax, ajaxOptions ); if ( ajaxOptions.signal && ajaxOptions.signal.aborted ) { if ( ajaxOptions.signal.reason !== ABORTED_BY_ABORTABLE_PROMISE ) { apiDeferred.reject( ajaxOptions.signal.reason, ajaxOptions.signal.reason ); } else { // Fake aborted promise apiDeferred.reject( 'http', { textStatus: 'abort', exception: 'abort' } ); } return apiDeferred.promise( { abort: function () {} } ); } let token; // Ensure that token parameter is last (per [[mw:API:Edit#Token]]). if ( parameters.token ) { token = parameters.token; delete parameters.token; } this.preprocessParameters( parameters, this.defaults.useUS ); // If multipart/form-data has been requested and emulation is possible, emulate it if ( ajaxOptions.type === 'POST' && window.FormData && ajaxOptions.contentType === 'multipart/form-data' ) { const formData = new FormData(); for ( const key in parameters ) { formData.append( key, parameters[ key ] ); } // If we extracted a token parameter, add it back in. if ( token ) { formData.append( 'token', token ); } ajaxOptions.data = formData; // Prevent jQuery from mangling our FormData object ajaxOptions.processData = false; // Prevent jQuery from overriding the Content-Type header ajaxOptions.contentType = false; } else { // This works because jQuery accepts data as a query string or as an Object ajaxOptions.data = $.param( parameters ); // If we extracted a token parameter, add it back in. if ( token ) { ajaxOptions.data += '&token=' + encodeURIComponent( token ); } if ( ajaxOptions.contentType === 'multipart/form-data' ) { // We were asked to emulate but can't, so drop the Content-Type header, otherwise // it'll be wrong and the server will fail to decode the POST body delete ajaxOptions.contentType; } } ajaxOptions.headers = ajaxOptions.headers || {}; const lowercaseHeaders = Object.keys( ajaxOptions.headers || {} ).map( ( k ) => k.toLowerCase() ); if ( lowercaseHeaders.indexOf( 'api-user-agent' ) === -1 ) { ajaxOptions.headers[ 'Api-User-Agent' ] = this.defaults.userAgent; } // Make the AJAX request const xhr = $.ajax( ajaxOptions ) // If AJAX fails, or is aborted by the abortable promise's .abort() method, // reject API call with error code 'http' and the details in the second argument. .fail( ( jqXHR, textStatus, exception ) => { apiDeferred.reject( 'http', { xhr: jqXHR, textStatus: textStatus, exception: exception } ); } ) // AJAX success just means "200 OK" response, also check API error codes .done( ( result, textStatus, jqXHR ) => { let code; if ( result === undefined || result === null || result === '' ) { apiDeferred.reject( 'ok-but-empty', 'OK response but empty result (check HTTP headers?)', result, jqXHR ); } else if ( result.error ) { // errorformat=bc code = result.error.code === undefined ? 'unknown' : result.error.code; apiDeferred.reject( code, result, result, jqXHR ); } else if ( result.errors ) { // errorformat!=bc code = result.errors[ 0 ].code === undefined ? 'unknown' : result.errors[ 0 ].code; apiDeferred.reject( code, result, result, jqXHR ); } else { apiDeferred.resolve( result, jqXHR ); } } ); const requestIndex = this.requests.length; this.requests.push( xhr ); xhr.always( () => { this.requests[ requestIndex ] = null; } ); if ( ajaxOptions.signal ) { ajaxOptions.signal.addEventListener( 'abort', () => { // If aborted by the abortable promise's .abort() method, skip this, so that the promise // gets rejected with the legacy values (see the code in `fail( … )` above). if ( ajaxOptions.signal.reason !== ABORTED_BY_ABORTABLE_PROMISE ) { apiDeferred.reject( ajaxOptions.signal.reason, ajaxOptions.signal.reason ); } // Cancel the HTTP request (which will reject the promise if we skipped the case above) xhr.abort(); } ); } // Return the Promise return apiDeferred.promise( { abort: xhr.abort } ).fail( ( code, details ) => { if ( !( ( code === 'http' && details && details.textStatus === 'abort' ) || ( details instanceof DOMException && details.name === 'AbortError' ) ) ) { mw.log( 'mw.Api error: ', code, details ); } } ); }, /** * Helper for adding support for abortable promises in mw.Api methods. * * This methods does three things: * - Returns an object with an `abort` method that can be used as a base for * an {@link mw.Api~AbortablePromise}. * - Updates the provided `ajaxOptions` with a `signal` that will be triggered by said method. * - If the `ajaxOptions` already had a `signal`, forwards evens from it to the new one. * * This ensures that both the signal provided in `ajaxOptions` (if any) and the * `abort` method on the returned object can cancel the HTTP requests. * It's only needed when supporting the old-style `promise.abort()` method. * * @since 1.44 * @param {Object} ajaxOptions Options object to modify (will set `ajaxOptions.signal`) * @return {Object} Base object for {@link mw.Api~AbortablePromise} * * @example