Gergő Tisza has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/253300

Change subject: [WIP] Update for AuthManager
......................................................................

[WIP] Update for AuthManager

FIXME:
* core needs to be fixed not to readd its own fields when there is
  no password form
* do we need throttling for the second-factor guesses?
* expand data change handling in core so changes to TOTP secret
  can be done that way. If that does not work, at least make the
  special page call securitySensitiveOperationStatus.

Bug: T110457
Change-Id: Ic492b8f2477c475f8414b61505139e9a1df2ba5b
---
A OATHAuth.hooks.legacy.php
M OATHAuth.hooks.php
M OATHAuth.php
A auth/TOTPAuthenticationRequest.php
A auth/TOTPSecondaryAuthenticationProvider.php
M i18n/en.json
M i18n/qqq.json
7 files changed, 397 insertions(+), 92 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/OATHAuth 
refs/changes/00/253300/1

diff --git a/OATHAuth.hooks.legacy.php b/OATHAuth.hooks.legacy.php
new file mode 100644
index 0000000..f04c2aa
--- /dev/null
+++ b/OATHAuth.hooks.legacy.php
@@ -0,0 +1,177 @@
+<?php
+
+/**
+ * Hooks for Extension:OATHAuth
+ * @deprecated B/C class for compatibility with pre-AuthManager core
+ */
+class OATHAuthLegacyHooks {
+       /**
+        * @param $template UserloginTemplate
+        * @return bool
+        */
+       static function ModifyUITemplate( &$template ) {
+               $input = '<div><label for="wpOATHToken">'
+                       . wfMessage( 'oathauth-token' )->escaped()
+                       . '</label>'
+                       . Html::input( 'wpOATHToken', null, 'text', array(
+                                       'class' => 'loginText', 'id' => 
'wpOATHToken', 'tabindex' => '3', 'size' => '20'
+                               ) ) . '</div>';
+
+               $template->set( 'extrafields', $template->get( 'extrafields', 
'' ) . $input );
+
+               return true;
+       }
+
+       /**
+        * @param $extraFields array
+        * @return bool
+        */
+       static function ChangePasswordForm( &$extraFields ) {
+               $tokenField = array( 'wpOATHToken', 'oathauth-token', 
'password', '' );
+               array_push( $extraFields, $tokenField );
+
+               return true;
+       }
+
+       /**
+        * @param $user User
+        * @param $password string
+        * @param $newpassword string
+        * @param &$errorMsg string
+        * @return bool
+        */
+       static function AbortChangePassword( $user, $password, $newpassword, 
&$errorMsg ) {
+               $result = self::authenticate( $user );
+               if ( $result ) {
+                       return true;
+               } else {
+                       $errorMsg = 'oathauth-abortlogin';
+
+                       return false;
+               }
+       }
+
+       /**
+        * @param $user User
+        * @param $password string
+        * @param &$abort int
+        * @param &$errorMsg string
+        * @return bool
+        */
+       static function AbortLogin( $user, $password, &$abort, &$errorMsg ) {
+               $result = self::authenticate( $user );
+               if ( $result ) {
+                       return true;
+               } else {
+                       $abort = LoginForm::ABORTED;
+                       $errorMsg = 'oathauth-abortlogin';
+                       return false;
+               }
+       }
+
+       /**
+        * @param $user User
+        * @return bool
+        */
+       static function authenticate( $user ) {
+               global $wgRequest;
+
+               $token = $wgRequest->getText( 'wpOATHToken' );
+               $oathuser = OATHUser::newFromUser( $user );
+               # Though it's weird to default to true, we only want to deny
+               # users who have two-factor enabled and have validated their
+               # token.
+               $result = true;
+               if ( $oathuser && $oathuser->isEnabled() && 
$oathuser->isValidated() ) {
+                       $result = $oathuser->verifyToken( $token );
+               }
+               return $result;
+       }
+
+       /**
+        * Determine if two-factor authentication is enabled for $wgUser
+        *
+        * @param bool &$isEnabled Will be set to true if enabled, false 
otherwise
+        *
+        * @return bool False if enabled, true otherwise
+        */
+       static function TwoFactorIsEnabled( &$isEnabled ) {
+               global $wgUser;
+
+               $user = OATHUser::newFromUser( $wgUser );
+               if ( $user && $user->isEnabled() && $user->isValidated() ) {
+                       $isEnabled = true;
+                       # This two-factor extension is enabled by the user,
+                       # we don't need to check others.
+                       return false;
+               } else {
+                       $isEnabled = false;
+                       # This two-factor extension isn't enabled by the user,
+                       # but others may be.
+                       return true;
+               }
+       }
+
+       /**
+        * Add the necessary user preferences for OATHAuth
+        *
+        * @param User $user
+        * @param array $preferences
+        *
+        * @return bool
+        */
+       public static function manageOATH( User $user, array &$preferences ) {
+               $oathUser = OATHUser::newFromUser( $user );
+
+               $title = SpecialPage::getTitleFor( 'OATH' );
+               if ( $oathUser->isEnabled() && $oathUser->isValidated() ) {
+                       $preferences['oath-disable'] = array(
+                               'type' => 'info',
+                               'raw' => 'true',
+                               'default' => Linker::link(
+                                       $title,
+                                       wfMessage( 'oathauth-disable' 
)->escaped(),
+                                       array(),
+                                       array(
+                                               'action' => 'disable',
+                                               'returnto' => 
SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                                       )
+                               ),
+                               'label-message' => 'oathauth-prefs-label',
+                               'section' => 'personal/info',
+                       );
+                       $preferences['oath-reset'] = array(
+                               'type' => 'info',
+                               'raw' => 'true',
+                               'default' => Linker::link(
+                                       $title,
+                                       wfMessage( 'oathauth-reset' 
)->escaped(),
+                                       array(),
+                                       array(
+                                               'action' => 'reset',
+                                               'returnto' => 
SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                                       )
+                               ),
+                               'section' => 'personal/info',
+                       );
+               } else {
+                       $preferences['oath-enable'] = array(
+                               'type' => 'info',
+                               'raw' => 'true',
+                               'default' => Linker::link(
+                                       $title,
+                                       wfMessage( 'oathauth-enable' 
)->escaped(),
+                                       array(),
+                                       array(
+                                               'action' => 'enable',
+                                               'returnto' => 
SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                                       )
+                               ),
+                               'label-message' => 'oathauth-prefs-label',
+                               'section' => 'personal/info',
+                       );
+               }
+
+               return true;
+       }
+}
diff --git a/OATHAuth.hooks.php b/OATHAuth.hooks.php
index 4aa8a67..ee8803a 100644
--- a/OATHAuth.hooks.php
+++ b/OATHAuth.hooks.php
@@ -1,100 +1,45 @@
 <?php
