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:
parent
a2f75a52ef
commit
2f30ff7a86
8 changed files with 204 additions and 1 deletions
|
|
@ -1012,6 +1012,7 @@ $wgAutoloadLocalClasses = array(
|
||||||
'ResourceLoaderEditToolbarModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderEditToolbarModule.php',
|
'ResourceLoaderEditToolbarModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderEditToolbarModule.php',
|
||||||
'ResourceLoaderFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFileModule.php',
|
'ResourceLoaderFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFileModule.php',
|
||||||
'ResourceLoaderFilePath' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePath.php',
|
'ResourceLoaderFilePath' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePath.php',
|
||||||
|
'ResourceLoaderForeignApiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderForeignApiModule.php',
|
||||||
'ResourceLoaderImage' => __DIR__ . '/includes/resourceloader/ResourceLoaderImage.php',
|
'ResourceLoaderImage' => __DIR__ . '/includes/resourceloader/ResourceLoaderImage.php',
|
||||||
'ResourceLoaderImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderImageModule.php',
|
'ResourceLoaderImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderImageModule.php',
|
||||||
'ResourceLoaderJqueryMsgModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderJqueryMsgModule.php',
|
'ResourceLoaderJqueryMsgModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderJqueryMsgModule.php',
|
||||||
|
|
|
||||||
|
|
@ -2448,6 +2448,13 @@ $user: The user having their password expiration reset
|
||||||
$oldSessionID: old session id
|
$oldSessionID: old session id
|
||||||
$newSessionID: new 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
|
'ResourceLoaderGetConfigVars': Called at the end of
|
||||||
ResourceLoaderStartUpModule::getConfigSettings(). Use this to export static
|
ResourceLoaderStartUpModule::getConfigSettings(). Use this to export static
|
||||||
configuration variables to JavaScript. Things that depend on the current page
|
configuration variables to JavaScript. Things that depend on the current page
|
||||||
|
|
|
||||||
33
includes/resourceloader/ResourceLoaderForeignApiModule.php
Normal file
33
includes/resourceloader/ResourceLoaderForeignApiModule.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "API",
|
"name": "API",
|
||||||
"classes": ["mw.Api*"]
|
"classes": ["mw.Api*", "mw.ForeignApi*"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Language",
|
"name": "Language",
|
||||||
|
|
|
||||||
|
|
@ -981,6 +981,18 @@ return array(
|
||||||
'oojs-ui',
|
'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(
|
'mediawiki.helplink' => array(
|
||||||
'position' => 'top',
|
'position' => 'top',
|
||||||
'styles' => array(
|
'styles' => array(
|
||||||
|
|
|
||||||
109
resources/src/mediawiki.api/mediawiki.ForeignApi.js
Normal file
109
resources/src/mediawiki.api/mediawiki.ForeignApi.js
Normal 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 ) );
|
||||||
|
|
@ -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.parse.test.js',
|
||||||
'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.upload.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.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.special/mediawiki.special.recentchanges.test.js',
|
||||||
'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
|
'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
|
||||||
'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
|
'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
|
||||||
|
|
@ -111,6 +112,7 @@ return array(
|
||||||
'mediawiki.api.parse',
|
'mediawiki.api.parse',
|
||||||
'mediawiki.api.upload',
|
'mediawiki.api.upload',
|
||||||
'mediawiki.api.watch',
|
'mediawiki.api.watch',
|
||||||
|
'mediawiki.ForeignApi.core',
|
||||||
'mediawiki.jqueryMsg',
|
'mediawiki.jqueryMsg',
|
||||||
'mediawiki.messagePoster',
|
'mediawiki.messagePoster',
|
||||||
'mediawiki.RegExp',
|
'mediawiki.RegExp',
|
||||||
|
|
|
||||||
|
|
@ -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 ) );
|
||||||
Loading…
Reference in a new issue