jenkins-bot has submitted this change and it was merged.
Change subject: Implement a mediawiki.ForeignApi extension
......................................................................
Implement a mediawiki.ForeignApi extension
Extends mw.ForeignApi with CentralAuth authentication handling,
amending every request with a 'centralauthtoken' parameter, if
required.
Every request to the foreign wiki will be preceded by a
'action=centralauthtoken' request to the local wiki, except in cases
where we detect that it is unproductive or unnecessary.
Depends on Ic20b9682d28633baa87d22e6e9fb71ce507da58d in MediaWiki core.
Bug: T66636
Change-Id: I0fd05ef8b9c9db0fdb59c6cb248f364259f80456
---
M .jshintrc
M CentralAuth.php
M includes/CentralAuthHooks.php
A modules/ext.centralauth.ForeignApi.js
A tests/qunit/ext.centralauth.ForeignApi.test.js
5 files changed, 281 insertions(+), 2 deletions(-)
Approvals:
MarkTraceur: Looks good to me, approved
jenkins-bot: Verified
diff --git a/.jshintrc b/.jshintrc
index 66e3d48..f230c25 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -15,10 +15,11 @@
// Environment
"browser": true,
- "jquery": true,
"globals": {
"mediaWiki": false,
- "OO": false
+ "jQuery": false,
+ "OO": false,
+ "QUnit": false
}
}
diff --git a/CentralAuth.php b/CentralAuth.php
index 693d5de..bbea124 100644
--- a/CentralAuth.php
+++ b/CentralAuth.php
@@ -388,6 +388,8 @@
$wgHooks['UnitTestsList'][] = 'CentralAuthHooks::onUnitTestsList';
$wgHooks['SpecialContributionsBeforeMainOutput'][] =
'CentralAuthHooks::onSpecialContributionsBeforeMainOutput';
$wgHooks['SpecialPage_initList'][] =
'CentralAuthHooks::onSpecialPage_initList';
+$wgHooks['ResourceLoaderForeignApiModules'][] =
'CentralAuthHooks::onResourceLoaderForeignApiModules';
+$wgHooks['ResourceLoaderTestModules'][] =
'CentralAuthHooks::onResourceLoaderTestModules';
// For interaction with the Special:Renameuser extension
$wgHooks['RenameUserWarning'][] = 'CentralAuthHooks::onRenameUserWarning';
@@ -569,5 +571,12 @@
)
) + $commonModuleInfo;
+$wgResourceModules['ext.centralauth.ForeignApi'] = array(
+ 'scripts' => 'ext.centralauth.ForeignApi.js',
+ 'dependencies' => array(
+ 'mediawiki.ForeignApi.core',
+ ),
+) + $commonModuleInfo;
+
// Finish configuration after other extensions and settings are loaded.
$wgExtensionFunctions[] = 'CentralAuthHooks::onRunExtensionFunctions';
diff --git a/includes/CentralAuthHooks.php b/includes/CentralAuthHooks.php
index f67f5f9..f0c6b2f 100644
--- a/includes/CentralAuthHooks.php
+++ b/includes/CentralAuthHooks.php
@@ -2154,4 +2154,19 @@
);
}
}
+
+ public static function onResourceLoaderForeignApiModules( array
&$dependencies, ResourceLoaderContext $context = null ) {
+ $dependencies[] = 'ext.centralauth.ForeignApi';
+ return true;
+ }
+
+ public static function onResourceLoaderTestModules( array
&$testModules, ResourceLoader &$resourceLoader ) {
+ $testModules['qunit']['ext.centralauth.ForeignApi.test'] =
array(
+ 'scripts' => array(
'tests/qunit/ext.centralauth.ForeignApi.test.js' ),
+ 'dependencies' => array( 'ext.centralauth.ForeignApi' ),
+ 'localBasePath' => __DIR__ . '/..',
+ 'remoteExtPath' => 'CentralAuth',
+ );
+ return true;
+ }
}
diff --git a/modules/ext.centralauth.ForeignApi.js
b/modules/ext.centralauth.ForeignApi.js
new file mode 100644
index 0000000..fb790f8
--- /dev/null
+++ b/modules/ext.centralauth.ForeignApi.js
@@ -0,0 +1,180 @@
+( function ( mw, $ ) {
+
+ var
+ hasOwnProperty = Object.prototype.hasOwnProperty,
+ // Names of old token types which can be mapped to 'csrf' token
now
+ csrfTokenOldTypes = [ 'csrf', 'edit', 'delete', 'protect',
'move', 'block', 'unblock',
+ 'email', 'import', 'options' ];
+
+ /**
+ * Extend mw.ForeignApi with CentralAuth authentication handling.
+ *
+ * Every request to the foreign wiki will be preceded by a
'action=centralauthtoken' request to
+ * the local wiki. The foreign request will be extended with the
acquired token to ensure that the
+ * requests to foreign wiki will always be authenticated as the
currently logged-in user on local
+ * wiki.
+ *
+ * If we detect that the current user is anonymous, that the foreign
wiki doesn't have
+ * CentralAuth, or that the requests to foreign wiki will be correctly
authenticated without
+ * sending 'centralauthtoken' every time, the additional requests will
be skipped.
+ *
+ * @class mw.ForeignApi.plugin.CentralAuth
+ * @extends mw.ForeignApi
+ *
+ * @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 CentralAuthForeignApi( url, options ) {
+ // Call parent constructor
+ CentralAuthForeignApi.parent.call( this, url, options );
+
+ // Properties
+ // mw.Api instance used for action=centralauthtoken requests
+ this.localApi = new mw.Api();
+ // If this is set, action=centralauthtoken requests will be
suppressed
+ this.noTokenNeeded = false;
+ // Hold return value of checkForeignLogin() (if the user is
logged in)
+ this.foreignLoginPromise = null;
+ // 'csrf' token status, as requested by checkForeignLogin()
+ this.csrfToken = null;
+ this.csrfTokenBad = false;
+
+ if ( mw.config.get( 'wgUserName' ) === null ) {
+ // Anonymous users cannot obtain a centralauthtoken
+ this.noTokenNeeded = true;
+ } else {
+ // We're logged in locally, check to see if we're
logged in on the foreign wiki too, and thus
+ // can skip 'centralauthtoken' requests
+ this.foreignLoginPromise = this.checkForeignLogin();
+ }
+ }
+
+ OO.inheritClass( CentralAuthForeignApi, mw.ForeignApi );
+
+ /**
+ * Get a 'centralauthtoken' from the local wiki for use on the foreign
wiki.
+ *
+ * @private
+ * @return {jQuery.Promise}
+ */
+ CentralAuthForeignApi.prototype.getCentralAuthToken = function () {
+ return this.localApi.get( { action: 'centralauthtoken' }
).then( function ( resp ) {
+ if ( resp.error ) {
+ return $.Deferred().reject( resp.error );
+ } else {
+ return resp.centralauthtoken.centralauthtoken;
+ }
+ } );
+ };
+
+ /**
+ * Query the foreign wiki to see if we're already logged in there in
the user's browser, which
+ * means that there's no need to query for and use 'centralauthtoken'
parameter.
+ *
+ * To avoid wasted requests, get a CSRF token at the same time.
+ *
+ * @private
+ * @return {jQuery.Promise}
+ */
+ CentralAuthForeignApi.prototype.checkForeignLogin = function () {
+ var foreignApi = this;
+ return CentralAuthForeignApi.parent.prototype.ajax.call(
+ this,
+ { action: 'query', meta: 'userinfo|tokens' },
+ { type: 'GET' }
+ )
+ .then( function ( resp ) {
+ var userinfo = resp.query.userinfo;
+ if ( userinfo.anon === undefined &&
userinfo.name === mw.config.get( 'wgUserName' ) ) {
+ // We are logged in on the foreign wiki
+ foreignApi.noTokenNeeded = true;
+ foreignApi.csrfToken =
resp.query.tokens.csrftoken;
+ return true;
+ }
+ return $.Deferred().reject();
+ } );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ CentralAuthForeignApi.prototype.getToken = function ( type, assert ) {
+ var foreignApi = this;
+ if ( this.foreignLoginPromise && $.inArray( type,
csrfTokenOldTypes ) !== -1 ) {
+ return this.foreignLoginPromise.then(
+ function () {
+ if ( foreignApi.csrfToken &&
!foreignApi.csrfTokenBad ) {
+ return foreignApi.csrfToken;
+ }
+ return
CentralAuthForeignApi.parent.prototype.getToken.call( foreignApi, type, assert
);
+ },
+ function () {
+ return
CentralAuthForeignApi.parent.prototype.getToken.call( foreignApi, type, assert
);
+ }
+ );
+ }
+ return CentralAuthForeignApi.parent.prototype.getToken.call(
this, type, assert );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ CentralAuthForeignApi.prototype.badToken = function ( type ) {
+ // This may be a bit too aggressive, but that's better than not
being aggressive enough
+ this.csrfTokenBad = true;
+ return CentralAuthForeignApi.parent.prototype.badToken.call(
this, type );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ CentralAuthForeignApi.prototype.ajax = function ( parameters,
ajaxOptions ) {
+ var tokenPromise, foreignApi = this;
+ // If we know we can't get a 'centralauthtoken', or if one was
provided, don't request it
+ if ( this.noTokenNeeded || hasOwnProperty.call( parameters,
'centralauthtoken' ) ) {
+ tokenPromise = $.Deferred().reject();
+ } else if ( this.foreignLoginPromise ) {
+ tokenPromise = this.foreignLoginPromise.then(
+ // If succeeded, no 'centralauthtoken' needed
+ function () { return $.Deferred().reject(); },
+ // If failed, get the token
+ function () { return
foreignApi.getCentralAuthToken(); }
+ );
+ } else {
+ tokenPromise = this.getCentralAuthToken();
+ }
+
+ return tokenPromise.then(
+ function ( centralAuthToken ) {
+ var url, newParameters, newAjaxOptions;
+
+ // Add 'centralauthtoken' query parameter
+ newParameters = $.extend( { centralauthtoken:
centralAuthToken }, parameters );
+ // It must be part of the request URI, and not
just POST request body
+ if ( ajaxOptions.type !== 'GET' ) {
+ url = ( ajaxOptions && ajaxOptions.url
) || foreignApi.defaults.ajax.url;
+ url += ( url.indexOf( '?' ) !== -1 ?
'&' : '?' ) +
+ 'centralauthtoken=' +
encodeURIComponent( centralAuthToken );
+ newAjaxOptions = $.extend( {},
ajaxOptions, { url: url } );
+ } else {
+ newAjaxOptions = ajaxOptions;
+ }
+
+ return
CentralAuthForeignApi.parent.prototype.ajax.call( foreignApi, newParameters,
newAjaxOptions );
+ },
+ function () {
+ // We couldn't get the token, but continue
anyway. This is expected in some cases, like
+ // anonymous users.
+ return
CentralAuthForeignApi.parent.prototype.ajax.call( foreignApi, parameters,
ajaxOptions );
+ }
+ );
+ };
+
+ // Expose
+ mw.ForeignApi = CentralAuthForeignApi;
+
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/ext.centralauth.ForeignApi.test.js
b/tests/qunit/ext.centralauth.ForeignApi.test.js
new file mode 100644
index 0000000..16f4ef5
--- /dev/null
+++ b/tests/qunit/ext.centralauth.ForeignApi.test.js
@@ -0,0 +1,74 @@
+( function ( mw, $ ) {
+ QUnit.module( 'ext.centralauth.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();
+ },
+ config: {
+ wgUserName: true
+ }
+ } ) );
+
+ QUnit.test( 'Anonymous users do not get centralauthtoken', function (
assert ) {
+ QUnit.expect( 1 );
+ mw.config.set( 'wgUserName', null );
+
+ this.server.respond( function ( request ) {
+ request.respond( 200, { 'Content-Type':
'application/json' }, '[]' );
+ } );
+
+ var api = new mw.ForeignApi( '//localhost:4242/w/api.php' );
+
+ var spy = this.sandbox.spy( api, 'getCentralAuthToken' );
+ api.get( {} );
+ assert.ok( !spy.called, 'Anonymous users do not ask for
centralauthtoken' );
+ } );
+
+ QUnit.test( 'Logged in users get centralauthtoken if not logged in
remotely', function ( assert ) {
+ QUnit.expect( 1 );
+ mw.config.set( 'wgUserName', 'User' );
+
+ this.sandbox.stub( mw.ForeignApi.prototype, 'checkForeignLogin'
).returns(
+ $.Deferred().reject()
+ );
+
+ this.server.respond( function ( request ) {
+ request.respond( 200, { 'Content-Type':
'application/json' }, '[]' );
+ } );
+
+ var api = new mw.ForeignApi( '//localhost:4242/w/api.php' );
+
+ var spy = this.sandbox.stub( api, 'getCentralAuthToken'
).returns(
+ $.Deferred().resolve( 'CENTRALAUTHTOKEN' )
+ );
+ api.get( {} );
+ assert.ok( spy.called, 'Logged in users ask for
centralauthtoken if not logged in remotely' );
+ } );
+
+ QUnit.test( 'Logged in users do not get centralauthtoken if logged in
remotely', function ( assert ) {
+ QUnit.expect( 1 );
+ mw.config.set( 'wgUserName', 'User' );
+
+ this.sandbox.stub( mw.ForeignApi.prototype, 'checkForeignLogin'
).returns(
+ $.Deferred().resolve()
+ );
+
+ this.server.respond( function ( request ) {
+ request.respond( 200, { 'Content-Type':
'application/json' }, '[]' );
+ } );
+
+ var api = new mw.ForeignApi( '//localhost:4242/w/api.php' );
+
+ var spy = this.sandbox.stub( api, 'getCentralAuthToken'
).returns(
+ $.Deferred().resolve( 'CENTRALAUTHTOKEN' )
+ );
+ api.get( {} );
+ assert.ok( !spy.called, 'Logged in users do not ask for
centralauthtoken if logged in remotely' );
+ } );
+
+}( mediaWiki, jQuery ) );
--
To view, visit https://gerrit.wikimedia.org/r/231711
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I0fd05ef8b9c9db0fdb59c6cb248f364259f80456
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/CentralAuth
Gerrit-Branch: master
Gerrit-Owner: Bartosz Dziewoński <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: Bartosz Dziewoński <[email protected]>
Gerrit-Reviewer: Jdlrobson <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: MarkTraceur <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits