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

Reply via email to