+use MediaWiki\Auth\AuthManager;
 
 /**
  * Hooks for Extension:OATHAuth
  */
 class OATHAuthHooks {
        /**
-        * @param $template UserloginTemplate
+        * @param string $fieldName Name of the field
+        * @param array $singleFieldInfo Field information array for a single 
field (an item from
+        *   AuthenticationRequest::getFieldInfo()).
+        * @param array $fieldDescriptor HTMLFormField descriptor. The special 
key 'weight' can be set
+        *   to change the order of the fields.
+        * @param string $action One of the AuthManager::ACTION_* constants.
         * @return bool
         */
-       static function ModifyUITemplate( &$template ) {
-               $input = '<div><label for="wpOATHToken">'
-                       . wfMessage( 'oathauth-token' )->escaped()
-                       . '</label>'
-                       . Html::input( 'wpOATHToken', null, 'text', array(
-                                       'class' => 'loginText', 'id' => 
'wpOATHToken', 'tabindex' => '3', 'size' => '20'
-                               ) ) . '</div>';
-
-               $template->set( 'extrafields', $template->get( 'extrafields', 
'' ) . $input );
-
+       public static function onAuthChangeFormField(
+               $fieldName, $singleFieldInfo, &$fieldDescriptor, $action
+       ) {
+               if ( $fieldName === 'OATHToken' ) {
+                       $fieldDescriptor = array(
+                               'type' => $action === 
AuthManager::ACTION_CHANGE ? 'password' : 'text',
+                               'label' => 'oathauth-auth-token-label',
+                               'cssClass' => 'loginText',
+                               'id' => 'wpOATHToken',
+                               'size' => 20,
+                       );
+               }
                return true;
-       }
-
-       /**
-        * @param $extraFields array
-        * @return bool
-        */
-       static function ChangePasswordForm( &$extraFields ) {
-               $tokenField = array( 'wpOATHToken', 'oathauth-token', 
'password', '' );
-               array_push( $extraFields, $tokenField );
-
-               return true;
-       }
-
-       /**
-        * @param $user User
-        * @param $password string
-        * @param $newpassword string
-        * @param &$errorMsg string
-        * @return bool
-        */
-       static function AbortChangePassword( $user, $password, $newpassword, 
&$errorMsg ) {
-               $result = self::authenticate( $user );
-               if ( $result ) {
-                       return true;
-               } else {
-                       $errorMsg = 'oathauth-abortlogin';
-
-                       return false;
-               }
-       }
-
-       /**
-        * @param $user User
-        * @param $password string
-        * @param &$abort int
-        * @param &$errorMsg string
-        * @return bool
-        */
-       static function AbortLogin( $user, $password, &$abort, &$errorMsg ) {
-               $result = self::authenticate( $user );
-               if ( $result ) {
-                       return true;
-               } else {
-                       $abort = LoginForm::ABORTED;
-                       $errorMsg = 'oathauth-abortlogin';
-                       return false;
-               }
-       }
-
-       /**
-        * @param $user User
-        * @return bool
-        */
-       static function authenticate( $user ) {
-               global $wgRequest;
-
-               $token = $wgRequest->getText( 'wpOATHToken' );
-               $oathuser = OATHUser::newFromUser( $user );
-               # Though it's weird to default to true, we only want to deny
-               # users who have two-factor enabled and have validated their
-               # token.
-               $result = true;
-               if ( $oathuser && $oathuser->isEnabled() && 
$oathuser->isValidated() ) {
-                       $result = $oathuser->verifyToken( $token );
-               }
-               return $result;
        }
 
        /**
         * Determine if two-factor authentication is enabled for $wgUser
         *
-        * @param bool &$isEnabled Will be set to true if enabled, false 
otherwise
+        * This isn't the preferred mechanism for controlling access to 
sensitive features
+        * (see AuthManager::securitySensitiveOperationStatus() for that) but 
there is no harm in
+        * keeping it.
         *
+        * @param bool &$isEnabled Will be set to true if enabled, false 
otherwise
         * @return bool False if enabled, true otherwise
         */
-       static function TwoFactorIsEnabled( &$isEnabled ) {
+       public static function onTwoFactorIsEnabled( &$isEnabled ) {
                global $wgUser;
 
                $user = OATHUser::newFromUser( $wgUser );
@@ -116,10 +61,9 @@
         *
         * @param User $user
         * @param array $preferences
-        *
         * @return bool
         */
-       public static function manageOATH( User $user, array &$preferences ) {
+       public static function onGetPreferences( User $user, array 
&$preferences ) {
                $oathUser = OATHUser::newFromUser( $user );
 
                $title = SpecialPage::getTitleFor( 'OATH' );
diff --git a/OATHAuth.php b/OATHAuth.php
index b29911e..d84cae4 100644
--- a/OATHAuth.php
+++ b/OATHAuth.php
@@ -47,12 +47,15 @@
 $wgExtensionMessagesFiles['OATHAuth'] = $dir . 'OATHAuth.i18n.php';
 $wgExtensionMessagesFiles['OATHAuthAlias'] = $dir . 'OATHAuth.alias.php';
 $wgAutoloadClasses['OATHAuthHooks'] = $dir . 'OATHAuth.hooks.php';
+$wgAutoloadClasses['OATHAuthLegacyHooks'] = $dir . 'OATHAuth.hooks.legacy.php';
 $wgAutoloadClasses['HOTP'] = $dir . 'lib/hotp.php';
 $wgAutoloadClasses['HOTPResult'] = $dir . 'lib/hotp.php';
 $wgAutoloadClasses['Base32'] = $dir . 'lib/base32.php';
 $wgAutoloadClasses['OATHUser'] = $dir . 'OATHUser.php';
 $wgAutoloadClasses['SpecialOATH'] = $dir . 'special/SpecialOATH.php';
-$wgSpecialPages['OATH'] = 'SpecialOATH';
+$wgAutoloadClasses['TOTPAuthenticationRequest'] = $dir . 
'auth/TOTPAuthenticationRequest.php';
+$wgAutoloadClasses['TOTPSecondaryAuthenticationProvider'] =
+       $dir . 'auth/TOTPSecondaryAuthenticationProvider.php';
 
 $wgResourceModules['ext.oathauth'] = array(
        'scripts' => array(
@@ -64,12 +67,15 @@
        'remoteExtPath' => 'OATHAuth',
 );
 
-$wgHooks['AbortChangePassword'][] = 'OATHAuthHooks::AbortChangePassword';
-$wgHooks['AbortLogin'][] = 'OATHAuthHooks::AbortLogin';
-$wgHooks['UserLoginForm'][] = 'OATHAuthHooks::ModifyUITemplate';
-$wgHooks['ChangePasswordForm'][] = 'OATHAuthHooks::ChangePasswordForm';
-$wgHooks['TwoFactorIsEnabled'][] = 'OATHAuthHooks::TwoFactorIsEnabled';
+$wgSpecialPages['OATH'] = 'SpecialOATH';
 $wgHooks['LoadExtensionSchemaUpdates'][] = 
'OATHAuthHooks::OATHAuthSchemaUpdates';
-$wgHooks['GetPreferences'][] = 'OATHAuthHooks::manageOATH';
-
-
+if ( class_exists( 'MediaWiki\Auth\AuthManager' ) ) {
+       $wgHooks['AuthChangeFormField'][] = 
'OATHAuthHooks::onAuthChangeFormField';
+} else {
+       $wgHooks['AbortChangePassword'][] = 
'OATHAuthHooks::AbortChangePassword';
+       $wgHooks['AbortLogin'][] = 'OATHAuthHooks::AbortLogin';
+       $wgHooks['UserLoginForm'][] = 'OATHAuthHooks::ModifyUITemplate';
+       $wgHooks['ChangePasswordForm'][] = 'OATHAuthHooks::ChangePasswordForm';
+}
+$wgHooks['TwoFactorIsEnabled'][] = 'OATHAuthHooks::onTwoFactorIsEnabled';
+$wgHooks['GetPreferences'][] = 'OATHAuthHooks::onGetPreferences';
diff --git a/auth/TOTPAuthenticationRequest.php 
b/auth/TOTPAuthenticationRequest.php
new file mode 100644
index 0000000..07bfd17
--- /dev/null
+++ b/auth/TOTPAuthenticationRequest.php
@@ -0,0 +1,21 @@
+<?php
+
+use MediaWiki\Auth\AuthenticationRequest;
+
+/**
+ * AuthManager value object for the TOTP second factor of an authentication: a 
pseudorandom token
+ * that is generated from the current time indepdendently by the server and 
the client.
+ */
+class TOTPAuthenticationRequest extends AuthenticationRequest {
+       public $OATHToken;
+
+       public static function getFieldInfo() {
+               return array(
+                       'OATHToken' => array(
+                               'type' => 'string',
+                               'label' => 'oathauth-auth-token-label',
+                               'help' => 'oathauth-auth-token-help',
+                       ),
+               );
+       }
+}
diff --git a/auth/TOTPSecondaryAuthenticationProvider.php 
b/auth/TOTPSecondaryAuthenticationProvider.php
new file mode 100644
index 0000000..f6e976b
--- /dev/null
+++ b/auth/TOTPSecondaryAuthenticationProvider.php
@@ -0,0 +1,149 @@
+<?php
+
+use MediaWiki\Auth\AbstractSecondaryAuthenticationProvider;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Session\SessionManager;
+
+/**
+ * AuthManager secondary authentication provider for TOTP second-factor 
authentication.
+ *
+ * After a successful primary authentication, requests a time-based one-time 
password
+ * (typically generated by a mobile app such as Google Authenticator) from the 
user.
+ *
+ * @see AuthManager
+ * @see https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
+ */
+class TOTPSecondaryAuthenticationProvider extends 
AbstractSecondaryAuthenticationProvider {
+
+       /**
+        * Return the applicable list of AuthenticationRequests
+        *
+        * @see AuthManager::getAuthenticationRequestTypes()
+        * @param string $action One of the AuthManager::ACTION_* constants
+        * @return string[] AuthenticationRequest class names
+        */
+       public function getAuthenticationRequestTypes( $action ) {
+               switch ( $action ) {
+                       case AuthManager::ACTION_LOGIN:
+                               // don't ask for anything initially so the 
second factor is a separate screen
+                               return array();
+                       case AuthManager::ACTION_LOGIN_CONTINUE:
+                       case AuthManager::ACTION_ALL:
+                               return array( 'TOTPAuthenticationRequest' );
+                       default:
+                               return array();
+               }
+       }
+
+       /**
+        * If the user has enabled two-factor authentication, request a second 
factor.
+        *
+        * @param User $user User being authenticated. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs Keys are class names
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user is authenticated. Additional secondary providers 
may run.
+        *  - FAIL: The user is not authenticated. Fail the authentication 
process.
+        *  - ABSTAIN: Additional secondary providers may run.
+        *  - UI: Additional AuthenticationRequests are needed to complete the 
process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the 
process.
+        */
+       public function beginSecondaryAuthentication( $user, array $reqs ) {
+               $oathuser = OATHUser::newFromUser( $user );
+
+               if ( !$oathuser || !$oathuser->isEnabled() || 
!$oathuser->isValidated() ) {
+                       return AuthenticationResponse::newAbstain();
+               } else {
+                       return AuthenticationResponse::newUI( array( 
'TOTPAuthenticationRequest' ),
+                               wfMessage( 'oathauth-auth-ui' ) );
+               }
+       }
+
+
+       /**
+        * Continue an authentication flow
+        * @param User $user User being authenticated. This may become a
+        *   "UserValue" in the future, or User may be refactored into such.
+        * @param AuthenticationRequest[] $reqs Keys are class names
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user is authenticated. Additional secondary providers 
may run.
+        *  - FAIL: The user is not authenticated. Fail the authentication 
process.
+        *  - ABSTAIN: Additional secondary providers may run.
+        *  - UI: Additional AuthenticationRequests are needed to complete the 
process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the 
process.
+        */
+       public function continueSecondaryAuthentication( $user, array $reqs ) {
+               if ( !isset( $reqs['TOTPAuthenticationRequest'] ) ) {
+                       throw new LogicException( 'TOTPAuthenticationRequest 
required' );
+               }
+
+               $oathuser = OATHUser::newFromUser( $user );
+               $token = $reqs['TOTPAuthenticationRequest']->OATHToken;
+
+               if ( !$oathuser || !$oathuser->isEnabled() || 
!$oathuser->isValidated() ) {
+                       $this->logger->warning( 'Two-factor authentication was 
disabled mid-authentication for '
+                               . $user->getName() );
+                       return AuthenticationResponse::newAbstain();
+               }
+
+               if ( $oathuser->verifyToken( $token ) ) {
+                       return AuthenticationResponse::newPass();
+               } else {
+                       // FIXME how does this get throttled? (is that needed 
at all?)
+                       return AuthenticationResponse::newUI( array( 
'TOTPAuthenticationRequest' ),
+                               wfMessage( 'oathauth-abortlogin' ) );
+               }
+       }
+
+
+       /**
+        * Start an account creation flow
+        * @param User $user User being created (has been added to the 
database).
+        *   This may become a "UserValue" in the future, or User may be 
refactored
+        *   into such.
+        * @param AuthenticationRequest[] $reqs Keys are class names
+        * @return AuthenticationResponse Expected responses:
+        *  - PASS: The user creation is ok. Additional secondary providers may 
run.
+        *  - ABSTAIN: Additional secondary providers may run.
+        *  - UI: Additional AuthenticationRequests are needed to complete the 
process.
+        *  - REDIRECT: Redirection to a third party is needed to complete the 
process.
+        */
+       public function beginSecondaryAccountCreation( $user, array $reqs ) {
+               return AuthenticationResponse::newAbstain();
+       }
+
+       /**
+        * Validate a change of authentication data (e.g. passwords)
+        * @param AuthenticationRequest $req
+        * @return StatusValue
+        */
+       public function providerAllowsAuthenticationDataChange( 
AuthenticationRequest $req ) {
+               if ( !isset( $reqs['PasswordAuthenticationRequest'] ) ) {
+                       return StatusValue::newGood();
+               }
+
+               $user = SessionManager::getGlobalSession()->getUser();
+               $oathuser = OATHUser::newFromUser( $user );
+
+               if ( !$oathuser || !$oathuser->isEnabled() || 
!$oathuser->isValidated() ) {
+                       return StatusValue::newGood();
+               }
+
+               // this is a password change and the user enabled two-factor; 
second factor is required
+
+               if ( !isset( $reqs['TOTPAuthenticationRequest'] ) ) {
+                       return StatusValue::newFatal( 'oathauth-required' );
+               }
+
+               $token = $reqs['TOTPAuthenticationRequest'];
+               if ( $oathuser->verifyToken( $token ) ) {
+                       return StatusValue::newGood();
+               } else {
+                       return StatusValue::newFatal( 'oathauth-abortlogin' );
+               }
+       }
+
+
+}
diff --git a/i18n/en.json b/i18n/en.json
index 5514f1f..14f1dfe 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -33,5 +33,9 @@
        "oathauth-notloggedin": "Login required",
        "oathauth-mustbeloggedin": "You must be logged in to perform this 
action.",
        "oathauth-prefs-label": "Two-factor authentication:",
-       "oathauth-abortlogin": "The two-factor authentication token provided 
was invalid."
+       "oathauth-abortlogin": "The two-factor authentication token provided 
was invalid.",
+       "oathauth-auth-token-label": "Token",
+       "oathauth-auth-token-help": "The one-time password used as the second 
factor of two-factor authentication.",
+       "oathauth-auth-ui": "Please enter verification code",
+       "oathauth-auth-required": "A verification code is required"
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index a45e378..9e742bb 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -37,5 +37,9 @@
        "oathauth-notloggedin": "Page title seen on Special:OATH when a user is 
not logged in.\n{{Identical|Login required}}",
        "oathauth-mustbeloggedin": "Plain text seen on Special:OATH when a user 
is not logged in.",
        "oathauth-prefs-label": "Plain text label seen on 
Special:Preferences\n\nSee 
[https://en.wikipedia.org/wiki/Two_factor_authentication two factor 
authentication]",
-       "oathauth-abortlogin": "Error message shown on login and password 
change pages when authentication is aborted.\n\nSee 
[https://en.wikipedia.org/wiki/Two_factor_authentication two factor 
authentication]"
+       "oathauth-abortlogin": "Error message shown on login and password 
change pages when authentication is aborted.\n\nSee 
[https://en.wikipedia.org/wiki/Two_factor_authentication two factor 
authentication]",
+       "oathauth-auth-token-label": "Label of the second-factor field on 
special pages and in the API",
+       "oathauth-auth-token-help": "Extended help message for the second 
factor field in the API.",
+       "oathauth-auth-ui": "Shown on top of the login form when second factor 
is required",
+       "oathauth-auth-required": "Error message used when a password change 
would require a verification code but none is given"
 }

-- 
To view, visit https://gerrit.wikimedia.org/r/253300
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ic492b8f2477c475f8414b61505139e9a1df2ba5b
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/OATHAuth
Gerrit-Branch: master
Gerrit-Owner: GergÅ‘ Tisza <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to