summaryrefslogtreecommitdiff
path: root/resources/src/mediawiki.api
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2015-12-17 09:15:42 +0100
committerPierre Schmitz <pierre@archlinux.de>2015-12-17 09:44:51 +0100
commita1789ddde42033f1b05cc4929491214ee6e79383 (patch)
tree63615735c4ddffaaabf2428946bb26f90899f7bf /resources/src/mediawiki.api
parent9e06a62f265e3a2aaabecc598d4bc617e06fa32d (diff)
Update to MediaWiki 1.26.0
Diffstat (limited to 'resources/src/mediawiki.api')
-rw-r--r--resources/src/mediawiki.api/mediawiki.ForeignApi.js109
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.edit.js6
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.js124
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.login.js1
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.options.js4
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.parse.js2
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.upload.js391
-rw-r--r--resources/src/mediawiki.api/mediawiki.api.watch.js2
8 files changed, 595 insertions, 44 deletions
diff --git a/resources/src/mediawiki.api/mediawiki.ForeignApi.js b/resources/src/mediawiki.api/mediawiki.ForeignApi.js
new file mode 100644
index 00000000..b8cc0598
--- /dev/null
+++ b/resources/src/mediawiki.api/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
+ * <https://www.mediawiki.org/wiki/Manual:$wgCrossSiteAJAXdomains> 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.edit.js b/resources/src/mediawiki.api/mediawiki.api.edit.js
index dbe45bf6..e43285ff 100644
--- a/resources/src/mediawiki.api/mediawiki.api.edit.js
+++ b/resources/src/mediawiki.api/mediawiki.api.edit.js
@@ -11,10 +11,11 @@
* cached token and start over.
*
* @param {Object} params API parameters
+ * @param {Object} [ajaxOptions]
* @return {jQuery.Promise} See #post
*/
- postWithEditToken: function ( params ) {
- return this.postWithToken( 'edit', params );
+ postWithEditToken: function ( params, ajaxOptions ) {
+ return this.postWithToken( 'edit', params, ajaxOptions );
},
/**
@@ -30,6 +31,7 @@
/**
* Post a new section to the page.
+ *
* @see #postWithEditToken
* @param {mw.Title|String} title Target page
* @param {string} header
diff --git a/resources/src/mediawiki.api/mediawiki.api.js b/resources/src/mediawiki.api/mediawiki.api.js
index 3a19e021..73f3c8c6 100644
--- a/resources/src/mediawiki.api/mediawiki.api.js
+++ b/resources/src/mediawiki.api/mediawiki.api.js
@@ -1,26 +1,28 @@
( function ( mw, $ ) {
- // We allow people to omit these default parameters from API requests
- // there is very customizable error handling here, on a per-call basis
- // wondering, would it be simpler to make it easy to clone the api object,
- // change error handling, and use that instead?
- var defaultOptions = {
+ /**
+ * @class mw.Api
+ */
- // Query parameters for API requests
+ /**
+ * @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 options for jQuery.ajax()
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 = {};
@@ -39,37 +41,33 @@
* 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.
*
- * TODO: Share API objects with exact same config.
- *
* var api = new mw.Api();
* api.get( {
* action: 'query',
* meta: 'userinfo'
- * } ).done ( function ( data ) {
+ * } ).done( function ( data ) {
* console.log( data );
* } );
*
- * Multiple values for a parameter can be specified using an array (since MW 1.25):
+ * 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 ) {
+ * } ).done( function ( data ) {
* console.log( data );
* } );
*
- * @class
+ * 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. Ajax options can also be
- * overridden for each individual request to {@link jQuery#ajax} later on.
+ * @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 ) {
-
- if ( options === undefined ) {
- options = {};
- }
+ options = options || {};
// Force a string if we got a mw.Uri object
if ( options.ajax && options.ajax.url !== undefined ) {
@@ -80,9 +78,22 @@
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 () {
+ $.each( this.requests, function ( index, request ) {
+ if ( request ) {
+ request.abort();
+ }
+ } );
+ },
/**
* Perform API get request
@@ -113,6 +124,27 @@
},
/**
+ * 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
@@ -121,7 +153,8 @@
* Fail: Error code
*/
ajax: function ( parameters, ajaxOptions ) {
- var token,
+ var token, requestIndex,
+ api = this,
apiDeferred = $.Deferred(),
xhr, key, formData;
@@ -134,11 +167,7 @@
delete parameters.token;
}
- for ( key in parameters ) {
- if ( $.isArray( parameters[key] ) ) {
- parameters[key] = parameters[key].join( '|' );
- }
- }
+ this.preprocessParameters( parameters );
// If multipart/form-data has been requested and emulation is possible, emulate it
if (
@@ -150,7 +179,7 @@
formData = new FormData();
for ( key in parameters ) {
- formData.append( key, parameters[key] );
+ formData.append( key, parameters[ key ] );
}
// If we extracted a token parameter, add it back in.
if ( token ) {
@@ -206,6 +235,11 @@
}
} );
+ 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' ) ) {
@@ -242,11 +276,9 @@
// Error handler
function ( code ) {
if ( code === 'badtoken' ) {
- // Clear from cache
- promises[ api.defaults.ajax.url ][ tokenType + 'Token' ] =
- params.token = undefined;
-
+ 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 );
@@ -281,17 +313,16 @@
d = apiPromise
.then( function ( data ) {
- // If token type is not available for this user,
- // key '...token' is either missing or set to boolean false
- if ( data.tokens && data.tokens[type + 'token'] ) {
- return data.tokens[type + 'token'];
+ 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;
} )
@@ -306,6 +337,23 @@
}
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' ];
+ }
}
};
diff --git a/resources/src/mediawiki.api/mediawiki.api.login.js b/resources/src/mediawiki.api/mediawiki.api.login.js
index 25257927..2b709aae 100644
--- a/resources/src/mediawiki.api/mediawiki.api.login.js
+++ b/resources/src/mediawiki.api/mediawiki.api.login.js
@@ -1,5 +1,6 @@
/**
* Make the two-step login easier.
+ *
* @author Niklas Laxström
* @class mw.Api.plugin.login
* @since 1.22
diff --git a/resources/src/mediawiki.api/mediawiki.api.options.js b/resources/src/mediawiki.api/mediawiki.api.options.js
index b839fbdc..399e6f43 100644
--- a/resources/src/mediawiki.api/mediawiki.api.options.js
+++ b/resources/src/mediawiki.api/mediawiki.api.options.js
@@ -14,7 +14,7 @@
*/
saveOption: function ( name, value ) {
var param = {};
- param[name] = value;
+ param[ name ] = value;
return this.saveOptions( param );
},
@@ -38,7 +38,7 @@
deferreds = [];
for ( name in options ) {
- value = options[name] === null ? null : String( options[name] );
+ value = options[ name ] === null ? null : String( options[ name ] );
// Can we bundle this option, or does it need a separate request?
bundleable =
diff --git a/resources/src/mediawiki.api/mediawiki.api.parse.js b/resources/src/mediawiki.api/mediawiki.api.parse.js
index 2dcf8078..bc3d44f9 100644
--- a/resources/src/mediawiki.api/mediawiki.api.parse.js
+++ b/resources/src/mediawiki.api/mediawiki.api.parse.js
@@ -21,7 +21,7 @@
return apiPromise
.then( function ( data ) {
- return data.parse.text['*'];
+ return data.parse.text[ '*' ];
} )
.promise( { abort: apiPromise.abort } );
}
diff --git a/resources/src/mediawiki.api/mediawiki.api.upload.js b/resources/src/mediawiki.api/mediawiki.api.upload.js
new file mode 100644
index 00000000..6f3e4c3f
--- /dev/null
+++ b/resources/src/mediawiki.api/mediawiki.api.upload.js
@@ -0,0 +1,391 @@
+/**
+ * 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
+ * Given a non-empty object, return one of its keys.
+ *
+ * @param {Object} obj
+ * @return {string}
+ */
+ function getFirstKey( obj ) {
+ for ( var key in obj ) {
+ if ( obj.hasOwnProperty( key ) ) {
+ return key;
+ }
+ }
+ }
+
+ /**
+ * @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 $( '<input 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} 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' );
+ }
+
+ canUseFormData = formDataAvailable() && file instanceof window.File;
+
+ 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( 'edit' );
+ }
+
+ 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.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,
+ 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 ) {
+ throw new Error( '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 ) {
+ 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();
+ },
+
+ /**
+ * 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 ) {
+ throw new Error( '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 ) {
+ throw new Error( 'Filename not included in file data.' );
+ }
+
+ return api.postWithEditToken( data ).then( function ( result ) {
+ if ( result.upload && result.upload.warnings ) {
+ return $.Deferred().reject( getFirstKey( result.upload.warnings ), result ).promise();
+ }
+ return result;
+ } );
+ }
+
+ return this.upload( file, { stash: true, filename: data.filename } ).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 );
+ }
+ );
+ },
+
+ 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
index 40ba136d..a2ff1292 100644
--- a/resources/src/mediawiki.api/mediawiki.api.watch.js
+++ b/resources/src/mediawiki.api/mediawiki.api.watch.js
@@ -37,7 +37,7 @@
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];
+ return $.isArray( pages ) ? data.watch : data.watch[ 0 ];
} )
.promise( { abort: apiPromise.abort } );
}