in extending classes is deprecated. Extend related doSearch* methods
instead.
* CollationFa has been removed completely as it's not needed anymore
+* The following 'mediawiki.api' plugin modules were merged into mediawiki.api
+ and deprecated: mediawiki.api.category, mediawiki.api.edit,
+ mediawiki.api.login, mediawiki.api.options, mediawiki.api.parse,
+ mediawiki.api.upload, mediawiki.api.user, mediawiki.api.watch,
+ mediawiki.api.messages, and mediawiki.api.rollback.
=== Other changes in 1.32 ===
* Soft hyphens (U+00AD) are now automatically removed from titles; these
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.api' => [
- 'scripts' => 'resources/src/mediawiki.api.js',
+ 'scripts' => [
+ 'resources/src/mediawiki.api/index.js',
+ 'resources/src/mediawiki.api/category.js',
+ 'resources/src/mediawiki.api/edit.js',
+ 'resources/src/mediawiki.api/login.js',
+ 'resources/src/mediawiki.api/messages.js',
+ 'resources/src/mediawiki.api/options.js',
+ 'resources/src/mediawiki.api/parse.js',
+ 'resources/src/mediawiki.api/rollback.js',
+ 'resources/src/mediawiki.api/upload.js',
+ 'resources/src/mediawiki.api/user.js',
+ 'resources/src/mediawiki.api/watch.js',
+ ],
'dependencies' => [
+ 'mediawiki.Title',
'mediawiki.util',
'user.tokens',
],
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.api.category' => [
- 'scripts' => 'resources/src/mediawiki.api.category.js',
- 'dependencies' => [
- 'mediawiki.api',
- 'mediawiki.Title',
- ],
+ 'deprecated' => 'Use "mediawiki.api" instead.',
+ 'dependencies' => 'mediawiki.api',
],
'mediawiki.api.edit' => [
- 'scripts' => 'resources/src/mediawiki.api.edit.js',
+ 'deprecated' => 'Use "mediawiki.api" instead.',
'dependencies' => [
'mediawiki.api',
- 'mediawiki.user',
],
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.api.login' => [
- 'scripts' => 'resources/src/mediawiki.api.login.js',
+ 'deprecated' => 'Use "mediawiki.api" instead.',
'dependencies' => 'mediawiki.api',
],
'mediawiki.api.options' => [
- 'scripts' => 'resources/src/mediawiki.api.options.js',
+ 'deprecated' => 'Use "mediawiki.api" instead.',
'dependencies' => 'mediawiki.api',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.api.parse' => [
- 'scripts' => 'resources/src/mediawiki.api.parse.js',
+ 'deprecated' => 'Use "mediawiki.api" instead.',
'dependencies' => 'mediawiki.api',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.api.upload' => [
- 'scripts' => 'resources/src/mediawiki.api.upload.js',
- 'dependencies' => [
- 'mediawiki.api',
- 'mediawiki.api.edit',
- ],
+ 'deprecated' => 'Use "mediawiki.api" instead.',
+ 'dependencies' => 'mediawiki.api',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.api.user' => [
- 'scripts' => 'resources/src/mediawiki.api.user.js',
- 'dependencies' => [
- 'mediawiki.api',
- ],
+ 'deprecated' => 'Use "mediawiki.api" instead.',
+ 'dependencies' => 'mediawiki.api',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.api.watch' => [
- 'scripts' => 'resources/src/mediawiki.api.watch.js',
- 'dependencies' => [
- 'mediawiki.api',
- ],
+ 'deprecated' => 'Use "mediawiki.api" instead.',
+ 'dependencies' => 'mediawiki.api',
],
'mediawiki.api.messages' => [
- 'scripts' => 'resources/src/mediawiki.api.messages.js',
- 'dependencies' => [
- 'mediawiki.api',
- ],
+ 'deprecated' => 'Use "mediawiki.api" instead.',
+ 'dependencies' => 'mediawiki.api',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.api.rollback' => [
- 'scripts' => 'resources/src/mediawiki.api.rollback.js',
- 'dependencies' => [
- 'mediawiki.api',
- ],
+ 'deprecated' => 'Use "mediawiki.api" instead.',
+ 'dependencies' => 'mediawiki.api',
],
'mediawiki.content.json' => [
'styles' => 'resources/src/mediawiki.content.json.less',
'resources/src/mediawiki.messagePoster.wikitext/WikitextMessagePoster.js',
],
'dependencies' => [
- 'mediawiki.api.edit',
+ 'mediawiki.api',
'mediawiki.messagePoster',
],
'targets' => [ 'desktop', 'mobile' ],
'mediawiki.Upload' => [
'scripts' => 'resources/src/mediawiki.Upload.js',
'dependencies' => [
- 'mediawiki.api.upload',
+ 'mediawiki.api',
],
],
'mediawiki.ForeignUpload' => [
'mediawiki.widgets.CategoryMultiselectWidget',
'mediawiki.widgets.DateInputWidget',
'mediawiki.jqueryMsg',
- 'mediawiki.api.messages',
+ 'mediawiki.api',
'moment',
'mediawiki.libs.jpegmeta',
],
'scripts' => 'resources/src/mediawiki.user.js',
'dependencies' => [
'mediawiki.api',
- 'mediawiki.api.user',
'mediawiki.storage',
'user.options',
'user.tokens',
'mediawiki.page.watch.ajax' => [
'scripts' => 'resources/src/mediawiki.page.watch.ajax.js',
'dependencies' => [
- 'mediawiki.api.watch',
+ 'mediawiki.api',
'mediawiki.notify',
'mediawiki.util',
'mediawiki.Title',
'mediawiki.page.rollback' => [
'scripts' => 'resources/src/mediawiki.page.rollback.js',
'dependencies' => [
- 'mediawiki.api.rollback',
+ 'mediawiki.api',
'mediawiki.notify',
'mediawiki.util',
'jquery.spinner',
'mediawiki.String',
'oojs',
'mediawiki.api',
- 'mediawiki.api.options',
'mediawiki.jqueryMsg',
'mediawiki.Uri',
'mediawiki.user',
],
'dependencies' => [
'mediawiki.api',
- 'mediawiki.api.watch',
'mediawiki.notify',
'mediawiki.Title',
'mediawiki.util',
'jquery.spinner',
'mediawiki.jqueryMsg',
'mediawiki.api',
- 'mediawiki.api.parse',
'mediawiki.libs.jpegmeta',
'mediawiki.Title',
'mediawiki.util',
'watchlist-unwatch-undo',
],
'dependencies' => [
- 'mediawiki.api.watch',
+ 'mediawiki.api',
'mediawiki.jqueryMsg',
'mediawiki.Title',
'mediawiki.util',
+++ /dev/null
-/**
- * @class mw.Api.plugin.category
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
- /**
- * Determine if a category exists.
- *
- * @param {mw.Title|string} title
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {boolean} return.done.isCategory Whether the category exists.
- */
- isCategory: function ( title ) {
- var apiPromise = this.get( {
- formatversion: 2,
- prop: 'categoryinfo',
- titles: [ String( title ) ]
- } );
-
- return apiPromise
- .then( function ( data ) {
- return !!(
- data.query && // query is missing on title=""
- data.query.pages && // query.pages is missing on title="#" or title="mw:"
- data.query.pages[ 0 ].categoryinfo
- );
- } )
- .promise( { abort: apiPromise.abort } );
- },
-
- /**
- * Get a list of categories that match a certain prefix.
- *
- * E.g. given "Foo", return "Food", "Foolish people", "Foosball tables"...
- *
- * @param {string} prefix Prefix to match.
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {string[]} return.done.categories Matched categories
- */
- getCategoriesByPrefix: function ( prefix ) {
- // Fetch with allpages to only get categories that have a corresponding description page.
- var apiPromise = this.get( {
- formatversion: 2,
- list: 'allpages',
- apprefix: prefix,
- apnamespace: mw.config.get( 'wgNamespaceIds' ).category
- } );
-
- return apiPromise
- .then( function ( data ) {
- return data.query.allpages.map( function ( category ) {
- return new mw.Title( category.title ).getMainText();
- } );
- } )
- .promise( { abort: apiPromise.abort } );
- },
-
- /**
- * Get the categories that a particular page on the wiki belongs to.
- *
- * @param {mw.Title|string} title
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {boolean|mw.Title[]} return.done.categories List of category titles or false
- * if title was not found.
- */
- getCategories: function ( title ) {
- var apiPromise = this.get( {
- formatversion: 2,
- prop: 'categories',
- titles: [ String( title ) ]
- } );
-
- return apiPromise
- .then( function ( data ) {
- var page;
-
- if ( !data.query || !data.query.pages ) {
- return false;
- }
- page = data.query.pages[ 0 ];
- if ( !page.categories ) {
- return false;
- }
- return page.categories.map( function ( cat ) {
- return new mw.Title( cat.title );
- } );
- } )
- .promise( { abort: apiPromise.abort } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.category
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.edit
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
-
- /**
- * Post to API with csrf token. If we have no token, get one and try to post.
- * If we have a cached token try using that, and if it fails, blank out the
- * cached token and start over.
- *
- * @param {Object} params API parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise} See #post
- */
- postWithEditToken: function ( params, ajaxOptions ) {
- return this.postWithToken( 'csrf', params, ajaxOptions );
- },
-
- /**
- * API helper to grab a csrf token.
- *
- * @return {jQuery.Promise} Received token.
- */
- getEditToken: function () {
- return this.getToken( 'csrf' );
- },
-
- /**
- * Create a new page.
- *
- * Example:
- *
- * new mw.Api().create( 'Sandbox',
- * { summary: 'Load sand particles.' },
- * 'Sand.'
- * );
- *
- * @since 1.28
- * @param {mw.Title|string} title Page title
- * @param {Object} params Edit API parameters
- * @param {string} params.summary Edit summary
- * @param {string} content
- * @return {jQuery.Promise} API response
- */
- create: function ( title, params, content ) {
- return this.postWithEditToken( $.extend( {
- action: 'edit',
- title: String( title ),
- text: content,
- formatversion: '2',
-
- // Protect against errors and conflicts
- assert: mw.user.isAnon() ? undefined : 'user',
- createonly: true
- }, params ) ).then( function ( data ) {
- return data.edit;
- } );
- },
-
- /**
- * Edit an existing page.
- *
- * To create a new page, use #create() instead.
- *
- * Simple transformation:
- *
- * new mw.Api()
- * .edit( 'Sandbox', function ( revision ) {
- * return revision.content.replace( 'foo', 'bar' );
- * } )
- * .then( function () {
- * console.log( 'Saved! ');
- * } );
- *
- * Set save parameters by returning an object instead of a string:
- *
- * new mw.Api().edit(
- * 'Sandbox',
- * function ( revision ) {
- * return {
- * text: revision.content.replace( 'foo', 'bar' ),
- * summary: 'Replace "foo" with "bar".',
- * assert: 'bot',
- * minor: true
- * };
- * }
- * )
- * .then( function () {
- * console.log( 'Saved! ');
- * } );
- *
- * Transform asynchronously by returning a promise.
- *
- * new mw.Api()
- * .edit( 'Sandbox', function ( revision ) {
- * return Spelling
- * .corrections( revision.content )
- * .then( function ( report ) {
- * return {
- * text: report.output,
- * summary: report.changelog
- * };
- * } );
- * } )
- * .then( function () {
- * console.log( 'Saved! ');
- * } );
- *
- * @since 1.28
- * @param {mw.Title|string} title Page title
- * @param {Function} transform Callback that prepares the edit
- * @param {Object} transform.revision Current revision
- * @param {string} transform.revision.content Current revision content
- * @param {string|Object|jQuery.Promise} transform.return New content, object with edit
- * API parameters, or promise providing one of those.
- * @return {jQuery.Promise} Edit API response
- */
- edit: function ( title, transform ) {
- var basetimestamp, curtimestamp,
- api = this;
-
- title = String( title );
-
- return api.get( {
- action: 'query',
- prop: 'revisions',
- rvprop: [ 'content', 'timestamp' ],
- titles: [ title ],
- formatversion: '2',
- curtimestamp: true
- } )
- .then( function ( data ) {
- var page, revision;
- if ( !data.query || !data.query.pages ) {
- return $.Deferred().reject( 'unknown' );
- }
- page = data.query.pages[ 0 ];
- if ( !page || page.invalid ) {
- return $.Deferred().reject( 'invalidtitle' );
- }
- if ( page.missing ) {
- return $.Deferred().reject( 'nocreate-missing' );
- }
- revision = page.revisions[ 0 ];
- basetimestamp = revision.timestamp;
- curtimestamp = data.curtimestamp;
- return transform( {
- timestamp: revision.timestamp,
- content: revision.content
- } );
- } )
- .then( function ( params ) {
- var editParams = typeof params === 'object' ? params : { text: String( params ) };
- return api.postWithEditToken( $.extend( {
- action: 'edit',
- title: title,
- formatversion: '2',
-
- // Protect against errors and conflicts
- assert: mw.user.isAnon() ? undefined : 'user',
- basetimestamp: basetimestamp,
- starttimestamp: curtimestamp,
- nocreate: true
- }, editParams ) );
- } )
- .then( function ( data ) {
- return data.edit;
- } );
- },
-
- /**
- * Post a new section to the page.
- *
- * @see #postWithEditToken
- * @param {mw.Title|string} title Target page
- * @param {string} header
- * @param {string} message wikitext message
- * @param {Object} [additionalParams] Additional API parameters, e.g. `{ redirect: true }`
- * @return {jQuery.Promise}
- */
- newSection: function ( title, header, message, additionalParams ) {
- return this.postWithEditToken( $.extend( {
- action: 'edit',
- section: 'new',
- title: String( title ),
- summary: header,
- text: message
- }, additionalParams ) );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.edit
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-( function ( mw, $ ) {
-
- /**
- * @class mw.Api
- */
-
- /**
- * @property {Object} defaultOptions Default options for #ajax calls. Can be overridden by passing
- * `options` to mw.Api constructor.
- * @property {Object} defaultOptions.parameters Default query parameters for API requests.
- * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
- * @property {boolean} defaultOptions.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.
- * @private
- */
- var defaultOptions = {
- parameters: {
- action: 'query',
- format: 'json'
- },
- ajax: {
- url: mw.util.wikiScript( 'api' ),
- timeout: 30 * 1000, // 30 seconds
- dataType: 'json'
- }
- },
-
- // Keyed by ajax url and symbolic name for the individual request
- promises = {};
-
- function mapLegacyToken( action ) {
- // Legacy types for backward-compatibility with API action=tokens.
- var csrfActions = [
- 'edit',
- 'delete',
- 'protect',
- 'move',
- 'block',
- 'unblock',
- 'email',
- 'import',
- 'options'
- ];
- if ( csrfActions.indexOf( action ) !== -1 ) {
- mw.track( 'mw.deprecate', 'apitoken_' + action );
- mw.log.warn( 'Use of the "' + action + '" token is deprecated. Use "csrf" instead.' );
- return 'csrf';
- }
- return action;
- }
-
- // Pre-populate with fake ajax promises to save http requests for tokens
- // we already have on the page via the user.tokens module (T36733).
- promises[ defaultOptions.ajax.url ] = {};
- $.each( mw.user.tokens.get(), function ( key, value ) {
- // This requires #getToken to use the same key as user.tokens.
- // Format: token-type + "Token" (eg. csrfToken, patrolToken, watchToken).
- promises[ defaultOptions.ajax.url ][ key ] = $.Deferred()
- .resolve( value )
- .promise( { abort: function () {} } );
- } );
-
- /**
- * Constructor to create an object to interact with the API of a particular MediaWiki server.
- * mw.Api objects represent the API of a particular MediaWiki server.
- *
- * var api = new mw.Api();
- * api.get( {
- * action: 'query',
- * meta: 'userinfo'
- * } ).done( function ( 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'
- * } ).done( function ( data ) {
- * console.log( data );
- * } );
- *
- * Since MW 1.26, boolean values for a parameter can be specified directly. If the value is
- * `false` or `undefined`, the parameter will be omitted from the request, as required by the API.
- *
- * @constructor
- * @param {Object} [options] See #defaultOptions documentation above. Can also be overridden for
- * each individual request by passing them to #get or #post (or directly #ajax) later on.
- */
- mw.Api = function ( options ) {
- options = options || {};
-
- // Force a string if we got a mw.Uri object
- if ( options.ajax && options.ajax.url !== undefined ) {
- options.ajax.url = String( options.ajax.url );
- }
-
- options = $.extend( { useUS: !options.ajax || !options.ajax.url }, options );
-
- options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
- options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
-
- this.defaults = options;
- this.requests = [];
- };
-
- mw.Api.prototype = {
- /**
- * Abort all unfinished requests issued by this Api object.
- *
- * @method
- */
- abort: function () {
- this.requests.forEach( function ( request ) {
- if ( request ) {
- request.abort();
- }
- } );
- },
-
- /**
- * Perform API get request
- *
- * @param {Object} parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise}
- */
- get: function ( parameters, ajaxOptions ) {
- ajaxOptions = ajaxOptions || {};
- ajaxOptions.type = 'GET';
- return this.ajax( parameters, ajaxOptions );
- },
-
- /**
- * Perform API post request
- *
- * @param {Object} parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise}
- */
- 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 multi-valued parameters.
- */
- preprocessParameters: function ( parameters, useUS ) {
- var 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
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise} Done: API response data and the jqXHR object.
- * Fail: Error code
- */
- ajax: function ( parameters, ajaxOptions ) {
- var token, requestIndex,
- api = this,
- apiDeferred = $.Deferred(),
- xhr, key, formData;
-
- parameters = $.extend( {}, this.defaults.parameters, parameters );
- ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
-
- // 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'
- ) {
-
- formData = new FormData();
-
- for ( 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 );
- }
-
- // Depending on server configuration, MediaWiki may forbid periods in URLs, due to an IE 6
- // XSS bug. So let's escape them here. See WebRequest::checkUrlExtension() and T30235.
- ajaxOptions.data = ajaxOptions.data.replace( /\./g, '%2E' );
-
- 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;
- }
- }
-
- // Make the AJAX request
- xhr = $.ajax( ajaxOptions )
- // If AJAX fails, reject API call with error code 'http'
- // and details in second argument.
- .fail( function ( xhr, textStatus, exception ) {
- apiDeferred.reject( 'http', {
- xhr: xhr,
- textStatus: textStatus,
- exception: exception
- } );
- } )
- // AJAX success just means "200 OK" response, also check API error codes
- .done( function ( result, textStatus, jqXHR ) {
- var 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 );
- }
- } );
-
- requestIndex = this.requests.length;
- this.requests.push( xhr );
- xhr.always( function () {
- api.requests[ requestIndex ] = null;
- } );
- // Return the Promise
- return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
- if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
- mw.log( 'mw.Api error: ', code, details );
- }
- } );
- },
-
- /**
- * Post to API with specified type of token. If we have no token, get one and try to post.
- * If we have a cached token try using that, and if it fails, blank out the
- * cached token and start over. For example to change an user option you could do:
- *
- * new mw.Api().postWithToken( 'csrf', {
- * action: 'options',
- * optionname: 'gender',
- * optionvalue: 'female'
- * } );
- *
- * @param {string} tokenType The name of the token, like options or edit.
- * @param {Object} params API parameters
- * @param {Object} [ajaxOptions]
- * @return {jQuery.Promise} See #post
- * @since 1.22
- */
- postWithToken: function ( tokenType, params, ajaxOptions ) {
- var api = this,
- abortedPromise = $.Deferred().reject( 'http',
- { textStatus: 'abort', exception: 'abort' } ).promise(),
- abortable,
- aborted;
-
- return api.getToken( tokenType, params.assert ).then( function ( token ) {
- params.token = token;
- // Request was aborted while token request was running, but we
- // don't want to unnecessarily abort token requests, so abort
- // a fake request instead
- if ( aborted ) {
- return abortedPromise;
- }
-
- return ( abortable = api.post( params, ajaxOptions ) ).catch(
- // Error handler
- function ( code ) {
- if ( code === 'badtoken' ) {
- api.badToken( tokenType );
- // Try again, once
- params.token = undefined;
- abortable = null;
- return api.getToken( tokenType, params.assert ).then( function ( token ) {
- params.token = token;
- if ( aborted ) {
- return abortedPromise;
- }
-
- return ( abortable = api.post( params, ajaxOptions ) );
- } );
- }
-
- // Let caller handle the error code
- return $.Deferred().rejectWith( this, arguments );
- }
- );
- } ).promise( { abort: function () {
- if ( abortable ) {
- abortable.abort();
- } else {
- aborted = true;
- }
- } } );
- },
-
- /**
- * Get a token for a certain action from the API.
- *
- * The assert parameter is only for internal use by #postWithToken.
- *
- * @since 1.22
- * @param {string} type Token type
- * @param {string} [assert]
- * @return {jQuery.Promise} Received token.
- */
- getToken: function ( type, assert ) {
- var apiPromise, promiseGroup, d, reject;
- type = mapLegacyToken( type );
- promiseGroup = promises[ this.defaults.ajax.url ];
- d = promiseGroup && promiseGroup[ type + 'Token' ];
-
- if ( !promiseGroup ) {
- promiseGroup = promises[ this.defaults.ajax.url ] = {};
- }
-
- if ( !d ) {
- apiPromise = this.get( {
- action: 'query',
- meta: 'tokens',
- type: type,
- assert: assert
- } );
- reject = function () {
- // Clear promise. Do not cache errors.
- delete promiseGroup[ type + 'Token' ];
-
- // Let caller handle the error code
- return $.Deferred().rejectWith( this, arguments );
- };
- d = apiPromise
- .then( function ( res ) {
- if ( !res.query ) {
- return reject( 'query-missing', res );
- }
- // If token type is unknown, it is omitted from the response
- if ( !res.query.tokens[ type + 'token' ] ) {
- return $.Deferred().reject( 'token-missing', res );
- }
- return res.query.tokens[ type + 'token' ];
- }, reject )
- // Attach abort handler
- .promise( { abort: apiPromise.abort } );
-
- // Store deferred now so that we can use it again even if it isn't ready yet
- promiseGroup[ type + 'Token' ] = d;
- }
-
- return d;
- },
-
- /**
- * Indicate that the cached token for a certain action of the API is bad.
- *
- * Call this if you get a 'badtoken' error when using the token returned by #getToken.
- * You may also want to use #postWithToken instead, which invalidates bad cached tokens
- * automatically.
- *
- * @param {string} type Token type
- * @since 1.26
- */
- badToken: function ( type ) {
- var promiseGroup = promises[ this.defaults.ajax.url ];
-
- type = mapLegacyToken( type );
- if ( promiseGroup ) {
- delete promiseGroup[ type + 'Token' ];
- }
- }
- };
-
- /**
- * @static
- * @property {Array}
- * Very incomplete and outdated list of errors we might receive from the API. Do not use.
- * @deprecated since 1.29
- */
- mw.Api.errors = [
- // occurs when POST aborted
- // jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result
- 'ok-but-empty',
-
- // timeout
- 'timeout',
-
- // really a warning, but we treat it like an error
- 'duplicate',
- 'duplicate-archive',
-
- // upload succeeded, but no image info.
- // this is probably impossible, but might as well check for it
- 'noimageinfo',
- // remote errors, defined in API
- 'uploaddisabled',
- 'nomodule',
- 'mustbeposted',
- 'badaccess-groups',
- 'missingresult',
- 'missingparam',
- 'invalid-file-key',
- 'copyuploaddisabled',
- 'mustbeloggedin',
- 'empty-file',
- 'file-too-large',
- 'filetype-missing',
- 'filetype-banned',
- 'filetype-banned-type',
- 'filename-tooshort',
- 'illegal-filename',
- 'verification-error',
- 'hookaborted',
- 'unknown-error',
- 'internal-error',
- 'overwrite',
- 'badtoken',
- 'fetchfileerror',
- 'fileexists-shared-forbidden',
- 'invalidtitle',
- 'notloggedin',
- 'autoblocked',
- 'blocked',
-
- // Stash-specific errors - expanded
- 'stashfailed',
- 'stasherror',
- 'stashedfilenotfound',
- 'stashpathinvalid',
- 'stashfilestorage',
- 'stashzerolength',
- 'stashnotloggedin',
- 'stashwrongowner',
- 'stashnosuchfilekey'
- ];
- mw.log.deprecate( mw.Api, 'errors', mw.Api.errors, null, 'mw.Api.errors' );
-
- /**
- * @static
- * @property {Array}
- * Very incomplete and outdated list of warnings we might receive from the API. Do not use.
- * @deprecated since 1.29
- */
- mw.Api.warnings = [
- 'duplicate',
- 'exists'
- ];
- mw.log.deprecate( mw.Api, 'warnings', mw.Api.warnings, null, 'mw.Api.warnings' );
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * Make the two-step login easier.
- *
- * @author Niklas Laxström
- * @class mw.Api.plugin.login
- * @since 1.22
- */
-( function ( mw, $ ) {
- 'use strict';
-
- $.extend( mw.Api.prototype, {
- /**
- * @param {string} username
- * @param {string} password
- * @return {jQuery.Promise} See mw.Api#post
- */
- login: function ( username, password ) {
- var params, apiPromise, innerPromise,
- api = this;
-
- params = {
- action: 'login',
- lgname: username,
- lgpassword: password
- };
-
- apiPromise = api.post( params );
-
- return apiPromise
- .then( function ( data ) {
- params.lgtoken = data.login.token;
- innerPromise = api.post( params )
- .then( function ( data ) {
- var code;
- if ( data.login.result !== 'Success' ) {
- // Set proper error code whenever possible
- code = data.error && data.error.code || 'unknown';
- return $.Deferred().reject( code, data );
- }
- return data;
- } );
- return innerPromise;
- } )
- .promise( {
- abort: function () {
- apiPromise.abort();
- if ( innerPromise ) {
- innerPromise.abort();
- }
- }
- } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.login
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * Allows to retrieve a specific or a set of
- * messages to be added to mw.messages and returned
- * by the Api.
- *
- * @class mw.Api.plugin.messages
- * @since 1.27
- */
-( function ( mw, $ ) {
- 'use strict';
-
- $.extend( mw.Api.prototype, {
- /**
- * Get a set of messages.
- *
- * @param {Array} messages Messages to retrieve
- * @param {Object} [options] Additional parameters for the API call
- * @return {jQuery.Promise}
- */
- getMessages: function ( messages, options ) {
- options = options || {};
- return this.get( $.extend( {
- action: 'query',
- meta: 'allmessages',
- ammessages: messages,
- amlang: mw.config.get( 'wgUserLanguage' ),
- formatversion: 2
- }, options ) ).then( function ( data ) {
- var result = {};
-
- data.query.allmessages.forEach( function ( obj ) {
- if ( !obj.missing ) {
- result[ obj.name ] = obj.content;
- }
- } );
-
- return result;
- } );
- },
-
- /**
- * Loads a set of messages and add them to mw.messages.
- *
- * @param {Array} messages Messages to retrieve
- * @param {Object} [options] Additional parameters for the API call
- * @return {jQuery.Promise}
- */
- loadMessages: function ( messages, options ) {
- return this.getMessages( messages, options ).then( $.proxy( mw.messages, 'set' ) );
- },
-
- /**
- * Loads a set of messages and add them to mw.messages. Only messages that are not already known
- * are loaded. If all messages are known, the returned promise is resolved immediately.
- *
- * @param {Array} messages Messages to retrieve
- * @param {Object} [options] Additional parameters for the API call
- * @return {jQuery.Promise}
- */
- loadMessagesIfMissing: function ( messages, options ) {
- var missing = messages.filter( function ( msg ) {
- return !mw.message( msg ).exists();
- } );
-
- if ( missing.length === 0 ) {
- return $.Deferred().resolve();
- }
-
- return this.getMessages( missing, options ).then( $.proxy( mw.messages, 'set' ) );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.messages
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.options
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
-
- /**
- * Asynchronously save the value of a single user option using the API. See #saveOptions.
- *
- * @param {string} name
- * @param {string|null} value
- * @return {jQuery.Promise}
- */
- saveOption: function ( name, value ) {
- var param = {};
- param[ name ] = value;
- return this.saveOptions( param );
- },
-
- /**
- * Asynchronously save the values of user options using the API.
- *
- * If a value of `null` is provided, the given option will be reset to the default value.
- *
- * Any warnings returned by the API, including warnings about invalid option names or values,
- * are ignored. However, do not rely on this behavior.
- *
- * If necessary, the options will be saved using several sequential API requests. Only one promise
- * is always returned that will be resolved when all requests complete.
- *
- * @param {Object} options Options as a `{ name: value, … }` object
- * @return {jQuery.Promise}
- */
- saveOptions: function ( options ) {
- var name, value, bundleable,
- grouped = [],
- promise = $.Deferred().resolve();
-
- for ( name in options ) {
- value = options[ name ] === null ? null : String( options[ name ] );
-
- // Can we bundle this option, or does it need a separate request?
- if ( this.defaults.useUS ) {
- bundleable = name.indexOf( '=' ) === -1;
- } else {
- bundleable =
- ( value === null || value.indexOf( '|' ) === -1 ) &&
- ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
- }
-
- if ( bundleable ) {
- if ( value !== null ) {
- grouped.push( name + '=' + value );
- } else {
- // Omitting value resets the option
- grouped.push( name );
- }
- } else {
- if ( value !== null ) {
- promise = promise.then( function ( name, value ) {
- return this.postWithToken( 'csrf', {
- formatversion: 2,
- action: 'options',
- optionname: name,
- optionvalue: value
- } );
- }.bind( this, name, value ) );
- } else {
- // Omitting value resets the option
- promise = promise.then( function ( name ) {
- return this.postWithToken( 'csrf', {
- formatversion: 2,
- action: 'options',
- optionname: name
- } );
- }.bind( this, name ) );
- }
- }
- }
-
- if ( grouped.length ) {
- promise = promise.then( function () {
- return this.postWithToken( 'csrf', {
- formatversion: 2,
- action: 'options',
- change: grouped
- } );
- }.bind( this ) );
- }
-
- return promise;
- }
-
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.options
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.parse
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
- /**
- * Convenience method for 'action=parse'.
- *
- * @param {string|mw.Title} content Content to parse, either as a wikitext string or
- * a mw.Title.
- * @param {Object} additionalParams Parameters object to set custom settings, e.g.
- * redirects, sectionpreview. prop should not be overridden.
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {string} return.done.data Parsed HTML of `wikitext`.
- */
- parse: function ( content, additionalParams ) {
- var apiPromise,
- config = $.extend( {
- formatversion: 2,
- action: 'parse',
- contentmodel: 'wikitext'
- }, additionalParams );
-
- if ( mw.Title && content instanceof mw.Title ) {
- // Parse existing page
- config.page = content.getPrefixedDb();
- } else {
- // Parse wikitext from input
- config.text = String( content );
- }
-
- apiPromise = this.get( config );
-
- return apiPromise
- .then( function ( data ) {
- return data.parse.text;
- } )
- .promise( { abort: apiPromise.abort } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.parse
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.rollback
- * @since 1.28
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
- /**
- * Convenience method for `action=rollback`.
- *
- * @param {string|mw.Title} page
- * @param {string} user
- * @param {Object} [params] Additional parameters
- * @return {jQuery.Promise}
- */
- rollback: function ( page, user, params ) {
- return this.postWithToken( 'rollback', $.extend( {
- action: 'rollback',
- title: String( page ),
- user: user,
- uselang: mw.config.get( 'wgUserLanguage' )
- }, params ) ).then( function ( data ) {
- return data.rollback;
- } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.rollback
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * Provides an interface for uploading files to MediaWiki.
- *
- * @class mw.Api.plugin.upload
- * @singleton
- */
-( function ( mw, $ ) {
- var nonce = 0,
- fieldsAllowed = {
- stash: true,
- filekey: true,
- filename: true,
- comment: true,
- text: true,
- watchlist: true,
- ignorewarnings: true,
- chunk: true,
- offset: true,
- filesize: true,
- async: true
- };
-
- /**
- * Get nonce for iframe IDs on the page.
- *
- * @private
- * @return {number}
- */
- function getNonce() {
- return nonce++;
- }
-
- /**
- * Given a non-empty object, return one of its keys.
- *
- * @private
- * @param {Object} obj
- * @return {string}
- */
- function getFirstKey( obj ) {
- var key;
- for ( key in obj ) {
- if ( obj.hasOwnProperty( key ) ) {
- return key;
- }
- }
- }
-
- /**
- * Get new iframe object for an upload.
- *
- * @private
- * @param {string} id
- * @return {HTMLIframeElement}
- */
- function getNewIframe( id ) {
- var frame = document.createElement( 'iframe' );
- frame.id = id;
- frame.name = id;
- return frame;
- }
-
- /**
- * Shortcut for getting hidden inputs
- *
- * @private
- * @param {string} name
- * @param {string} val
- * @return {jQuery}
- */
- function getHiddenInput( name, val ) {
- return $( '<input>' ).attr( 'type', 'hidden' )
- .attr( 'name', name )
- .val( val );
- }
-
- /**
- * Process the result of the form submission, returned to an iframe.
- * This is the iframe's onload event.
- *
- * @param {HTMLIframeElement} iframe Iframe to extract result from
- * @return {Object} Response from the server. The return value may or may
- * not be an XMLDocument, this code was copied from elsewhere, so if you
- * see an unexpected return type, please file a bug.
- */
- function processIframeResult( iframe ) {
- var json,
- doc = iframe.contentDocument || frames[ iframe.id ].document;
-
- if ( doc.XMLDocument ) {
- // The response is a document property in IE
- return doc.XMLDocument;
- }
-
- if ( doc.body ) {
- // Get the json string
- // We're actually searching through an HTML doc here --
- // according to mdale we need to do this
- // because IE does not load JSON properly in an iframe
- json = $( doc.body ).find( 'pre' ).text();
-
- return JSON.parse( json );
- }
-
- // Response is a xml document
- return doc;
- }
-
- function formDataAvailable() {
- return window.FormData !== undefined &&
- window.File !== undefined &&
- window.File.prototype.slice !== undefined;
- }
-
- $.extend( mw.Api.prototype, {
- /**
- * Upload a file to MediaWiki.
- *
- * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
- * iframe if it doesn't.
- *
- * Caveats of iframe upload:
- * - The returned jQuery.Promise will not receive `progress` notifications during the upload
- * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
- * - You must pass a HTMLInputElement and not a File for it to be possible
- *
- * @param {HTMLInputElement|File|Blob} file HTML input type=file element with a file already inside
- * of it, or a File object.
- * @param {Object} data Other upload options, see action=upload API docs for more
- * @return {jQuery.Promise}
- */
- upload: function ( file, data ) {
- var isFileInput, canUseFormData;
-
- isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
-
- if ( formDataAvailable() && isFileInput && file.files ) {
- file = file.files[ 0 ];
- }
-
- if ( !file ) {
- throw new Error( 'No file' );
- }
-
- // Blobs are allowed in formdata uploads, it turns out
- canUseFormData = formDataAvailable() && ( file instanceof window.File || file instanceof window.Blob );
-
- if ( !isFileInput && !canUseFormData ) {
- throw new Error( 'Unsupported argument type passed to mw.Api.upload' );
- }
-
- if ( canUseFormData ) {
- return this.uploadWithFormData( file, data );
- }
-
- return this.uploadWithIframe( file, data );
- },
-
- /**
- * Upload a file to MediaWiki with an iframe and a form.
- *
- * This method is necessary for browsers without the File/FormData
- * APIs, and continues to work in browsers with those APIs.
- *
- * The rough sketch of how this method works is as follows:
- * 1. An iframe is loaded with no content.
- * 2. A form is submitted with the passed-in file input and some extras.
- * 3. The MediaWiki API receives that form data, and sends back a response.
- * 4. The response is sent to the iframe, because we set target=(iframe id)
- * 5. The response is parsed out of the iframe's document, and passed back
- * through the promise.
- *
- * @private
- * @param {HTMLInputElement} file The file input with a file in it.
- * @param {Object} data Other upload options, see action=upload API docs for more
- * @return {jQuery.Promise}
- */
- uploadWithIframe: function ( file, data ) {
- var key,
- tokenPromise = $.Deferred(),
- api = this,
- deferred = $.Deferred(),
- nonce = getNonce(),
- id = 'uploadframe-' + nonce,
- $form = $( '<form>' ),
- iframe = getNewIframe( id ),
- $iframe = $( iframe );
-
- for ( key in data ) {
- if ( !fieldsAllowed[ key ] ) {
- delete data[ key ];
- }
- }
-
- data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
- $form.addClass( 'mw-api-upload-form' );
-
- $form.css( 'display', 'none' )
- .attr( {
- action: this.defaults.ajax.url,
- method: 'POST',
- target: id,
- enctype: 'multipart/form-data'
- } );
-
- $iframe.one( 'load', function () {
- $iframe.one( 'load', function () {
- var result = processIframeResult( iframe );
- deferred.notify( 1 );
-
- if ( !result ) {
- deferred.reject( 'ok-but-empty', 'No response from API on upload attempt.' );
- } else if ( result.error ) {
- if ( result.error.code === 'badtoken' ) {
- api.badToken( 'csrf' );
- }
-
- deferred.reject( result.error.code, result );
- } else if ( result.upload && result.upload.warnings ) {
- deferred.reject( getFirstKey( result.upload.warnings ), result );
- } else {
- deferred.resolve( result );
- }
- } );
- tokenPromise.done( function () {
- $form.submit();
- } );
- } );
-
- $iframe.on( 'error', function ( error ) {
- deferred.reject( 'http', error );
- } );
-
- $iframe.prop( 'src', 'about:blank' ).hide();
-
- file.name = 'file';
-
- $.each( data, function ( key, val ) {
- $form.append( getHiddenInput( key, val ) );
- } );
-
- if ( !data.filename && !data.stash ) {
- throw new Error( 'Filename not included in file data.' );
- }
-
- if ( this.needToken() ) {
- this.getEditToken().then( function ( token ) {
- $form.append( getHiddenInput( 'token', token ) );
- tokenPromise.resolve();
- }, tokenPromise.reject );
- } else {
- tokenPromise.resolve();
- }
-
- $( 'body' ).append( $form, $iframe );
-
- deferred.always( function () {
- $form.remove();
- $iframe.remove();
- } );
-
- return deferred.promise();
- },
-
- /**
- * Uploads a file using the FormData API.
- *
- * @private
- * @param {File} file
- * @param {Object} data Other upload options, see action=upload API docs for more
- * @return {jQuery.Promise}
- */
- uploadWithFormData: function ( file, data ) {
- var key, request,
- deferred = $.Deferred();
-
- for ( key in data ) {
- if ( !fieldsAllowed[ key ] ) {
- delete data[ key ];
- }
- }
-
- data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
- if ( !data.chunk ) {
- data.file = file;
- }
-
- if ( !data.filename && !data.stash ) {
- throw new Error( 'Filename not included in file data.' );
- }
-
- // Use this.postWithEditToken() or this.post()
- request = this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
- // Use FormData (if we got here, we know that it's available)
- contentType: 'multipart/form-data',
- // No timeout (default from mw.Api is 30 seconds)
- timeout: 0,
- // Provide upload progress notifications
- xhr: function () {
- var xhr = $.ajaxSettings.xhr();
- if ( xhr.upload ) {
- // need to bind this event before we open the connection (see note at
- // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
- xhr.upload.addEventListener( 'progress', function ( ev ) {
- if ( ev.lengthComputable ) {
- deferred.notify( ev.loaded / ev.total );
- }
- } );
- }
- return xhr;
- }
- } )
- .done( function ( result ) {
- deferred.notify( 1 );
- if ( result.upload && result.upload.warnings ) {
- deferred.reject( getFirstKey( result.upload.warnings ), result );
- } else {
- deferred.resolve( result );
- }
- } )
- .fail( function ( errorCode, result ) {
- deferred.notify( 1 );
- deferred.reject( errorCode, result );
- } );
-
- return deferred.promise( { abort: request.abort } );
- },
-
- /**
- * Upload a file in several chunks.
- *
- * @param {File} file
- * @param {Object} data Other upload options, see action=upload API docs for more
- * @param {number} [chunkSize] Size (in bytes) per chunk (default: 5MB)
- * @param {number} [chunkRetries] Amount of times to retry a failed chunk (default: 1)
- * @return {jQuery.Promise}
- */
- chunkedUpload: function ( file, data, chunkSize, chunkRetries ) {
- var start, end, promise, next, active,
- deferred = $.Deferred();
-
- chunkSize = chunkSize === undefined ? 5 * 1024 * 1024 : chunkSize;
- chunkRetries = chunkRetries === undefined ? 1 : chunkRetries;
-
- if ( !data.filename ) {
- throw new Error( 'Filename not included in file data.' );
- }
-
- // Submit first chunk to get the filekey
- active = promise = this.uploadChunk( file, data, 0, chunkSize, '', chunkRetries )
- .done( chunkSize >= file.size ? deferred.resolve : null )
- .fail( deferred.reject )
- .progress( deferred.notify );
-
- // Now iteratively submit the rest of the chunks
- for ( start = chunkSize; start < file.size; start += chunkSize ) {
- end = Math.min( start + chunkSize, file.size );
- next = $.Deferred();
-
- // We could simply chain one this.uploadChunk after another with
- // .then(), but then we'd hit an `Uncaught RangeError: Maximum
- // call stack size exceeded` at as low as 1024 calls in Firefox
- // 47. This'll work around it, but comes with the drawback of
- // having to properly relay the results to the returned promise.
- // eslint-disable-next-line no-loop-func
- promise.done( function ( start, end, next, result ) {
- var filekey = result.upload.filekey;
- active = this.uploadChunk( file, data, start, end, filekey, chunkRetries )
- .done( end === file.size ? deferred.resolve : next.resolve )
- .fail( deferred.reject )
- .progress( deferred.notify );
- // start, end & next must be bound to closure, or they'd have
- // changed by the time the promises are resolved
- }.bind( this, start, end, next ) );
-
- promise = next;
- }
-
- return deferred.promise( { abort: active.abort } );
- },
-
- /**
- * Uploads 1 chunk.
- *
- * @private
- * @param {File} file
- * @param {Object} data Other upload options, see action=upload API docs for more
- * @param {number} start Chunk start position
- * @param {number} end Chunk end position
- * @param {string} [filekey] File key, for follow-up chunks
- * @param {number} [retries] Amount of times to retry request
- * @return {jQuery.Promise}
- */
- uploadChunk: function ( file, data, start, end, filekey, retries ) {
- var upload,
- api = this,
- chunk = this.slice( file, start, end );
-
- // When uploading in chunks, we're going to be issuing a lot more
- // requests and there's always a chance of 1 getting dropped.
- // In such case, it could be useful to try again: a network hickup
- // doesn't necessarily have to result in upload failure...
- retries = retries === undefined ? 1 : retries;
-
- data.filesize = file.size;
- data.chunk = chunk;
- data.offset = start;
-
- // filekey must only be added when uploading follow-up chunks; the
- // first chunk should never have a filekey (it'll be generated)
- if ( filekey && start !== 0 ) {
- data.filekey = filekey;
- }
-
- upload = this.uploadWithFormData( file, data );
- return upload.then(
- null,
- function ( code, result ) {
- var retry;
-
- // uploadWithFormData will reject uploads with warnings, but
- // these warnings could be "harmless" or recovered from
- // (e.g. exists-normalized, when it'll be renamed later)
- // In the case of (only) a warning, we still want to
- // continue the chunked upload until it completes: then
- // reject it - at least it's been fully uploaded by then and
- // failure handlers have a complete result object (including
- // possibly more warnings, e.g. duplicate)
- // This matches .upload, which also completes the upload.
- if ( result.upload && result.upload.warnings && code in result.upload.warnings ) {
- if ( end === file.size ) {
- // uploaded last chunk = reject with result data
- return $.Deferred().reject( code, result );
- } else {
- // still uploading chunks = resolve to keep going
- return $.Deferred().resolve( result );
- }
- }
-
- if ( retries === 0 ) {
- return $.Deferred().reject( code, result );
- }
-
- // If the call flat out failed, we may want to try again...
- retry = api.uploadChunk.bind( this, file, data, start, end, filekey, retries - 1 );
- return api.retry( code, result, retry );
- },
- function ( fraction ) {
- // Since we're only uploading small parts of a file, we
- // need to adjust the reported progress to reflect where
- // we actually are in the combined upload
- return ( start + fraction * ( end - start ) ) / file.size;
- }
- ).promise( { abort: upload.abort } );
- },
-
- /**
- * Launch the upload anew if it failed because of network issues.
- *
- * @private
- * @param {string} code Error code
- * @param {Object} result API result
- * @param {Function} callable
- * @return {jQuery.Promise}
- */
- retry: function ( code, result, callable ) {
- var uploadPromise,
- retryTimer,
- deferred = $.Deferred(),
- // Wrap around the callable, so that once it completes, it'll
- // resolve/reject the promise we'll return
- retry = function () {
- uploadPromise = callable();
- uploadPromise.then( deferred.resolve, deferred.reject );
- };
-
- // Don't retry if the request failed because we aborted it (or if
- // it's another kind of request failure)
- if ( code !== 'http' || result.textStatus === 'abort' ) {
- return deferred.reject( code, result );
- }
-
- retryTimer = setTimeout( retry, 1000 );
- return deferred.promise( { abort: function () {
- // Clear the scheduled upload, or abort if already in flight
- if ( retryTimer ) {
- clearTimeout( retryTimer );
- }
- if ( uploadPromise.abort ) {
- uploadPromise.abort();
- }
- } } );
- },
-
- /**
- * Slice a chunk out of a File object.
- *
- * @private
- * @param {File} file
- * @param {number} start
- * @param {number} stop
- * @return {Blob}
- */
- slice: function ( file, start, stop ) {
- if ( file.mozSlice ) {
- // FF <= 12
- return file.mozSlice( start, stop, file.type );
- } else if ( file.webkitSlice ) {
- // Chrome <= 20
- return file.webkitSlice( start, stop, file.type );
- } else {
- // On really old browser versions (before slice was prefixed),
- // slice() would take (start, length) instead of (start, end)
- // We'll ignore that here...
- return file.slice( start, stop, file.type );
- }
- },
-
- /**
- * This function will handle how uploads to stash (via uploadToStash or
- * chunkedUploadToStash) are resolved/rejected.
- *
- * After a successful stash, it'll resolve with a callback which, when
- * called, will finalize the upload in stash (with the given data, or
- * with additional/conflicting data)
- *
- * A failed stash can still be recovered from as long as 'filekey' is
- * present. In that case, it'll also resolve with the callback to
- * finalize the upload (all warnings are then ignored.)
- * Otherwise, it'll just reject as you'd expect, with code & result.
- *
- * @private
- * @param {jQuery.Promise} uploadPromise
- * @param {Object} data
- * @return {jQuery.Promise}
- * @return {Function} return.finishUpload Call this function to finish the upload.
- * @return {Object} return.finishUpload.data Additional data for the upload.
- * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
- * @return {Object} return.finishUpload.return.data API return value for the final upload
- */
- finishUploadToStash: function ( uploadPromise, data ) {
- var filekey,
- api = this;
-
- function finishUpload( moreData ) {
- return api.uploadFromStash( filekey, $.extend( data, moreData ) );
- }
-
- return uploadPromise.then(
- function ( result ) {
- filekey = result.upload.filekey;
- return finishUpload;
- },
- function ( errorCode, result ) {
- if ( result && result.upload && result.upload.filekey ) {
- // Ignore any warnings if 'filekey' was returned, that's all we care about
- filekey = result.upload.filekey;
- return $.Deferred().resolve( finishUpload );
- }
- return $.Deferred().reject( errorCode, result );
- }
- );
- },
-
- /**
- * Upload a file to the stash.
- *
- * This function will return a promise, which when resolved, will pass back a function
- * to finish the stash upload. You can call that function with an argument containing
- * more, or conflicting, data to pass to the server. For example:
- *
- * // upload a file to the stash with a placeholder filename
- * api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
- * // finish is now the function we can use to finalize the upload
- * // pass it a new filename from user input to override the initial value
- * finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
- * // the upload is complete, data holds the API response
- * } );
- * } );
- *
- * @param {File|HTMLInputElement} file
- * @param {Object} [data]
- * @return {jQuery.Promise}
- * @return {Function} return.finishUpload Call this function to finish the upload.
- * @return {Object} return.finishUpload.data Additional data for the upload.
- * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
- * @return {Object} return.finishUpload.return.data API return value for the final upload
- */
- uploadToStash: function ( file, data ) {
- var promise;
-
- if ( !data.filename ) {
- throw new Error( 'Filename not included in file data.' );
- }
-
- promise = this.upload( file, { stash: true, filename: data.filename } );
-
- return this.finishUploadToStash( promise, data );
- },
-
- /**
- * Upload a file to the stash, in chunks.
- *
- * This function will return a promise, which when resolved, will pass back a function
- * to finish the stash upload.
- *
- * @see #method-uploadToStash
- * @param {File|HTMLInputElement} file
- * @param {Object} [data]
- * @param {number} [chunkSize] Size (in bytes) per chunk (default: 5MB)
- * @param {number} [chunkRetries] Amount of times to retry a failed chunk (default: 1)
- * @return {jQuery.Promise}
- * @return {Function} return.finishUpload Call this function to finish the upload.
- * @return {Object} return.finishUpload.data Additional data for the upload.
- * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
- * @return {Object} return.finishUpload.return.data API return value for the final upload
- */
- chunkedUploadToStash: function ( file, data, chunkSize, chunkRetries ) {
- var promise;
-
- if ( !data.filename ) {
- throw new Error( 'Filename not included in file data.' );
- }
-
- promise = this.chunkedUpload(
- file,
- { stash: true, filename: data.filename },
- chunkSize,
- chunkRetries
- );
-
- return this.finishUploadToStash( promise, data );
- },
-
- /**
- * Finish an upload in the stash.
- *
- * @param {string} filekey
- * @param {Object} data
- * @return {jQuery.Promise}
- */
- uploadFromStash: function ( filekey, data ) {
- data.filekey = filekey;
- data.action = 'upload';
- data.format = 'json';
-
- if ( !data.filename ) {
- throw new Error( 'Filename not included in file data.' );
- }
-
- return this.postWithEditToken( data ).then( function ( result ) {
- if ( result.upload && result.upload.warnings ) {
- return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
- }
- return result;
- } );
- },
-
- needToken: function () {
- return true;
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.upload
- */
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.user
- * @since 1.27
- */
-( function ( mw, $ ) {
-
- $.extend( mw.Api.prototype, {
-
- /**
- * Get the current user's groups and rights.
- *
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {Object} return.done.userInfo
- * @return {string[]} return.done.userInfo.groups User groups that the current user belongs to
- * @return {string[]} return.done.userInfo.rights Current user's rights
- */
- getUserInfo: function () {
- return this.get( {
- action: 'query',
- meta: 'userinfo',
- uiprop: [ 'groups', 'rights' ]
- } ).then( function ( data ) {
- if ( data.query && data.query.userinfo ) {
- return data.query.userinfo;
- }
- return $.Deferred().reject().promise();
- } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.user
- */
-
-}( mediaWiki, jQuery ) );
+++ /dev/null
-/**
- * @class mw.Api.plugin.watch
- * @since 1.19
- */
-( function ( mw, $ ) {
-
- /**
- * @private
- * @static
- * @context mw.Api
- *
- * @param {string|mw.Title|string[]|mw.Title[]} pages Full page name or instance of mw.Title, or an
- * array thereof. If an array is passed, the return value passed to the promise will also be an
- * array of appropriate objects.
- * @param {Object} [addParams]
- * @return {jQuery.Promise}
- * @return {Function} return.done
- * @return {Object|Object[]} return.done.watch Object or list of objects (depends on the `pages`
- * parameter)
- * @return {string} return.done.watch.title Full pagename
- * @return {boolean} return.done.watch.watched Whether the page is now watched or unwatched
- */
- function doWatchInternal( pages, addParams ) {
- // XXX: Parameter addParams is undocumented because we inherit this
- // documentation in the public method...
- var apiPromise = this.postWithToken( 'watch',
- $.extend(
- {
- formatversion: 2,
- action: 'watch',
- titles: Array.isArray( pages ) ? pages : String( pages )
- },
- addParams
- )
- );
-
- return apiPromise
- .then( function ( data ) {
- // If a single page was given (not an array) respond with a single item as well.
- return Array.isArray( pages ) ? data.watch : data.watch[ 0 ];
- } )
- .promise( { abort: apiPromise.abort } );
- }
-
- $.extend( mw.Api.prototype, {
- /**
- * Convenience method for `action=watch`.
- *
- * @inheritdoc #doWatchInternal
- */
- watch: function ( pages ) {
- return doWatchInternal.call( this, pages );
- },
-
- /**
- * Convenience method for `action=watch&unwatch=1`.
- *
- * @inheritdoc #doWatchInternal
- */
- unwatch: function ( pages ) {
- return doWatchInternal.call( this, pages, { unwatch: 1 } );
- }
- } );
-
- /**
- * @class mw.Api
- * @mixins mw.Api.plugin.watch
- */
-
-}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.category
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Determine if a category exists.
+ *
+ * @param {mw.Title|string} title
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {boolean} return.done.isCategory Whether the category exists.
+ */
+ isCategory: function ( title ) {
+ var apiPromise = this.get( {
+ formatversion: 2,
+ prop: 'categoryinfo',
+ titles: [ String( title ) ]
+ } );
+
+ return apiPromise
+ .then( function ( data ) {
+ return !!(
+ data.query && // query is missing on title=""
+ data.query.pages && // query.pages is missing on title="#" or title="mw:"
+ data.query.pages[ 0 ].categoryinfo
+ );
+ } )
+ .promise( { abort: apiPromise.abort } );
+ },
+
+ /**
+ * Get a list of categories that match a certain prefix.
+ *
+ * E.g. given "Foo", return "Food", "Foolish people", "Foosball tables"...
+ *
+ * @param {string} prefix Prefix to match.
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string[]} return.done.categories Matched categories
+ */
+ getCategoriesByPrefix: function ( prefix ) {
+ // Fetch with allpages to only get categories that have a corresponding description page.
+ var apiPromise = this.get( {
+ formatversion: 2,
+ list: 'allpages',
+ apprefix: prefix,
+ apnamespace: mw.config.get( 'wgNamespaceIds' ).category
+ } );
+
+ return apiPromise
+ .then( function ( data ) {
+ return data.query.allpages.map( function ( category ) {
+ return new mw.Title( category.title ).getMainText();
+ } );
+ } )
+ .promise( { abort: apiPromise.abort } );
+ },
+
+ /**
+ * Get the categories that a particular page on the wiki belongs to.
+ *
+ * @param {mw.Title|string} title
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {boolean|mw.Title[]} return.done.categories List of category titles or false
+ * if title was not found.
+ */
+ getCategories: function ( title ) {
+ var apiPromise = this.get( {
+ formatversion: 2,
+ prop: 'categories',
+ titles: [ String( title ) ]
+ } );
+
+ return apiPromise
+ .then( function ( data ) {
+ var page;
+
+ if ( !data.query || !data.query.pages ) {
+ return false;
+ }
+ page = data.query.pages[ 0 ];
+ if ( !page.categories ) {
+ return false;
+ }
+ return page.categories.map( function ( cat ) {
+ return new mw.Title( cat.title );
+ } );
+ } )
+ .promise( { abort: apiPromise.abort } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.category
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.edit
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+
+ /**
+ * Post to API with csrf token. If we have no token, get one and try to post.
+ * If we have a cached token try using that, and if it fails, blank out the
+ * cached token and start over.
+ *
+ * @param {Object} params API parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} See #post
+ */
+ postWithEditToken: function ( params, ajaxOptions ) {
+ return this.postWithToken( 'csrf', params, ajaxOptions );
+ },
+
+ /**
+ * API helper to grab a csrf token.
+ *
+ * @return {jQuery.Promise} Received token.
+ */
+ getEditToken: function () {
+ return this.getToken( 'csrf' );
+ },
+
+ /**
+ * Create a new page.
+ *
+ * Example:
+ *
+ * new mw.Api().create( 'Sandbox',
+ * { summary: 'Load sand particles.' },
+ * 'Sand.'
+ * );
+ *
+ * @since 1.28
+ * @param {mw.Title|string} title Page title
+ * @param {Object} params Edit API parameters
+ * @param {string} params.summary Edit summary
+ * @param {string} content
+ * @return {jQuery.Promise} API response
+ */
+ create: function ( title, params, content ) {
+ return this.postWithEditToken( $.extend( {
+ action: 'edit',
+ title: String( title ),
+ text: content,
+ formatversion: '2',
+
+ // Protect against errors and conflicts
+ assert: mw.config.get( 'wgUserName' ) ? 'user' : undefined,
+ createonly: true
+ }, params ) ).then( function ( data ) {
+ return data.edit;
+ } );
+ },
+
+ /**
+ * Edit an existing page.
+ *
+ * To create a new page, use #create() instead.
+ *
+ * Simple transformation:
+ *
+ * new mw.Api()
+ * .edit( 'Sandbox', function ( revision ) {
+ * return revision.content.replace( 'foo', 'bar' );
+ * } )
+ * .then( function () {
+ * console.log( 'Saved! ');
+ * } );
+ *
+ * Set save parameters by returning an object instead of a string:
+ *
+ * new mw.Api().edit(
+ * 'Sandbox',
+ * function ( revision ) {
+ * return {
+ * text: revision.content.replace( 'foo', 'bar' ),
+ * summary: 'Replace "foo" with "bar".',
+ * assert: 'bot',
+ * minor: true
+ * };
+ * }
+ * )
+ * .then( function () {
+ * console.log( 'Saved! ');
+ * } );
+ *
+ * Transform asynchronously by returning a promise.
+ *
+ * new mw.Api()
+ * .edit( 'Sandbox', function ( revision ) {
+ * return Spelling
+ * .corrections( revision.content )
+ * .then( function ( report ) {
+ * return {
+ * text: report.output,
+ * summary: report.changelog
+ * };
+ * } );
+ * } )
+ * .then( function () {
+ * console.log( 'Saved! ');
+ * } );
+ *
+ * @since 1.28
+ * @param {mw.Title|string} title Page title
+ * @param {Function} transform Callback that prepares the edit
+ * @param {Object} transform.revision Current revision
+ * @param {string} transform.revision.content Current revision content
+ * @param {string|Object|jQuery.Promise} transform.return New content, object with edit
+ * API parameters, or promise providing one of those.
+ * @return {jQuery.Promise} Edit API response
+ */
+ edit: function ( title, transform ) {
+ var basetimestamp, curtimestamp,
+ api = this;
+
+ title = String( title );
+
+ return api.get( {
+ action: 'query',
+ prop: 'revisions',
+ rvprop: [ 'content', 'timestamp' ],
+ titles: [ title ],
+ formatversion: '2',
+ curtimestamp: true
+ } )
+ .then( function ( data ) {
+ var page, revision;
+ if ( !data.query || !data.query.pages ) {
+ return $.Deferred().reject( 'unknown' );
+ }
+ page = data.query.pages[ 0 ];
+ if ( !page || page.invalid ) {
+ return $.Deferred().reject( 'invalidtitle' );
+ }
+ if ( page.missing ) {
+ return $.Deferred().reject( 'nocreate-missing' );
+ }
+ revision = page.revisions[ 0 ];
+ basetimestamp = revision.timestamp;
+ curtimestamp = data.curtimestamp;
+ return transform( {
+ timestamp: revision.timestamp,
+ content: revision.content
+ } );
+ } )
+ .then( function ( params ) {
+ var editParams = typeof params === 'object' ? params : { text: String( params ) };
+ return api.postWithEditToken( $.extend( {
+ action: 'edit',
+ title: title,
+ formatversion: '2',
+
+ // Protect against errors and conflicts
+ assert: mw.config.get( 'wgUserName' ) ? 'user' : undefined,
+ basetimestamp: basetimestamp,
+ starttimestamp: curtimestamp,
+ nocreate: true
+ }, editParams ) );
+ } )
+ .then( function ( data ) {
+ return data.edit;
+ } );
+ },
+
+ /**
+ * Post a new section to the page.
+ *
+ * @see #postWithEditToken
+ * @param {mw.Title|string} title Target page
+ * @param {string} header
+ * @param {string} message wikitext message
+ * @param {Object} [additionalParams] Additional API parameters, e.g. `{ redirect: true }`
+ * @return {jQuery.Promise}
+ */
+ newSection: function ( title, header, message, additionalParams ) {
+ return this.postWithEditToken( $.extend( {
+ action: 'edit',
+ section: 'new',
+ title: String( title ),
+ summary: header,
+ text: message
+ }, additionalParams ) );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.edit
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw, $ ) {
+
+ /**
+ * @class mw.Api
+ */
+
+ /**
+ * @property {Object} defaultOptions Default options for #ajax calls. Can be overridden by passing
+ * `options` to mw.Api constructor.
+ * @property {Object} defaultOptions.parameters Default query parameters for API requests.
+ * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
+ * @property {boolean} defaultOptions.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.
+ * @private
+ */
+ var defaultOptions = {
+ parameters: {
+ action: 'query',
+ format: 'json'
+ },
+ ajax: {
+ url: mw.util.wikiScript( 'api' ),
+ timeout: 30 * 1000, // 30 seconds
+ dataType: 'json'
+ }
+ },
+
+ // Keyed by ajax url and symbolic name for the individual request
+ promises = {};
+
+ function mapLegacyToken( action ) {
+ // Legacy types for backward-compatibility with API action=tokens.
+ var csrfActions = [
+ 'edit',
+ 'delete',
+ 'protect',
+ 'move',
+ 'block',
+ 'unblock',
+ 'email',
+ 'import',
+ 'options'
+ ];
+ if ( csrfActions.indexOf( action ) !== -1 ) {
+ mw.track( 'mw.deprecate', 'apitoken_' + action );
+ mw.log.warn( 'Use of the "' + action + '" token is deprecated. Use "csrf" instead.' );
+ return 'csrf';
+ }
+ return action;
+ }
+
+ // Pre-populate with fake ajax promises to save http requests for tokens
+ // we already have on the page via the user.tokens module (T36733).
+ promises[ defaultOptions.ajax.url ] = {};
+ $.each( mw.user.tokens.get(), function ( key, value ) {
+ // This requires #getToken to use the same key as user.tokens.
+ // Format: token-type + "Token" (eg. csrfToken, patrolToken, watchToken).
+ promises[ defaultOptions.ajax.url ][ key ] = $.Deferred()
+ .resolve( value )
+ .promise( { abort: function () {} } );
+ } );
+
+ /**
+ * Constructor to create an object to interact with the API of a particular MediaWiki server.
+ * mw.Api objects represent the API of a particular MediaWiki server.
+ *
+ * var api = new mw.Api();
+ * api.get( {
+ * action: 'query',
+ * meta: 'userinfo'
+ * } ).done( function ( 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'
+ * } ).done( function ( data ) {
+ * console.log( data );
+ * } );
+ *
+ * Since MW 1.26, boolean values for a parameter can be specified directly. If the value is
+ * `false` or `undefined`, the parameter will be omitted from the request, as required by the API.
+ *
+ * @constructor
+ * @param {Object} [options] See #defaultOptions documentation above. Can also be overridden for
+ * each individual request by passing them to #get or #post (or directly #ajax) later on.
+ */
+ mw.Api = function ( options ) {
+ options = options || {};
+
+ // Force a string if we got a mw.Uri object
+ if ( options.ajax && options.ajax.url !== undefined ) {
+ options.ajax.url = String( options.ajax.url );
+ }
+
+ options = $.extend( { useUS: !options.ajax || !options.ajax.url }, options );
+
+ options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
+ options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
+
+ this.defaults = options;
+ this.requests = [];
+ };
+
+ mw.Api.prototype = {
+ /**
+ * Abort all unfinished requests issued by this Api object.
+ *
+ * @method
+ */
+ abort: function () {
+ this.requests.forEach( function ( request ) {
+ if ( request ) {
+ request.abort();
+ }
+ } );
+ },
+
+ /**
+ * Perform API get request
+ *
+ * @param {Object} parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise}
+ */
+ get: function ( parameters, ajaxOptions ) {
+ ajaxOptions = ajaxOptions || {};
+ ajaxOptions.type = 'GET';
+ return this.ajax( parameters, ajaxOptions );
+ },
+
+ /**
+ * Perform API post request
+ *
+ * @param {Object} parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise}
+ */
+ 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 multi-valued parameters.
+ */
+ preprocessParameters: function ( parameters, useUS ) {
+ var 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
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} Done: API response data and the jqXHR object.
+ * Fail: Error code
+ */
+ ajax: function ( parameters, ajaxOptions ) {
+ var token, requestIndex,
+ api = this,
+ apiDeferred = $.Deferred(),
+ xhr, key, formData;
+
+ parameters = $.extend( {}, this.defaults.parameters, parameters );
+ ajaxOptions = $.extend( {}, this.defaults.ajax, ajaxOptions );
+
+ // 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'
+ ) {
+
+ formData = new FormData();
+
+ for ( 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 );
+ }
+
+ // Depending on server configuration, MediaWiki may forbid periods in URLs, due to an IE 6
+ // XSS bug. So let's escape them here. See WebRequest::checkUrlExtension() and T30235.
+ ajaxOptions.data = ajaxOptions.data.replace( /\./g, '%2E' );
+
+ 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;
+ }
+ }
+
+ // Make the AJAX request
+ xhr = $.ajax( ajaxOptions )
+ // If AJAX fails, reject API call with error code 'http'
+ // and details in second argument.
+ .fail( function ( xhr, textStatus, exception ) {
+ apiDeferred.reject( 'http', {
+ xhr: xhr,
+ textStatus: textStatus,
+ exception: exception
+ } );
+ } )
+ // AJAX success just means "200 OK" response, also check API error codes
+ .done( function ( result, textStatus, jqXHR ) {
+ var 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 );
+ }
+ } );
+
+ requestIndex = this.requests.length;
+ this.requests.push( xhr );
+ xhr.always( function () {
+ api.requests[ requestIndex ] = null;
+ } );
+ // Return the Promise
+ return apiDeferred.promise( { abort: xhr.abort } ).fail( function ( code, details ) {
+ if ( !( code === 'http' && details && details.textStatus === 'abort' ) ) {
+ mw.log( 'mw.Api error: ', code, details );
+ }
+ } );
+ },
+
+ /**
+ * Post to API with specified type of token. If we have no token, get one and try to post.
+ * If we have a cached token try using that, and if it fails, blank out the
+ * cached token and start over. For example to change an user option you could do:
+ *
+ * new mw.Api().postWithToken( 'csrf', {
+ * action: 'options',
+ * optionname: 'gender',
+ * optionvalue: 'female'
+ * } );
+ *
+ * @param {string} tokenType The name of the token, like options or edit.
+ * @param {Object} params API parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise} See #post
+ * @since 1.22
+ */
+ postWithToken: function ( tokenType, params, ajaxOptions ) {
+ var api = this,
+ abortedPromise = $.Deferred().reject( 'http',
+ { textStatus: 'abort', exception: 'abort' } ).promise(),
+ abortable,
+ aborted;
+
+ return api.getToken( tokenType, params.assert ).then( function ( token ) {
+ params.token = token;
+ // Request was aborted while token request was running, but we
+ // don't want to unnecessarily abort token requests, so abort
+ // a fake request instead
+ if ( aborted ) {
+ return abortedPromise;
+ }
+
+ return ( abortable = api.post( params, ajaxOptions ) ).catch(
+ // Error handler
+ function ( code ) {
+ if ( code === 'badtoken' ) {
+ api.badToken( tokenType );
+ // Try again, once
+ params.token = undefined;
+ abortable = null;
+ return api.getToken( tokenType, params.assert ).then( function ( token ) {
+ params.token = token;
+ if ( aborted ) {
+ return abortedPromise;
+ }
+
+ return ( abortable = api.post( params, ajaxOptions ) );
+ } );
+ }
+
+ // Let caller handle the error code
+ return $.Deferred().rejectWith( this, arguments );
+ }
+ );
+ } ).promise( { abort: function () {
+ if ( abortable ) {
+ abortable.abort();
+ } else {
+ aborted = true;
+ }
+ } } );
+ },
+
+ /**
+ * Get a token for a certain action from the API.
+ *
+ * The assert parameter is only for internal use by #postWithToken.
+ *
+ * @since 1.22
+ * @param {string} type Token type
+ * @param {string} [assert]
+ * @return {jQuery.Promise} Received token.
+ */
+ getToken: function ( type, assert ) {
+ var apiPromise, promiseGroup, d, reject;
+ type = mapLegacyToken( type );
+ promiseGroup = promises[ this.defaults.ajax.url ];
+ d = promiseGroup && promiseGroup[ type + 'Token' ];
+
+ if ( !promiseGroup ) {
+ promiseGroup = promises[ this.defaults.ajax.url ] = {};
+ }
+
+ if ( !d ) {
+ apiPromise = this.get( {
+ action: 'query',
+ meta: 'tokens',
+ type: type,
+ assert: assert
+ } );
+ reject = function () {
+ // Clear promise. Do not cache errors.
+ delete promiseGroup[ type + 'Token' ];
+
+ // Let caller handle the error code
+ return $.Deferred().rejectWith( this, arguments );
+ };
+ d = apiPromise
+ .then( function ( res ) {
+ if ( !res.query ) {
+ return reject( 'query-missing', res );
+ }
+ // If token type is unknown, it is omitted from the response
+ if ( !res.query.tokens[ type + 'token' ] ) {
+ return $.Deferred().reject( 'token-missing', res );
+ }
+ return res.query.tokens[ type + 'token' ];
+ }, reject )
+ // Attach abort handler
+ .promise( { abort: apiPromise.abort } );
+
+ // Store deferred now so that we can use it again even if it isn't ready yet
+ promiseGroup[ type + 'Token' ] = d;
+ }
+
+ return d;
+ },
+
+ /**
+ * Indicate that the cached token for a certain action of the API is bad.
+ *
+ * Call this if you get a 'badtoken' error when using the token returned by #getToken.
+ * You may also want to use #postWithToken instead, which invalidates bad cached tokens
+ * automatically.
+ *
+ * @param {string} type Token type
+ * @since 1.26
+ */
+ badToken: function ( type ) {
+ var promiseGroup = promises[ this.defaults.ajax.url ];
+
+ type = mapLegacyToken( type );
+ if ( promiseGroup ) {
+ delete promiseGroup[ type + 'Token' ];
+ }
+ }
+ };
+
+ /**
+ * @static
+ * @property {Array}
+ * Very incomplete and outdated list of errors we might receive from the API. Do not use.
+ * @deprecated since 1.29
+ */
+ mw.Api.errors = [
+ // occurs when POST aborted
+ // jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result
+ 'ok-but-empty',
+
+ // timeout
+ 'timeout',
+
+ // really a warning, but we treat it like an error
+ 'duplicate',
+ 'duplicate-archive',
+
+ // upload succeeded, but no image info.
+ // this is probably impossible, but might as well check for it
+ 'noimageinfo',
+ // remote errors, defined in API
+ 'uploaddisabled',
+ 'nomodule',
+ 'mustbeposted',
+ 'badaccess-groups',
+ 'missingresult',
+ 'missingparam',
+ 'invalid-file-key',
+ 'copyuploaddisabled',
+ 'mustbeloggedin',
+ 'empty-file',
+ 'file-too-large',
+ 'filetype-missing',
+ 'filetype-banned',
+ 'filetype-banned-type',
+ 'filename-tooshort',
+ 'illegal-filename',
+ 'verification-error',
+ 'hookaborted',
+ 'unknown-error',
+ 'internal-error',
+ 'overwrite',
+ 'badtoken',
+ 'fetchfileerror',
+ 'fileexists-shared-forbidden',
+ 'invalidtitle',
+ 'notloggedin',
+ 'autoblocked',
+ 'blocked',
+
+ // Stash-specific errors - expanded
+ 'stashfailed',
+ 'stasherror',
+ 'stashedfilenotfound',
+ 'stashpathinvalid',
+ 'stashfilestorage',
+ 'stashzerolength',
+ 'stashnotloggedin',
+ 'stashwrongowner',
+ 'stashnosuchfilekey'
+ ];
+ mw.log.deprecate( mw.Api, 'errors', mw.Api.errors, null, 'mw.Api.errors' );
+
+ /**
+ * @static
+ * @property {Array}
+ * Very incomplete and outdated list of warnings we might receive from the API. Do not use.
+ * @deprecated since 1.29
+ */
+ mw.Api.warnings = [
+ 'duplicate',
+ 'exists'
+ ];
+ mw.log.deprecate( mw.Api, 'warnings', mw.Api.warnings, null, 'mw.Api.warnings' );
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * Make the two-step login easier.
+ *
+ * @author Niklas Laxström
+ * @class mw.Api.plugin.login
+ * @since 1.22
+ */
+( function ( mw, $ ) {
+ 'use strict';
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * @param {string} username
+ * @param {string} password
+ * @return {jQuery.Promise} See mw.Api#post
+ */
+ login: function ( username, password ) {
+ var params, apiPromise, innerPromise,
+ api = this;
+
+ params = {
+ action: 'login',
+ lgname: username,
+ lgpassword: password
+ };
+
+ apiPromise = api.post( params );
+
+ return apiPromise
+ .then( function ( data ) {
+ params.lgtoken = data.login.token;
+ innerPromise = api.post( params )
+ .then( function ( data ) {
+ var code;
+ if ( data.login.result !== 'Success' ) {
+ // Set proper error code whenever possible
+ code = data.error && data.error.code || 'unknown';
+ return $.Deferred().reject( code, data );
+ }
+ return data;
+ } );
+ return innerPromise;
+ } )
+ .promise( {
+ abort: function () {
+ apiPromise.abort();
+ if ( innerPromise ) {
+ innerPromise.abort();
+ }
+ }
+ } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.login
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * Allows to retrieve a specific or a set of
+ * messages to be added to mw.messages and returned
+ * by the Api.
+ *
+ * @class mw.Api.plugin.messages
+ * @since 1.27
+ */
+( function ( mw, $ ) {
+ 'use strict';
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Get a set of messages.
+ *
+ * @param {Array} messages Messages to retrieve
+ * @param {Object} [options] Additional parameters for the API call
+ * @return {jQuery.Promise}
+ */
+ getMessages: function ( messages, options ) {
+ options = options || {};
+ return this.get( $.extend( {
+ action: 'query',
+ meta: 'allmessages',
+ ammessages: messages,
+ amlang: mw.config.get( 'wgUserLanguage' ),
+ formatversion: 2
+ }, options ) ).then( function ( data ) {
+ var result = {};
+
+ data.query.allmessages.forEach( function ( obj ) {
+ if ( !obj.missing ) {
+ result[ obj.name ] = obj.content;
+ }
+ } );
+
+ return result;
+ } );
+ },
+
+ /**
+ * Loads a set of messages and add them to mw.messages.
+ *
+ * @param {Array} messages Messages to retrieve
+ * @param {Object} [options] Additional parameters for the API call
+ * @return {jQuery.Promise}
+ */
+ loadMessages: function ( messages, options ) {
+ return this.getMessages( messages, options ).then( $.proxy( mw.messages, 'set' ) );
+ },
+
+ /**
+ * Loads a set of messages and add them to mw.messages. Only messages that are not already known
+ * are loaded. If all messages are known, the returned promise is resolved immediately.
+ *
+ * @param {Array} messages Messages to retrieve
+ * @param {Object} [options] Additional parameters for the API call
+ * @return {jQuery.Promise}
+ */
+ loadMessagesIfMissing: function ( messages, options ) {
+ var missing = messages.filter( function ( msg ) {
+ return !mw.message( msg ).exists();
+ } );
+
+ if ( missing.length === 0 ) {
+ return $.Deferred().resolve();
+ }
+
+ return this.getMessages( missing, options ).then( $.proxy( mw.messages, 'set' ) );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.messages
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.options
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+
+ /**
+ * Asynchronously save the value of a single user option using the API. See #saveOptions.
+ *
+ * @param {string} name
+ * @param {string|null} value
+ * @return {jQuery.Promise}
+ */
+ saveOption: function ( name, value ) {
+ var param = {};
+ param[ name ] = value;
+ return this.saveOptions( param );
+ },
+
+ /**
+ * Asynchronously save the values of user options using the API.
+ *
+ * If a value of `null` is provided, the given option will be reset to the default value.
+ *
+ * Any warnings returned by the API, including warnings about invalid option names or values,
+ * are ignored. However, do not rely on this behavior.
+ *
+ * If necessary, the options will be saved using several sequential API requests. Only one promise
+ * is always returned that will be resolved when all requests complete.
+ *
+ * @param {Object} options Options as a `{ name: value, … }` object
+ * @return {jQuery.Promise}
+ */
+ saveOptions: function ( options ) {
+ var name, value, bundleable,
+ grouped = [],
+ promise = $.Deferred().resolve();
+
+ for ( name in options ) {
+ value = options[ name ] === null ? null : String( options[ name ] );
+
+ // Can we bundle this option, or does it need a separate request?
+ if ( this.defaults.useUS ) {
+ bundleable = name.indexOf( '=' ) === -1;
+ } else {
+ bundleable =
+ ( value === null || value.indexOf( '|' ) === -1 ) &&
+ ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
+ }
+
+ if ( bundleable ) {
+ if ( value !== null ) {
+ grouped.push( name + '=' + value );
+ } else {
+ // Omitting value resets the option
+ grouped.push( name );
+ }
+ } else {
+ if ( value !== null ) {
+ promise = promise.then( function ( name, value ) {
+ return this.postWithToken( 'csrf', {
+ formatversion: 2,
+ action: 'options',
+ optionname: name,
+ optionvalue: value
+ } );
+ }.bind( this, name, value ) );
+ } else {
+ // Omitting value resets the option
+ promise = promise.then( function ( name ) {
+ return this.postWithToken( 'csrf', {
+ formatversion: 2,
+ action: 'options',
+ optionname: name
+ } );
+ }.bind( this, name ) );
+ }
+ }
+ }
+
+ if ( grouped.length ) {
+ promise = promise.then( function () {
+ return this.postWithToken( 'csrf', {
+ formatversion: 2,
+ action: 'options',
+ change: grouped
+ } );
+ }.bind( this ) );
+ }
+
+ return promise;
+ }
+
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.options
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.parse
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Convenience method for 'action=parse'.
+ *
+ * @param {string|mw.Title} content Content to parse, either as a wikitext string or
+ * a mw.Title.
+ * @param {Object} additionalParams Parameters object to set custom settings, e.g.
+ * redirects, sectionpreview. prop should not be overridden.
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {string} return.done.data Parsed HTML of `wikitext`.
+ */
+ parse: function ( content, additionalParams ) {
+ var apiPromise,
+ config = $.extend( {
+ formatversion: 2,
+ action: 'parse',
+ contentmodel: 'wikitext'
+ }, additionalParams );
+
+ if ( mw.Title && content instanceof mw.Title ) {
+ // Parse existing page
+ config.page = content.getPrefixedDb();
+ } else {
+ // Parse wikitext from input
+ config.text = String( content );
+ }
+
+ apiPromise = this.get( config );
+
+ return apiPromise
+ .then( function ( data ) {
+ return data.parse.text;
+ } )
+ .promise( { abort: apiPromise.abort } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.parse
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.rollback
+ * @since 1.28
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Convenience method for `action=rollback`.
+ *
+ * @param {string|mw.Title} page
+ * @param {string} user
+ * @param {Object} [params] Additional parameters
+ * @return {jQuery.Promise}
+ */
+ rollback: function ( page, user, params ) {
+ return this.postWithToken( 'rollback', $.extend( {
+ action: 'rollback',
+ title: String( page ),
+ user: user,
+ uselang: mw.config.get( 'wgUserLanguage' )
+ }, params ) ).then( function ( data ) {
+ return data.rollback;
+ } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.rollback
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * Provides an interface for uploading files to MediaWiki.
+ *
+ * @class mw.Api.plugin.upload
+ * @singleton
+ */
+( function ( mw, $ ) {
+ var nonce = 0,
+ fieldsAllowed = {
+ stash: true,
+ filekey: true,
+ filename: true,
+ comment: true,
+ text: true,
+ watchlist: true,
+ ignorewarnings: true,
+ chunk: true,
+ offset: true,
+ filesize: true,
+ async: true
+ };
+
+ /**
+ * Get nonce for iframe IDs on the page.
+ *
+ * @private
+ * @return {number}
+ */
+ function getNonce() {
+ return nonce++;
+ }
+
+ /**
+ * Given a non-empty object, return one of its keys.
+ *
+ * @private
+ * @param {Object} obj
+ * @return {string}
+ */
+ function getFirstKey( obj ) {
+ var key;
+ for ( key in obj ) {
+ if ( obj.hasOwnProperty( key ) ) {
+ return key;
+ }
+ }
+ }
+
+ /**
+ * Get new iframe object for an upload.
+ *
+ * @private
+ * @param {string} id
+ * @return {HTMLIframeElement}
+ */
+ function getNewIframe( id ) {
+ var frame = document.createElement( 'iframe' );
+ frame.id = id;
+ frame.name = id;
+ return frame;
+ }
+
+ /**
+ * Shortcut for getting hidden inputs
+ *
+ * @private
+ * @param {string} name
+ * @param {string} val
+ * @return {jQuery}
+ */
+ function getHiddenInput( name, val ) {
+ return $( '<input>' ).attr( 'type', 'hidden' )
+ .attr( 'name', name )
+ .val( val );
+ }
+
+ /**
+ * Process the result of the form submission, returned to an iframe.
+ * This is the iframe's onload event.
+ *
+ * @param {HTMLIframeElement} iframe Iframe to extract result from
+ * @return {Object} Response from the server. The return value may or may
+ * not be an XMLDocument, this code was copied from elsewhere, so if you
+ * see an unexpected return type, please file a bug.
+ */
+ function processIframeResult( iframe ) {
+ var json,
+ doc = iframe.contentDocument || frames[ iframe.id ].document;
+
+ if ( doc.XMLDocument ) {
+ // The response is a document property in IE
+ return doc.XMLDocument;
+ }
+
+ if ( doc.body ) {
+ // Get the json string
+ // We're actually searching through an HTML doc here --
+ // according to mdale we need to do this
+ // because IE does not load JSON properly in an iframe
+ json = $( doc.body ).find( 'pre' ).text();
+
+ return JSON.parse( json );
+ }
+
+ // Response is a xml document
+ return doc;
+ }
+
+ function formDataAvailable() {
+ return window.FormData !== undefined &&
+ window.File !== undefined &&
+ window.File.prototype.slice !== undefined;
+ }
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Upload a file to MediaWiki.
+ *
+ * The file will be uploaded using AJAX and FormData, if the browser supports it, or via an
+ * iframe if it doesn't.
+ *
+ * Caveats of iframe upload:
+ * - The returned jQuery.Promise will not receive `progress` notifications during the upload
+ * - It is incompatible with uploads to a foreign wiki using mw.ForeignApi
+ * - You must pass a HTMLInputElement and not a File for it to be possible
+ *
+ * @param {HTMLInputElement|File|Blob} file HTML input type=file element with a file already inside
+ * of it, or a File object.
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ upload: function ( file, data ) {
+ var isFileInput, canUseFormData;
+
+ isFileInput = file && file.nodeType === Node.ELEMENT_NODE;
+
+ if ( formDataAvailable() && isFileInput && file.files ) {
+ file = file.files[ 0 ];
+ }
+
+ if ( !file ) {
+ throw new Error( 'No file' );
+ }
+
+ // Blobs are allowed in formdata uploads, it turns out
+ canUseFormData = formDataAvailable() && ( file instanceof window.File || file instanceof window.Blob );
+
+ if ( !isFileInput && !canUseFormData ) {
+ throw new Error( 'Unsupported argument type passed to mw.Api.upload' );
+ }
+
+ if ( canUseFormData ) {
+ return this.uploadWithFormData( file, data );
+ }
+
+ return this.uploadWithIframe( file, data );
+ },
+
+ /**
+ * Upload a file to MediaWiki with an iframe and a form.
+ *
+ * This method is necessary for browsers without the File/FormData
+ * APIs, and continues to work in browsers with those APIs.
+ *
+ * The rough sketch of how this method works is as follows:
+ * 1. An iframe is loaded with no content.
+ * 2. A form is submitted with the passed-in file input and some extras.
+ * 3. The MediaWiki API receives that form data, and sends back a response.
+ * 4. The response is sent to the iframe, because we set target=(iframe id)
+ * 5. The response is parsed out of the iframe's document, and passed back
+ * through the promise.
+ *
+ * @private
+ * @param {HTMLInputElement} file The file input with a file in it.
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ uploadWithIframe: function ( file, data ) {
+ var key,
+ tokenPromise = $.Deferred(),
+ api = this,
+ deferred = $.Deferred(),
+ nonce = getNonce(),
+ id = 'uploadframe-' + nonce,
+ $form = $( '<form>' ),
+ iframe = getNewIframe( id ),
+ $iframe = $( iframe );
+
+ for ( key in data ) {
+ if ( !fieldsAllowed[ key ] ) {
+ delete data[ key ];
+ }
+ }
+
+ data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
+ $form.addClass( 'mw-api-upload-form' );
+
+ $form.css( 'display', 'none' )
+ .attr( {
+ action: this.defaults.ajax.url,
+ method: 'POST',
+ target: id,
+ enctype: 'multipart/form-data'
+ } );
+
+ $iframe.one( 'load', function () {
+ $iframe.one( 'load', function () {
+ var result = processIframeResult( iframe );
+ deferred.notify( 1 );
+
+ if ( !result ) {
+ deferred.reject( 'ok-but-empty', 'No response from API on upload attempt.' );
+ } else if ( result.error ) {
+ if ( result.error.code === 'badtoken' ) {
+ api.badToken( 'csrf' );
+ }
+
+ deferred.reject( result.error.code, result );
+ } else if ( result.upload && result.upload.warnings ) {
+ deferred.reject( getFirstKey( result.upload.warnings ), result );
+ } else {
+ deferred.resolve( result );
+ }
+ } );
+ tokenPromise.done( function () {
+ $form.submit();
+ } );
+ } );
+
+ $iframe.on( 'error', function ( error ) {
+ deferred.reject( 'http', error );
+ } );
+
+ $iframe.prop( 'src', 'about:blank' ).hide();
+
+ file.name = 'file';
+
+ $.each( data, function ( key, val ) {
+ $form.append( getHiddenInput( key, val ) );
+ } );
+
+ if ( !data.filename && !data.stash ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ if ( this.needToken() ) {
+ this.getEditToken().then( function ( token ) {
+ $form.append( getHiddenInput( 'token', token ) );
+ tokenPromise.resolve();
+ }, tokenPromise.reject );
+ } else {
+ tokenPromise.resolve();
+ }
+
+ $( 'body' ).append( $form, $iframe );
+
+ deferred.always( function () {
+ $form.remove();
+ $iframe.remove();
+ } );
+
+ return deferred.promise();
+ },
+
+ /**
+ * Uploads a file using the FormData API.
+ *
+ * @private
+ * @param {File} file
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @return {jQuery.Promise}
+ */
+ uploadWithFormData: function ( file, data ) {
+ var key, request,
+ deferred = $.Deferred();
+
+ for ( key in data ) {
+ if ( !fieldsAllowed[ key ] ) {
+ delete data[ key ];
+ }
+ }
+
+ data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data );
+ if ( !data.chunk ) {
+ data.file = file;
+ }
+
+ if ( !data.filename && !data.stash ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ // Use this.postWithEditToken() or this.post()
+ request = this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, {
+ // Use FormData (if we got here, we know that it's available)
+ contentType: 'multipart/form-data',
+ // No timeout (default from mw.Api is 30 seconds)
+ timeout: 0,
+ // Provide upload progress notifications
+ xhr: function () {
+ var xhr = $.ajaxSettings.xhr();
+ if ( xhr.upload ) {
+ // need to bind this event before we open the connection (see note at
+ // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress)
+ xhr.upload.addEventListener( 'progress', function ( ev ) {
+ if ( ev.lengthComputable ) {
+ deferred.notify( ev.loaded / ev.total );
+ }
+ } );
+ }
+ return xhr;
+ }
+ } )
+ .done( function ( result ) {
+ deferred.notify( 1 );
+ if ( result.upload && result.upload.warnings ) {
+ deferred.reject( getFirstKey( result.upload.warnings ), result );
+ } else {
+ deferred.resolve( result );
+ }
+ } )
+ .fail( function ( errorCode, result ) {
+ deferred.notify( 1 );
+ deferred.reject( errorCode, result );
+ } );
+
+ return deferred.promise( { abort: request.abort } );
+ },
+
+ /**
+ * Upload a file in several chunks.
+ *
+ * @param {File} file
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @param {number} [chunkSize] Size (in bytes) per chunk (default: 5MB)
+ * @param {number} [chunkRetries] Amount of times to retry a failed chunk (default: 1)
+ * @return {jQuery.Promise}
+ */
+ chunkedUpload: function ( file, data, chunkSize, chunkRetries ) {
+ var start, end, promise, next, active,
+ deferred = $.Deferred();
+
+ chunkSize = chunkSize === undefined ? 5 * 1024 * 1024 : chunkSize;
+ chunkRetries = chunkRetries === undefined ? 1 : chunkRetries;
+
+ if ( !data.filename ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ // Submit first chunk to get the filekey
+ active = promise = this.uploadChunk( file, data, 0, chunkSize, '', chunkRetries )
+ .done( chunkSize >= file.size ? deferred.resolve : null )
+ .fail( deferred.reject )
+ .progress( deferred.notify );
+
+ // Now iteratively submit the rest of the chunks
+ for ( start = chunkSize; start < file.size; start += chunkSize ) {
+ end = Math.min( start + chunkSize, file.size );
+ next = $.Deferred();
+
+ // We could simply chain one this.uploadChunk after another with
+ // .then(), but then we'd hit an `Uncaught RangeError: Maximum
+ // call stack size exceeded` at as low as 1024 calls in Firefox
+ // 47. This'll work around it, but comes with the drawback of
+ // having to properly relay the results to the returned promise.
+ // eslint-disable-next-line no-loop-func
+ promise.done( function ( start, end, next, result ) {
+ var filekey = result.upload.filekey;
+ active = this.uploadChunk( file, data, start, end, filekey, chunkRetries )
+ .done( end === file.size ? deferred.resolve : next.resolve )
+ .fail( deferred.reject )
+ .progress( deferred.notify );
+ // start, end & next must be bound to closure, or they'd have
+ // changed by the time the promises are resolved
+ }.bind( this, start, end, next ) );
+
+ promise = next;
+ }
+
+ return deferred.promise( { abort: active.abort } );
+ },
+
+ /**
+ * Uploads 1 chunk.
+ *
+ * @private
+ * @param {File} file
+ * @param {Object} data Other upload options, see action=upload API docs for more
+ * @param {number} start Chunk start position
+ * @param {number} end Chunk end position
+ * @param {string} [filekey] File key, for follow-up chunks
+ * @param {number} [retries] Amount of times to retry request
+ * @return {jQuery.Promise}
+ */
+ uploadChunk: function ( file, data, start, end, filekey, retries ) {
+ var upload,
+ api = this,
+ chunk = this.slice( file, start, end );
+
+ // When uploading in chunks, we're going to be issuing a lot more
+ // requests and there's always a chance of 1 getting dropped.
+ // In such case, it could be useful to try again: a network hickup
+ // doesn't necessarily have to result in upload failure...
+ retries = retries === undefined ? 1 : retries;
+
+ data.filesize = file.size;
+ data.chunk = chunk;
+ data.offset = start;
+
+ // filekey must only be added when uploading follow-up chunks; the
+ // first chunk should never have a filekey (it'll be generated)
+ if ( filekey && start !== 0 ) {
+ data.filekey = filekey;
+ }
+
+ upload = this.uploadWithFormData( file, data );
+ return upload.then(
+ null,
+ function ( code, result ) {
+ var retry;
+
+ // uploadWithFormData will reject uploads with warnings, but
+ // these warnings could be "harmless" or recovered from
+ // (e.g. exists-normalized, when it'll be renamed later)
+ // In the case of (only) a warning, we still want to
+ // continue the chunked upload until it completes: then
+ // reject it - at least it's been fully uploaded by then and
+ // failure handlers have a complete result object (including
+ // possibly more warnings, e.g. duplicate)
+ // This matches .upload, which also completes the upload.
+ if ( result.upload && result.upload.warnings && code in result.upload.warnings ) {
+ if ( end === file.size ) {
+ // uploaded last chunk = reject with result data
+ return $.Deferred().reject( code, result );
+ } else {
+ // still uploading chunks = resolve to keep going
+ return $.Deferred().resolve( result );
+ }
+ }
+
+ if ( retries === 0 ) {
+ return $.Deferred().reject( code, result );
+ }
+
+ // If the call flat out failed, we may want to try again...
+ retry = api.uploadChunk.bind( this, file, data, start, end, filekey, retries - 1 );
+ return api.retry( code, result, retry );
+ },
+ function ( fraction ) {
+ // Since we're only uploading small parts of a file, we
+ // need to adjust the reported progress to reflect where
+ // we actually are in the combined upload
+ return ( start + fraction * ( end - start ) ) / file.size;
+ }
+ ).promise( { abort: upload.abort } );
+ },
+
+ /**
+ * Launch the upload anew if it failed because of network issues.
+ *
+ * @private
+ * @param {string} code Error code
+ * @param {Object} result API result
+ * @param {Function} callable
+ * @return {jQuery.Promise}
+ */
+ retry: function ( code, result, callable ) {
+ var uploadPromise,
+ retryTimer,
+ deferred = $.Deferred(),
+ // Wrap around the callable, so that once it completes, it'll
+ // resolve/reject the promise we'll return
+ retry = function () {
+ uploadPromise = callable();
+ uploadPromise.then( deferred.resolve, deferred.reject );
+ };
+
+ // Don't retry if the request failed because we aborted it (or if
+ // it's another kind of request failure)
+ if ( code !== 'http' || result.textStatus === 'abort' ) {
+ return deferred.reject( code, result );
+ }
+
+ retryTimer = setTimeout( retry, 1000 );
+ return deferred.promise( { abort: function () {
+ // Clear the scheduled upload, or abort if already in flight
+ if ( retryTimer ) {
+ clearTimeout( retryTimer );
+ }
+ if ( uploadPromise.abort ) {
+ uploadPromise.abort();
+ }
+ } } );
+ },
+
+ /**
+ * Slice a chunk out of a File object.
+ *
+ * @private
+ * @param {File} file
+ * @param {number} start
+ * @param {number} stop
+ * @return {Blob}
+ */
+ slice: function ( file, start, stop ) {
+ if ( file.mozSlice ) {
+ // FF <= 12
+ return file.mozSlice( start, stop, file.type );
+ } else if ( file.webkitSlice ) {
+ // Chrome <= 20
+ return file.webkitSlice( start, stop, file.type );
+ } else {
+ // On really old browser versions (before slice was prefixed),
+ // slice() would take (start, length) instead of (start, end)
+ // We'll ignore that here...
+ return file.slice( start, stop, file.type );
+ }
+ },
+
+ /**
+ * This function will handle how uploads to stash (via uploadToStash or
+ * chunkedUploadToStash) are resolved/rejected.
+ *
+ * After a successful stash, it'll resolve with a callback which, when
+ * called, will finalize the upload in stash (with the given data, or
+ * with additional/conflicting data)
+ *
+ * A failed stash can still be recovered from as long as 'filekey' is
+ * present. In that case, it'll also resolve with the callback to
+ * finalize the upload (all warnings are then ignored.)
+ * Otherwise, it'll just reject as you'd expect, with code & result.
+ *
+ * @private
+ * @param {jQuery.Promise} uploadPromise
+ * @param {Object} data
+ * @return {jQuery.Promise}
+ * @return {Function} return.finishUpload Call this function to finish the upload.
+ * @return {Object} return.finishUpload.data Additional data for the upload.
+ * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
+ * @return {Object} return.finishUpload.return.data API return value for the final upload
+ */
+ finishUploadToStash: function ( uploadPromise, data ) {
+ var filekey,
+ api = this;
+
+ function finishUpload( moreData ) {
+ return api.uploadFromStash( filekey, $.extend( data, moreData ) );
+ }
+
+ return uploadPromise.then(
+ function ( result ) {
+ filekey = result.upload.filekey;
+ return finishUpload;
+ },
+ function ( errorCode, result ) {
+ if ( result && result.upload && result.upload.filekey ) {
+ // Ignore any warnings if 'filekey' was returned, that's all we care about
+ filekey = result.upload.filekey;
+ return $.Deferred().resolve( finishUpload );
+ }
+ return $.Deferred().reject( errorCode, result );
+ }
+ );
+ },
+
+ /**
+ * Upload a file to the stash.
+ *
+ * This function will return a promise, which when resolved, will pass back a function
+ * to finish the stash upload. You can call that function with an argument containing
+ * more, or conflicting, data to pass to the server. For example:
+ *
+ * // upload a file to the stash with a placeholder filename
+ * api.uploadToStash( file, { filename: 'testing.png' } ).done( function ( finish ) {
+ * // finish is now the function we can use to finalize the upload
+ * // pass it a new filename from user input to override the initial value
+ * finish( { filename: getFilenameFromUser() } ).done( function ( data ) {
+ * // the upload is complete, data holds the API response
+ * } );
+ * } );
+ *
+ * @param {File|HTMLInputElement} file
+ * @param {Object} [data]
+ * @return {jQuery.Promise}
+ * @return {Function} return.finishUpload Call this function to finish the upload.
+ * @return {Object} return.finishUpload.data Additional data for the upload.
+ * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
+ * @return {Object} return.finishUpload.return.data API return value for the final upload
+ */
+ uploadToStash: function ( file, data ) {
+ var promise;
+
+ if ( !data.filename ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ promise = this.upload( file, { stash: true, filename: data.filename } );
+
+ return this.finishUploadToStash( promise, data );
+ },
+
+ /**
+ * Upload a file to the stash, in chunks.
+ *
+ * This function will return a promise, which when resolved, will pass back a function
+ * to finish the stash upload.
+ *
+ * @see #method-uploadToStash
+ * @param {File|HTMLInputElement} file
+ * @param {Object} [data]
+ * @param {number} [chunkSize] Size (in bytes) per chunk (default: 5MB)
+ * @param {number} [chunkRetries] Amount of times to retry a failed chunk (default: 1)
+ * @return {jQuery.Promise}
+ * @return {Function} return.finishUpload Call this function to finish the upload.
+ * @return {Object} return.finishUpload.data Additional data for the upload.
+ * @return {jQuery.Promise} return.finishUpload.return API promise for the final upload
+ * @return {Object} return.finishUpload.return.data API return value for the final upload
+ */
+ chunkedUploadToStash: function ( file, data, chunkSize, chunkRetries ) {
+ var promise;
+
+ if ( !data.filename ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ promise = this.chunkedUpload(
+ file,
+ { stash: true, filename: data.filename },
+ chunkSize,
+ chunkRetries
+ );
+
+ return this.finishUploadToStash( promise, data );
+ },
+
+ /**
+ * Finish an upload in the stash.
+ *
+ * @param {string} filekey
+ * @param {Object} data
+ * @return {jQuery.Promise}
+ */
+ uploadFromStash: function ( filekey, data ) {
+ data.filekey = filekey;
+ data.action = 'upload';
+ data.format = 'json';
+
+ if ( !data.filename ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ return this.postWithEditToken( data ).then( function ( result ) {
+ if ( result.upload && result.upload.warnings ) {
+ return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
+ }
+ return result;
+ } );
+ },
+
+ needToken: function () {
+ return true;
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.upload
+ */
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.user
+ * @since 1.27
+ */
+( function ( mw, $ ) {
+
+ $.extend( mw.Api.prototype, {
+
+ /**
+ * Get the current user's groups and rights.
+ *
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {Object} return.done.userInfo
+ * @return {string[]} return.done.userInfo.groups User groups that the current user belongs to
+ * @return {string[]} return.done.userInfo.rights Current user's rights
+ */
+ getUserInfo: function () {
+ return this.get( {
+ action: 'query',
+ meta: 'userinfo',
+ uiprop: [ 'groups', 'rights' ]
+ } ).then( function ( data ) {
+ if ( data.query && data.query.userinfo ) {
+ return data.query.userinfo;
+ }
+ return $.Deferred().reject().promise();
+ } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.user
+ */
+
+}( mediaWiki, jQuery ) );
--- /dev/null
+/**
+ * @class mw.Api.plugin.watch
+ * @since 1.19
+ */
+( function ( mw, $ ) {
+
+ /**
+ * @private
+ * @static
+ * @context mw.Api
+ *
+ * @param {string|mw.Title|string[]|mw.Title[]} pages Full page name or instance of mw.Title, or an
+ * array thereof. If an array is passed, the return value passed to the promise will also be an
+ * array of appropriate objects.
+ * @param {Object} [addParams]
+ * @return {jQuery.Promise}
+ * @return {Function} return.done
+ * @return {Object|Object[]} return.done.watch Object or list of objects (depends on the `pages`
+ * parameter)
+ * @return {string} return.done.watch.title Full pagename
+ * @return {boolean} return.done.watch.watched Whether the page is now watched or unwatched
+ */
+ function doWatchInternal( pages, addParams ) {
+ // XXX: Parameter addParams is undocumented because we inherit this
+ // documentation in the public method...
+ var apiPromise = this.postWithToken( 'watch',
+ $.extend(
+ {
+ formatversion: 2,
+ action: 'watch',
+ titles: Array.isArray( pages ) ? pages : String( pages )
+ },
+ addParams
+ )
+ );
+
+ return apiPromise
+ .then( function ( data ) {
+ // If a single page was given (not an array) respond with a single item as well.
+ return Array.isArray( pages ) ? data.watch : data.watch[ 0 ];
+ } )
+ .promise( { abort: apiPromise.abort } );
+ }
+
+ $.extend( mw.Api.prototype, {
+ /**
+ * Convenience method for `action=watch`.
+ *
+ * @inheritdoc #doWatchInternal
+ */
+ watch: function ( pages ) {
+ return doWatchInternal.call( this, pages );
+ },
+
+ /**
+ * Convenience method for `action=watch&unwatch=1`.
+ *
+ * @inheritdoc #doWatchInternal
+ */
+ unwatch: function ( pages ) {
+ return doWatchInternal.call( this, pages, { unwatch: 1 } );
+ }
+ } );
+
+ /**
+ * @class mw.Api
+ * @mixins mw.Api.plugin.watch
+ */
+
+}( mediaWiki, jQuery ) );
'jquery.tablesorter',
'jquery.textSelection',
'mediawiki.api',
- 'mediawiki.api.category',
- 'mediawiki.api.messages',
- 'mediawiki.api.options',
- 'mediawiki.api.parse',
- 'mediawiki.api.upload',
- 'mediawiki.api.watch',
'mediawiki.ForeignApi.core',
'mediawiki.jqueryMsg',
'mediawiki.messagePoster',