From: Timo Tijhof Date: Tue, 29 Sep 2015 03:05:09 +0000 (-0700) Subject: Restructure /resources/src/mediawiki.api/ X-Git-Tag: 1.31.0-rc.0~9689 X-Git-Url: http://git.cyclocoop.org/%24self?a=commitdiff_plain;h=0bfdd927beb0861868c1bdb5c67cb17344b78cd0;p=lhc%2Fweb%2Fwiklou.git Restructure /resources/src/mediawiki.api/ Re-do Ifbb0f6751 in a smaller scope as a first step. Change-Id: I346f3587d3bfeaf0fe3467cd1f4dcf2d134ecc08 --- diff --git a/jsduck.json b/jsduck.json index 5dd4977715..c0641ded40 100644 --- a/jsduck.json +++ b/jsduck.json @@ -13,7 +13,6 @@ "maintenance/jsduck/external.js", "resources/src/mediawiki", "resources/src/mediawiki.action", - "resources/src/mediawiki.api", "resources/src/mediawiki.language", "resources/src/mediawiki.messagePoster", "resources/src/mediawiki.page", diff --git a/resources/Resources.php b/resources/Resources.php index 107ccecf4c..3b3769eeeb 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -860,7 +860,7 @@ return array( 'position' => 'top', ), 'mediawiki.api' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.js', + 'scripts' => 'resources/src/mediawiki/api.js', 'dependencies' => array( 'mediawiki.util', 'user.tokens', @@ -868,14 +868,14 @@ return array( 'targets' => array( 'desktop', 'mobile' ), ), 'mediawiki.api.category' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.category.js', + 'scripts' => 'resources/src/mediawiki/api/category.js', 'dependencies' => array( 'mediawiki.api', 'mediawiki.Title', ), ), 'mediawiki.api.edit' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.edit.js', + 'scripts' => 'resources/src/mediawiki/api/edit.js', 'dependencies' => array( 'mediawiki.api', 'mediawiki.Title', @@ -883,21 +883,21 @@ return array( 'targets' => array( 'desktop', 'mobile' ), ), 'mediawiki.api.login' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.login.js', + 'scripts' => 'resources/src/mediawiki/api/login.js', 'dependencies' => 'mediawiki.api', ), 'mediawiki.api.options' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.options.js', + 'scripts' => 'resources/src/mediawiki/api/options.js', 'dependencies' => 'mediawiki.api', 'targets' => array( 'desktop', 'mobile' ), ), 'mediawiki.api.parse' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.parse.js', + 'scripts' => 'resources/src/mediawiki/api/parse.js', 'dependencies' => 'mediawiki.api', 'targets' => array( 'desktop', 'mobile' ), ), 'mediawiki.api.upload' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.upload.js', + 'scripts' => 'resources/src/mediawiki/api/upload.js', 'dependencies' => array( 'dom-level2-shim', 'mediawiki.api', @@ -906,7 +906,7 @@ return array( ), ), 'mediawiki.api.watch' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.watch.js', + 'scripts' => 'resources/src/mediawiki/api/watch.js', 'dependencies' => array( 'mediawiki.api', ), @@ -993,7 +993,7 @@ return array( 'dependencies' => 'mediawiki.ForeignApi.core', ), 'mediawiki.ForeignApi.core' => array( - 'scripts' => 'resources/src/mediawiki.api/mediawiki.ForeignApi.js', + 'scripts' => 'resources/src/mediawiki/ForeignApi.js', 'dependencies' => array( 'mediawiki.api', 'oojs', diff --git a/resources/src/mediawiki.api/mediawiki.ForeignApi.js b/resources/src/mediawiki.api/mediawiki.ForeignApi.js deleted file mode 100644 index b8cc059819..0000000000 --- a/resources/src/mediawiki.api/mediawiki.ForeignApi.js +++ /dev/null @@ -1,109 +0,0 @@ -( function ( mw, $ ) { - - /** - * Create an object like mw.Api, but automatically handling everything required to communicate - * with another MediaWiki wiki via cross-origin requests (CORS). - * - * The foreign wiki must be configured to accept requests from the current wiki. See - * for details. - * - * var api = new mw.ForeignApi( 'https://commons.wikimedia.org/w/api.php' ); - * api.get( { - * action: 'query', - * meta: 'userinfo' - * } ).done( function ( data ) { - * console.log( data ); - * } ); - * - * To ensure that the user at the foreign wiki is logged in, pass the `assert: 'user'` parameter - * to #get/#post (since MW 1.23): if they are not, the API request will fail. (Note that this - * doesn't guarantee that it's the same user.) - * - * Authentication-related MediaWiki extensions may extend this class to ensure that the user - * authenticated on the current wiki will be automatically authenticated on the foreign one. These - * extension modules should be registered using the ResourceLoaderForeignApiModules hook. See - * CentralAuth for a practical example. The general pattern to extend and override the name is: - * - * function MyForeignApi() {}; - * OO.inheritClass( MyForeignApi, mw.ForeignApi ); - * mw.ForeignApi = MyForeignApi; - * - * @class mw.ForeignApi - * @extends mw.Api - * @since 1.26 - * - * @constructor - * @param {string|mw.Uri} url URL pointing to another wiki's `api.php` endpoint. - * @param {Object} [options] See mw.Api. - * - * @author Bartosz Dziewoński - * @author Jon Robson - */ - function CoreForeignApi( url, options ) { - if ( !url || $.isPlainObject( url ) ) { - throw new Error( 'mw.ForeignApi() requires a `url` parameter' ); - } - - this.apiUrl = String( url ); - - options = $.extend( /*deep=*/ true, - { - ajax: { - url: this.apiUrl, - xhrFields: { - withCredentials: true - } - }, - parameters: { - // Add 'origin' query parameter to all requests. - origin: this.getOrigin() - } - }, - options - ); - - // Call parent constructor - CoreForeignApi.parent.call( this, options ); - } - - OO.inheritClass( CoreForeignApi, mw.Api ); - - /** - * Return the origin to use for API requests, in the required format (protocol, host and port, if - * any). - * - * @protected - * @return {string} - */ - CoreForeignApi.prototype.getOrigin = function () { - var origin = location.protocol + '//' + location.hostname; - if ( location.port ) { - origin += ':' + location.port; - } - return origin; - }; - - /** - * @inheritdoc - */ - CoreForeignApi.prototype.ajax = function ( parameters, ajaxOptions ) { - var url, origin, newAjaxOptions; - - // 'origin' query parameter must be part of the request URI, and not just POST request body - if ( ajaxOptions.type === 'POST' ) { - url = ( ajaxOptions && ajaxOptions.url ) || this.defaults.ajax.url; - origin = ( parameters && parameters.origin ) || this.defaults.parameters.origin; - url += ( url.indexOf( '?' ) !== -1 ? '&' : '?' ) + - 'origin=' + encodeURIComponent( origin ); - newAjaxOptions = $.extend( {}, ajaxOptions, { url: url } ); - } else { - newAjaxOptions = ajaxOptions; - } - - return CoreForeignApi.parent.prototype.ajax.call( this, parameters, newAjaxOptions ); - }; - - // Expose - mw.ForeignApi = CoreForeignApi; - -}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.api/mediawiki.api.category.js b/resources/src/mediawiki.api/mediawiki.api.category.js deleted file mode 100644 index 14077e022f..0000000000 --- a/resources/src/mediawiki.api/mediawiki.api.category.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * @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( { - prop: 'categoryinfo', - titles: String( title ) - } ); - - return apiPromise - .then( function ( data ) { - var exists = false; - if ( data.query && data.query.pages ) { - $.each( data.query.pages, function ( id, page ) { - if ( page.categoryinfo ) { - exists = true; - } - } ); - } - return exists; - } ) - .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( { - list: 'allpages', - apprefix: prefix, - apnamespace: mw.config.get( 'wgNamespaceIds' ).category - } ); - - return apiPromise - .then( function ( data ) { - var texts = []; - if ( data.query && data.query.allpages ) { - $.each( data.query.allpages, function ( i, category ) { - texts.push( new mw.Title( category.title ).getMainText() ); - } ); - } - return texts; - } ) - .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( { - prop: 'categories', - titles: String( title ) - } ); - - return apiPromise - .then( function ( data ) { - var titles = false; - if ( data.query && data.query.pages ) { - $.each( data.query.pages, function ( id, page ) { - if ( page.categories ) { - if ( titles === false ) { - titles = []; - } - $.each( page.categories, function ( i, cat ) { - titles.push( new mw.Title( cat.title ) ); - } ); - } - } ); - } - return titles; - } ) - .promise( { abort: apiPromise.abort } ); - } - } ); - - /** - * @class mw.Api - * @mixins mw.Api.plugin.category - */ - -}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.api/mediawiki.api.edit.js b/resources/src/mediawiki.api/mediawiki.api.edit.js deleted file mode 100644 index e43285ff11..0000000000 --- a/resources/src/mediawiki.api/mediawiki.api.edit.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @class mw.Api.plugin.edit - */ -( function ( mw, $ ) { - - $.extend( mw.Api.prototype, { - - /** - * Post to API with edit 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( 'edit', params, ajaxOptions ); - }, - - /** - * API helper to grab an edit token. - * - * @return {jQuery.Promise} - * @return {Function} return.done - * @return {string} return.done.token Received token. - */ - getEditToken: function () { - return this.getToken( '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 ) ); diff --git a/resources/src/mediawiki.api/mediawiki.api.js b/resources/src/mediawiki.api/mediawiki.api.js deleted file mode 100644 index 43b20b8b2e..0000000000 --- a/resources/src/mediawiki.api/mediawiki.api.js +++ /dev/null @@ -1,416 +0,0 @@ -( 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. - * @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 = {}; - - // Pre-populate with fake ajax promises to save http requests for tokens - // we already have on the page via the user.tokens module (bug 34733). - 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. editToken, 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 ) { - // TODO: Share API objects with exact same config. - 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.parameters = $.extend( {}, defaultOptions.parameters, options.parameters ); - options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax ); - - this.defaults = options; - }; - - mw.Api.prototype = { - - /** - * 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 - * - * TODO: Post actions for non-local hostnames will need proxy. - * - * @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. - * - * @private - * @param {Object} parameters (modified in-place) - */ - preprocessParameters: function ( parameters ) { - var key; - // Handle common MediaWiki API idioms for passing parameters - for ( key in parameters ) { - // Multiple values are pipe-separated - if ( $.isArray( parameters[ key ] ) ) { - parameters[ key ] = parameters[ key ].join( '|' ); - } - // Boolean values are only false when not given at all - if ( parameters[ key ] === false || parameters[ key ] === undefined ) { - 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, - 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 ); - - // 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 { - // Some deployed MediaWiki >= 1.17 forbid periods in URLs, due to an IE XSS bug - // So let's escape them here. See bug #28235 - // This works because jQuery accepts data as a query string or as an Object - ajaxOptions.data = $.param( parameters ).replace( /\./g, '%2E' ); - - // If we extracted a token parameter, add it back in. - if ( token ) { - ajaxOptions.data += '&token=' + encodeURIComponent( token ); - } - - if ( ajaxOptions.contentType === 'multipart/form-data' ) { - // We were asked to emulate but can't, so drop the Content-Type header, otherwise - // it'll be wrong and the server will fail to decode the POST body - delete ajaxOptions.contentType; - } - } - - // 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 ) { - if ( result === undefined || result === null || result === '' ) { - apiDeferred.reject( 'ok-but-empty', - 'OK response but empty result (check HTTP headers?)' - ); - } else if ( result.error ) { - var code = result.error.code === undefined ? 'unknown' : result.error.code; - apiDeferred.reject( code, result ); - } else { - apiDeferred.resolve( result, jqXHR ); - } - } ); - - // 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( 'options', { - * 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; - - return api.getToken( tokenType, params.assert ).then( function ( token ) { - params.token = token; - return api.post( params, ajaxOptions ).then( - // If no error, return to caller as-is - null, - // Error handler - function ( code ) { - if ( code === 'badtoken' ) { - api.badToken( tokenType ); - // Try again, once - params.token = undefined; - return api.getToken( tokenType, params.assert ).then( function ( token ) { - params.token = token; - return api.post( params, ajaxOptions ); - } ); - } - - // Different error, pass on to let caller handle the error code - return this; - } - ); - } ); - }, - - /** - * Get a token for a certain action from the API. - * - * The assert parameter is only for internal use by postWithToken. - * - * @param {string} type Token type - * @return {jQuery.Promise} - * @return {Function} return.done - * @return {string} return.done.token Received token. - * @since 1.22 - */ - getToken: function ( type, assert ) { - var apiPromise, - promiseGroup = promises[ this.defaults.ajax.url ], - d = promiseGroup && promiseGroup[ type + 'Token' ]; - - if ( !d ) { - apiPromise = this.get( { action: 'tokens', type: type, assert: assert } ); - - d = apiPromise - .then( function ( data ) { - if ( data.tokens && data.tokens[ type + 'token' ] ) { - return data.tokens[ type + 'token' ]; - } - - // If token type is not available for this user, - // key '...token' is either missing or set to boolean false - return $.Deferred().reject( 'token-missing', data ); - }, function () { - // Clear promise. Do not cache errors. - delete promiseGroup[ type + 'Token' ]; - // Pass on to allow the caller to handle the error - return this; - } ) - // Attach abort handler - .promise( { abort: apiPromise.abort } ); - - // Store deferred now so that we can use it again even if it isn't ready yet - if ( !promiseGroup ) { - promiseGroup = promises[ this.defaults.ajax.url ] = {}; - } - 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 ]; - if ( promiseGroup ) { - delete promiseGroup[ type + 'Token' ]; - } - } - }; - - /** - * @static - * @property {Array} - * List of errors we might receive from the API. - * For now, this just documents our expectation that there should be similar messages - * available. - */ - 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', - - // Stash-specific errors - expanded - 'stashfailed', - 'stasherror', - 'stashedfilenotfound', - 'stashpathinvalid', - 'stashfilestorage', - 'stashzerolength', - 'stashnotloggedin', - 'stashwrongowner', - 'stashnosuchfilekey' - ]; - - /** - * @static - * @property {Array} - * List of warnings we might receive from the API. - * For now, this just documents our expectation that there should be similar messages - * available. - */ - mw.Api.warnings = [ - 'duplicate', - 'exists' - ]; - -}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.api/mediawiki.api.login.js b/resources/src/mediawiki.api/mediawiki.api.login.js deleted file mode 100644 index 2b709aae7b..0000000000 --- a/resources/src/mediawiki.api/mediawiki.api.login.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 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 ) ); diff --git a/resources/src/mediawiki.api/mediawiki.api.options.js b/resources/src/mediawiki.api/mediawiki.api.options.js deleted file mode 100644 index 399e6f4356..0000000000 --- a/resources/src/mediawiki.api/mediawiki.api.options.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @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 parallel 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 = [], - deferreds = []; - - for ( name in options ) { - value = options[ name ] === null ? null : String( options[ name ] ); - - // Can we bundle this option, or does it need a separate request? - 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 ) { - deferreds.push( this.postWithToken( 'options', { - action: 'options', - optionname: name, - optionvalue: value - } ) ); - } else { - // Omitting value resets the option - deferreds.push( this.postWithToken( 'options', { - action: 'options', - optionname: name - } ) ); - } - } - } - - if ( grouped.length ) { - deferreds.push( this.postWithToken( 'options', { - action: 'options', - change: grouped.join( '|' ) - } ) ); - } - - return $.when.apply( $, deferreds ); - } - - } ); - - /** - * @class mw.Api - * @mixins mw.Api.plugin.options - */ - -}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.api/mediawiki.api.parse.js b/resources/src/mediawiki.api/mediawiki.api.parse.js deleted file mode 100644 index bc3d44f946..0000000000 --- a/resources/src/mediawiki.api/mediawiki.api.parse.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @class mw.Api.plugin.parse - */ -( function ( mw, $ ) { - - $.extend( mw.Api.prototype, { - /** - * Convenience method for 'action=parse'. - * - * @param {string} wikitext - * @return {jQuery.Promise} - * @return {Function} return.done - * @return {string} return.done.data Parsed HTML of `wikitext`. - */ - parse: function ( wikitext ) { - var apiPromise = this.get( { - action: 'parse', - contentmodel: 'wikitext', - text: wikitext - } ); - - return apiPromise - .then( function ( data ) { - return data.parse.text[ '*' ]; - } ) - .promise( { abort: apiPromise.abort } ); - } - } ); - - /** - * @class mw.Api - * @mixins mw.Api.plugin.parse - */ - -}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.api/mediawiki.api.upload.js b/resources/src/mediawiki.api/mediawiki.api.upload.js deleted file mode 100644 index 4abff28f62..0000000000 --- a/resources/src/mediawiki.api/mediawiki.api.upload.js +++ /dev/null @@ -1,368 +0,0 @@ -/** - * 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 - }; - - /** - * @private - * Get nonce for iframe IDs on the page. - * - * @return {number} - */ - function getNonce() { - return nonce++; - } - - /** - * @private - * Get new iframe object for an upload. - * - * @return {HTMLIframeElement} - */ - function getNewIframe( id ) { - var frame = document.createElement( 'iframe' ); - frame.id = id; - frame.name = id; - return frame; - } - - /** - * @private - * Shortcut for getting hidden inputs - * - * @return {jQuery} - */ - function getHiddenInput( name, val ) { - return $( '' ) - .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} 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 ) { - return $.Deferred().reject( 'No file' ); - } - - canUseFormData = formDataAvailable() && file instanceof window.File; - - if ( !isFileInput && !canUseFormData ) { - return $.Deferred().reject( '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 = $( '
' ), - 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 ); - - if ( !result ) { - deferred.reject( 'No response from API on upload attempt.' ); - } else if ( result.error || result.warnings ) { - if ( result.error && result.error.code === 'badtoken' ) { - api.badToken( 'edit' ); - } - - deferred.reject( result.error || result.warnings ); - } else { - deferred.notify( 1 ); - deferred.resolve( result ); - } - } ); - tokenPromise.done( function () { - $form.submit(); - } ); - } ); - - $iframe.error( function ( error ) { - deferred.reject( 'iframe failed to load: ' + 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 ) { - return $.Deferred().reject( '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, - deferred = $.Deferred(); - - for ( key in data ) { - if ( !fieldsAllowed[ key ] ) { - delete data[ key ]; - } - } - - data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data ); - data.file = file; - - if ( !data.filename && !data.stash ) { - return $.Deferred().reject( 'Filename not included in file data.' ); - } - - // Use this.postWithEditToken() or this.post() - this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, { - // Use FormData (if we got here, we know that it's available) - contentType: 'multipart/form-data', - // 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 ) { - if ( result.error || result.warnings ) { - deferred.reject( result.error || result.warnings ); - } else { - deferred.notify( 1 ); - deferred.resolve( result ); - } - } ) - .fail( function ( result ) { - deferred.reject( result ); - } ); - - return deferred.promise(); - }, - - /** - * 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.finishStashUpload Call this function to finish the upload. - * @return {Object} return.finishStashUpload.data Additional data for the upload. - * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload - * @return {Object} return.finishStashUpload.return.data API return value for the final upload - */ - uploadToStash: function ( file, data ) { - var filekey, - api = this; - - if ( !data.filename ) { - return $.Deferred().reject( 'Filename not included in file data.' ); - } - - function finishUpload( moreData ) { - data = $.extend( data, moreData ); - data.filekey = filekey; - data.action = 'upload'; - data.format = 'json'; - - if ( !data.filename ) { - return $.Deferred().reject( 'Filename not included in file data.' ); - } - - return api.postWithEditToken( data ).then( function ( result ) { - if ( result.upload && ( result.upload.error || result.upload.warnings ) ) { - return $.Deferred().reject( result.upload.error || result.upload.warnings ).promise(); - } - return result; - } ); - } - - return this.upload( file, { stash: true, filename: data.filename } ).then( function ( result ) { - if ( result && result.upload && result.upload.filekey ) { - filekey = result.upload.filekey; - } else if ( result && ( result.error || result.warning ) ) { - return $.Deferred().reject( result ); - } - - return finishUpload; - } ); - }, - - needToken: function () { - return true; - } - } ); - - /** - * @class mw.Api - * @mixins mw.Api.plugin.upload - */ -}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.api/mediawiki.api.watch.js b/resources/src/mediawiki.api/mediawiki.api.watch.js deleted file mode 100644 index a2ff1292bb..0000000000 --- a/resources/src/mediawiki.api/mediawiki.api.watch.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @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. - * @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 - * @return {string} return.done.watch.message Parsed HTML of the confirmational interface message - */ - function doWatchInternal( pages, addParams ) { - // XXX: Parameter addParams is undocumented because we inherit this - // documentation in the public method... - var apiPromise = this.postWithToken( 'watch', - $.extend( - { - action: 'watch', - titles: $.isArray( pages ) ? pages.join( '|' ) : String( pages ), - uselang: mw.config.get( 'wgUserLanguage' ) - }, - addParams - ) - ); - - return apiPromise - .then( function ( data ) { - // If a single page was given (not an array) respond with a single item as well. - return $.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 ) ); diff --git a/resources/src/mediawiki/ForeignApi.js b/resources/src/mediawiki/ForeignApi.js new file mode 100644 index 0000000000..b8cc059819 --- /dev/null +++ b/resources/src/mediawiki/ForeignApi.js @@ -0,0 +1,109 @@ +( function ( mw, $ ) { + + /** + * Create an object like mw.Api, but automatically handling everything required to communicate + * with another MediaWiki wiki via cross-origin requests (CORS). + * + * The foreign wiki must be configured to accept requests from the current wiki. See + * for details. + * + * var api = new mw.ForeignApi( 'https://commons.wikimedia.org/w/api.php' ); + * api.get( { + * action: 'query', + * meta: 'userinfo' + * } ).done( function ( data ) { + * console.log( data ); + * } ); + * + * To ensure that the user at the foreign wiki is logged in, pass the `assert: 'user'` parameter + * to #get/#post (since MW 1.23): if they are not, the API request will fail. (Note that this + * doesn't guarantee that it's the same user.) + * + * Authentication-related MediaWiki extensions may extend this class to ensure that the user + * authenticated on the current wiki will be automatically authenticated on the foreign one. These + * extension modules should be registered using the ResourceLoaderForeignApiModules hook. See + * CentralAuth for a practical example. The general pattern to extend and override the name is: + * + * function MyForeignApi() {}; + * OO.inheritClass( MyForeignApi, mw.ForeignApi ); + * mw.ForeignApi = MyForeignApi; + * + * @class mw.ForeignApi + * @extends mw.Api + * @since 1.26 + * + * @constructor + * @param {string|mw.Uri} url URL pointing to another wiki's `api.php` endpoint. + * @param {Object} [options] See mw.Api. + * + * @author Bartosz Dziewoński + * @author Jon Robson + */ + function CoreForeignApi( url, options ) { + if ( !url || $.isPlainObject( url ) ) { + throw new Error( 'mw.ForeignApi() requires a `url` parameter' ); + } + + this.apiUrl = String( url ); + + options = $.extend( /*deep=*/ true, + { + ajax: { + url: this.apiUrl, + xhrFields: { + withCredentials: true + } + }, + parameters: { + // Add 'origin' query parameter to all requests. + origin: this.getOrigin() + } + }, + options + ); + + // Call parent constructor + CoreForeignApi.parent.call( this, options ); + } + + OO.inheritClass( CoreForeignApi, mw.Api ); + + /** + * Return the origin to use for API requests, in the required format (protocol, host and port, if + * any). + * + * @protected + * @return {string} + */ + CoreForeignApi.prototype.getOrigin = function () { + var origin = location.protocol + '//' + location.hostname; + if ( location.port ) { + origin += ':' + location.port; + } + return origin; + }; + + /** + * @inheritdoc + */ + CoreForeignApi.prototype.ajax = function ( parameters, ajaxOptions ) { + var url, origin, newAjaxOptions; + + // 'origin' query parameter must be part of the request URI, and not just POST request body + if ( ajaxOptions.type === 'POST' ) { + url = ( ajaxOptions && ajaxOptions.url ) || this.defaults.ajax.url; + origin = ( parameters && parameters.origin ) || this.defaults.parameters.origin; + url += ( url.indexOf( '?' ) !== -1 ? '&' : '?' ) + + 'origin=' + encodeURIComponent( origin ); + newAjaxOptions = $.extend( {}, ajaxOptions, { url: url } ); + } else { + newAjaxOptions = ajaxOptions; + } + + return CoreForeignApi.parent.prototype.ajax.call( this, parameters, newAjaxOptions ); + }; + + // Expose + mw.ForeignApi = CoreForeignApi; + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/api.js b/resources/src/mediawiki/api.js new file mode 100644 index 0000000000..43b20b8b2e --- /dev/null +++ b/resources/src/mediawiki/api.js @@ -0,0 +1,416 @@ +( 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. + * @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 = {}; + + // Pre-populate with fake ajax promises to save http requests for tokens + // we already have on the page via the user.tokens module (bug 34733). + 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. editToken, 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 ) { + // TODO: Share API objects with exact same config. + 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.parameters = $.extend( {}, defaultOptions.parameters, options.parameters ); + options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax ); + + this.defaults = options; + }; + + mw.Api.prototype = { + + /** + * 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 + * + * TODO: Post actions for non-local hostnames will need proxy. + * + * @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. + * + * @private + * @param {Object} parameters (modified in-place) + */ + preprocessParameters: function ( parameters ) { + var key; + // Handle common MediaWiki API idioms for passing parameters + for ( key in parameters ) { + // Multiple values are pipe-separated + if ( $.isArray( parameters[ key ] ) ) { + parameters[ key ] = parameters[ key ].join( '|' ); + } + // Boolean values are only false when not given at all + if ( parameters[ key ] === false || parameters[ key ] === undefined ) { + 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, + 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 ); + + // 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 { + // Some deployed MediaWiki >= 1.17 forbid periods in URLs, due to an IE XSS bug + // So let's escape them here. See bug #28235 + // This works because jQuery accepts data as a query string or as an Object + ajaxOptions.data = $.param( parameters ).replace( /\./g, '%2E' ); + + // If we extracted a token parameter, add it back in. + if ( token ) { + ajaxOptions.data += '&token=' + encodeURIComponent( token ); + } + + if ( ajaxOptions.contentType === 'multipart/form-data' ) { + // We were asked to emulate but can't, so drop the Content-Type header, otherwise + // it'll be wrong and the server will fail to decode the POST body + delete ajaxOptions.contentType; + } + } + + // 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 ) { + if ( result === undefined || result === null || result === '' ) { + apiDeferred.reject( 'ok-but-empty', + 'OK response but empty result (check HTTP headers?)' + ); + } else if ( result.error ) { + var code = result.error.code === undefined ? 'unknown' : result.error.code; + apiDeferred.reject( code, result ); + } else { + apiDeferred.resolve( result, jqXHR ); + } + } ); + + // 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( 'options', { + * 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; + + return api.getToken( tokenType, params.assert ).then( function ( token ) { + params.token = token; + return api.post( params, ajaxOptions ).then( + // If no error, return to caller as-is + null, + // Error handler + function ( code ) { + if ( code === 'badtoken' ) { + api.badToken( tokenType ); + // Try again, once + params.token = undefined; + return api.getToken( tokenType, params.assert ).then( function ( token ) { + params.token = token; + return api.post( params, ajaxOptions ); + } ); + } + + // Different error, pass on to let caller handle the error code + return this; + } + ); + } ); + }, + + /** + * Get a token for a certain action from the API. + * + * The assert parameter is only for internal use by postWithToken. + * + * @param {string} type Token type + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {string} return.done.token Received token. + * @since 1.22 + */ + getToken: function ( type, assert ) { + var apiPromise, + promiseGroup = promises[ this.defaults.ajax.url ], + d = promiseGroup && promiseGroup[ type + 'Token' ]; + + if ( !d ) { + apiPromise = this.get( { action: 'tokens', type: type, assert: assert } ); + + d = apiPromise + .then( function ( data ) { + if ( data.tokens && data.tokens[ type + 'token' ] ) { + return data.tokens[ type + 'token' ]; + } + + // If token type is not available for this user, + // key '...token' is either missing or set to boolean false + return $.Deferred().reject( 'token-missing', data ); + }, function () { + // Clear promise. Do not cache errors. + delete promiseGroup[ type + 'Token' ]; + // Pass on to allow the caller to handle the error + return this; + } ) + // Attach abort handler + .promise( { abort: apiPromise.abort } ); + + // Store deferred now so that we can use it again even if it isn't ready yet + if ( !promiseGroup ) { + promiseGroup = promises[ this.defaults.ajax.url ] = {}; + } + 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 ]; + if ( promiseGroup ) { + delete promiseGroup[ type + 'Token' ]; + } + } + }; + + /** + * @static + * @property {Array} + * List of errors we might receive from the API. + * For now, this just documents our expectation that there should be similar messages + * available. + */ + 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', + + // Stash-specific errors - expanded + 'stashfailed', + 'stasherror', + 'stashedfilenotfound', + 'stashpathinvalid', + 'stashfilestorage', + 'stashzerolength', + 'stashnotloggedin', + 'stashwrongowner', + 'stashnosuchfilekey' + ]; + + /** + * @static + * @property {Array} + * List of warnings we might receive from the API. + * For now, this just documents our expectation that there should be similar messages + * available. + */ + mw.Api.warnings = [ + 'duplicate', + 'exists' + ]; + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/api/category.js b/resources/src/mediawiki/api/category.js new file mode 100644 index 0000000000..14077e022f --- /dev/null +++ b/resources/src/mediawiki/api/category.js @@ -0,0 +1,108 @@ +/** + * @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( { + prop: 'categoryinfo', + titles: String( title ) + } ); + + return apiPromise + .then( function ( data ) { + var exists = false; + if ( data.query && data.query.pages ) { + $.each( data.query.pages, function ( id, page ) { + if ( page.categoryinfo ) { + exists = true; + } + } ); + } + return exists; + } ) + .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( { + list: 'allpages', + apprefix: prefix, + apnamespace: mw.config.get( 'wgNamespaceIds' ).category + } ); + + return apiPromise + .then( function ( data ) { + var texts = []; + if ( data.query && data.query.allpages ) { + $.each( data.query.allpages, function ( i, category ) { + texts.push( new mw.Title( category.title ).getMainText() ); + } ); + } + return texts; + } ) + .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( { + prop: 'categories', + titles: String( title ) + } ); + + return apiPromise + .then( function ( data ) { + var titles = false; + if ( data.query && data.query.pages ) { + $.each( data.query.pages, function ( id, page ) { + if ( page.categories ) { + if ( titles === false ) { + titles = []; + } + $.each( page.categories, function ( i, cat ) { + titles.push( new mw.Title( cat.title ) ); + } ); + } + } ); + } + return titles; + } ) + .promise( { abort: apiPromise.abort } ); + } + } ); + + /** + * @class mw.Api + * @mixins mw.Api.plugin.category + */ + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/api/edit.js b/resources/src/mediawiki/api/edit.js new file mode 100644 index 0000000000..e43285ff11 --- /dev/null +++ b/resources/src/mediawiki/api/edit.js @@ -0,0 +1,58 @@ +/** + * @class mw.Api.plugin.edit + */ +( function ( mw, $ ) { + + $.extend( mw.Api.prototype, { + + /** + * Post to API with edit 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( 'edit', params, ajaxOptions ); + }, + + /** + * API helper to grab an edit token. + * + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {string} return.done.token Received token. + */ + getEditToken: function () { + return this.getToken( '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 ) ); diff --git a/resources/src/mediawiki/api/login.js b/resources/src/mediawiki/api/login.js new file mode 100644 index 0000000000..2b709aae7b --- /dev/null +++ b/resources/src/mediawiki/api/login.js @@ -0,0 +1,60 @@ +/** + * 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 ) ); diff --git a/resources/src/mediawiki/api/options.js b/resources/src/mediawiki/api/options.js new file mode 100644 index 0000000000..399e6f4356 --- /dev/null +++ b/resources/src/mediawiki/api/options.js @@ -0,0 +1,89 @@ +/** + * @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 parallel 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 = [], + deferreds = []; + + for ( name in options ) { + value = options[ name ] === null ? null : String( options[ name ] ); + + // Can we bundle this option, or does it need a separate request? + 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 ) { + deferreds.push( this.postWithToken( 'options', { + action: 'options', + optionname: name, + optionvalue: value + } ) ); + } else { + // Omitting value resets the option + deferreds.push( this.postWithToken( 'options', { + action: 'options', + optionname: name + } ) ); + } + } + } + + if ( grouped.length ) { + deferreds.push( this.postWithToken( 'options', { + action: 'options', + change: grouped.join( '|' ) + } ) ); + } + + return $.when.apply( $, deferreds ); + } + + } ); + + /** + * @class mw.Api + * @mixins mw.Api.plugin.options + */ + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/api/parse.js b/resources/src/mediawiki/api/parse.js new file mode 100644 index 0000000000..bc3d44f946 --- /dev/null +++ b/resources/src/mediawiki/api/parse.js @@ -0,0 +1,35 @@ +/** + * @class mw.Api.plugin.parse + */ +( function ( mw, $ ) { + + $.extend( mw.Api.prototype, { + /** + * Convenience method for 'action=parse'. + * + * @param {string} wikitext + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {string} return.done.data Parsed HTML of `wikitext`. + */ + parse: function ( wikitext ) { + var apiPromise = this.get( { + action: 'parse', + contentmodel: 'wikitext', + text: wikitext + } ); + + return apiPromise + .then( function ( data ) { + return data.parse.text[ '*' ]; + } ) + .promise( { abort: apiPromise.abort } ); + } + } ); + + /** + * @class mw.Api + * @mixins mw.Api.plugin.parse + */ + +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/api/upload.js b/resources/src/mediawiki/api/upload.js new file mode 100644 index 0000000000..4abff28f62 --- /dev/null +++ b/resources/src/mediawiki/api/upload.js @@ -0,0 +1,368 @@ +/** + * 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 + }; + + /** + * @private + * Get nonce for iframe IDs on the page. + * + * @return {number} + */ + function getNonce() { + return nonce++; + } + + /** + * @private + * Get new iframe object for an upload. + * + * @return {HTMLIframeElement} + */ + function getNewIframe( id ) { + var frame = document.createElement( 'iframe' ); + frame.id = id; + frame.name = id; + return frame; + } + + /** + * @private + * Shortcut for getting hidden inputs + * + * @return {jQuery} + */ + function getHiddenInput( name, val ) { + return $( '' ) + .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} 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 ) { + return $.Deferred().reject( 'No file' ); + } + + canUseFormData = formDataAvailable() && file instanceof window.File; + + if ( !isFileInput && !canUseFormData ) { + return $.Deferred().reject( '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 = $( '' ), + 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 ); + + if ( !result ) { + deferred.reject( 'No response from API on upload attempt.' ); + } else if ( result.error || result.warnings ) { + if ( result.error && result.error.code === 'badtoken' ) { + api.badToken( 'edit' ); + } + + deferred.reject( result.error || result.warnings ); + } else { + deferred.notify( 1 ); + deferred.resolve( result ); + } + } ); + tokenPromise.done( function () { + $form.submit(); + } ); + } ); + + $iframe.error( function ( error ) { + deferred.reject( 'iframe failed to load: ' + 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 ) { + return $.Deferred().reject( '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, + deferred = $.Deferred(); + + for ( key in data ) { + if ( !fieldsAllowed[ key ] ) { + delete data[ key ]; + } + } + + data = $.extend( {}, this.defaults.parameters, { action: 'upload' }, data ); + data.file = file; + + if ( !data.filename && !data.stash ) { + return $.Deferred().reject( 'Filename not included in file data.' ); + } + + // Use this.postWithEditToken() or this.post() + this[ this.needToken() ? 'postWithEditToken' : 'post' ]( data, { + // Use FormData (if we got here, we know that it's available) + contentType: 'multipart/form-data', + // 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 ) { + if ( result.error || result.warnings ) { + deferred.reject( result.error || result.warnings ); + } else { + deferred.notify( 1 ); + deferred.resolve( result ); + } + } ) + .fail( function ( result ) { + deferred.reject( result ); + } ); + + return deferred.promise(); + }, + + /** + * 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.finishStashUpload Call this function to finish the upload. + * @return {Object} return.finishStashUpload.data Additional data for the upload. + * @return {jQuery.Promise} return.finishStashUpload.return API promise for the final upload + * @return {Object} return.finishStashUpload.return.data API return value for the final upload + */ + uploadToStash: function ( file, data ) { + var filekey, + api = this; + + if ( !data.filename ) { + return $.Deferred().reject( 'Filename not included in file data.' ); + } + + function finishUpload( moreData ) { + data = $.extend( data, moreData ); + data.filekey = filekey; + data.action = 'upload'; + data.format = 'json'; + + if ( !data.filename ) { + return $.Deferred().reject( 'Filename not included in file data.' ); + } + + return api.postWithEditToken( data ).then( function ( result ) { + if ( result.upload && ( result.upload.error || result.upload.warnings ) ) { + return $.Deferred().reject( result.upload.error || result.upload.warnings ).promise(); + } + return result; + } ); + } + + return this.upload( file, { stash: true, filename: data.filename } ).then( function ( result ) { + if ( result && result.upload && result.upload.filekey ) { + filekey = result.upload.filekey; + } else if ( result && ( result.error || result.warning ) ) { + return $.Deferred().reject( result ); + } + + return finishUpload; + } ); + }, + + needToken: function () { + return true; + } + } ); + + /** + * @class mw.Api + * @mixins mw.Api.plugin.upload + */ +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/api/watch.js b/resources/src/mediawiki/api/watch.js new file mode 100644 index 0000000000..a2ff1292bb --- /dev/null +++ b/resources/src/mediawiki/api/watch.js @@ -0,0 +1,70 @@ +/** + * @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. + * @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 + * @return {string} return.done.watch.message Parsed HTML of the confirmational interface message + */ + function doWatchInternal( pages, addParams ) { + // XXX: Parameter addParams is undocumented because we inherit this + // documentation in the public method... + var apiPromise = this.postWithToken( 'watch', + $.extend( + { + action: 'watch', + titles: $.isArray( pages ) ? pages.join( '|' ) : String( pages ), + uselang: mw.config.get( 'wgUserLanguage' ) + }, + addParams + ) + ); + + return apiPromise + .then( function ( data ) { + // If a single page was given (not an array) respond with a single item as well. + return $.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 ) );