Introduce mediawiki.ForeignApi

mw.ForeignApi is an extension of mw.Api, automatically handling
everything required to communicate with another MediaWiki wiki via
cross-origin requests (CORS).

Authentication-related MediaWiki extensions may extend it further to
ensure that the user authenticated on the current wiki will be
automatically authenticated on the foreign one. A CentralAuth
implementation is provided in I0fd05ef8b9c9db0fdb59c6cb248f364259f80456.

Bug: T66636
Change-Id: Ic20b9682d28633baa87d22e6e9fb71ce507da58d
This commit is contained in:
Bartosz Dziewoński 2015-08-15 01:17:49 +02:00
parent a2f75a52ef
commit 2f30ff7a86
8 changed files with 204 additions and 1 deletions

View file

@ -1012,6 +1012,7 @@ $wgAutoloadLocalClasses = array(
'ResourceLoaderEditToolbarModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderEditToolbarModule.php',
'ResourceLoaderFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFileModule.php',
'ResourceLoaderFilePath' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePath.php',
'ResourceLoaderForeignApiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderForeignApiModule.php',
'ResourceLoaderImage' => __DIR__ . '/includes/resourceloader/ResourceLoaderImage.php',
'ResourceLoaderImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderImageModule.php',
'ResourceLoaderJqueryMsgModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderJqueryMsgModule.php',

View file

@ -2448,6 +2448,13 @@ $user: The user having their password expiration reset
$oldSessionID: old session id
$newSessionID: new session id
'ResourceLoaderForeignApiModules': Called from ResourceLoaderForeignApiModule.
Use this to add dependencies to 'mediawiki.ForeignApi' module when you wish
to override its behavior. See the module docs for more information.
&$dependencies: string[] List of modules that 'mediawiki.ForeignApi' should
depend on
$context: ResourceLoaderContext|null
'ResourceLoaderGetConfigVars': Called at the end of
ResourceLoaderStartUpModule::getConfigSettings(). Use this to export static
configuration variables to JavaScript. Things that depend on the current page

View file

@ -0,0 +1,33 @@
<?php
/**
* ResourceLoader module for mediawiki.ForeignApi that has dynamically
* generated dependencies, via a hook usable by extensions.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* ResourceLoader module for mediawiki.ForeignApi and its generated data
*/
class ResourceLoaderForeignApiModule extends ResourceLoaderFileModule {
public function getDependencies( ResourceLoaderContext $context = null ) {
$dependencies = $this->dependencies;
Hooks::run( 'ResourceLoaderForeignApiModules', array( &$dependencies, $context ) );
return $dependencies;
}
}

View file

@ -40,7 +40,7 @@
},
{
"name": "API",
"classes": ["mw.Api*"]
"classes": ["mw.Api*", "mw.ForeignApi*"]
},
{
"name": "Language",

View file

@ -981,6 +981,18 @@ return array(
'oojs-ui',
),
),
'mediawiki.ForeignApi' => array(
'class' => 'ResourceLoaderForeignApiModule',
// Additional dependencies generated dynamically
'dependencies' => 'mediawiki.ForeignApi.core',
),
'mediawiki.ForeignApi.core' => array(
'scripts' => 'resources/src/mediawiki.api/mediawiki.ForeignApi.js',
'dependencies' => array(
'mediawiki.api',
'oojs',
),
),
'mediawiki.helplink' => array(
'position' => 'top',
'styles' => array(

View file

@ -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 = window.location.protocol + '//' + window.location.hostname;
if ( window.location.port ) {
origin += ':' + window.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 !== 'GET' ) {
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 ) );

View file

@ -83,6 +83,7 @@ return array(
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js',
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.test.js',
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js',
'tests/qunit/suites/resources/mediawiki.api/mediawiki.ForeignApi.test.js',
'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
@ -111,6 +112,7 @@ return array(
'mediawiki.api.parse',
'mediawiki.api.upload',
'mediawiki.api.watch',
'mediawiki.ForeignApi.core',
'mediawiki.jqueryMsg',
'mediawiki.messagePoster',
'mediawiki.RegExp',

View file

@ -0,0 +1,39 @@
( function ( mw ) {
QUnit.module( 'mediawiki.ForeignApi', QUnit.newMwEnvironment( {
setup: function () {
this.server = this.sandbox.useFakeServer();
this.server.respondImmediately = true;
this.clock = this.sandbox.useFakeTimers();
},
teardown: function () {
// https://github.com/jquery/jquery/issues/2453
this.clock.tick();
}
} ) );
QUnit.test( 'origin is included in GET requests', function ( assert ) {
QUnit.expect( 1 );
var api = new mw.ForeignApi( '//localhost:4242/w/api.php' );
this.server.respond( function ( request ) {
assert.ok( request.url.match( /origin=/ ), 'origin is included in GET requests' );
request.respond( 200, { 'Content-Type': 'application/json' }, '[]' );
} );
api.get( {} );
} );
QUnit.test( 'origin is included in POST requests', function ( assert ) {
QUnit.expect( 2 );
var api = new mw.ForeignApi( '//localhost:4242/w/api.php' );
this.server.respond( function ( request ) {
assert.ok( request.requestBody.match( /origin=/ ), 'origin is included in POST request body' );
assert.ok( request.url.match( /origin=/ ), 'origin is included in POST request URL, too' );
request.respond( 200, { 'Content-Type': 'application/json' }, '[]' );
} );
api.post( {} );
} );
}( mediaWiki ) );