summaryrefslogtreecommitdiff
path: root/www/wiki/includes/auth
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/auth
first commit
Diffstat (limited to 'www/wiki/includes/auth')
-rw-r--r--www/wiki/includes/auth/AbstractAuthenticationProvider.php59
-rw-r--r--www/wiki/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php171
-rw-r--r--www/wiki/includes/auth/AbstractPreAuthenticationProvider.php62
-rw-r--r--www/wiki/includes/auth/AbstractPrimaryAuthenticationProvider.php118
-rw-r--r--www/wiki/includes/auth/AbstractSecondaryAuthenticationProvider.php86
-rw-r--r--www/wiki/includes/auth/AuthManager.php2455
-rw-r--r--www/wiki/includes/auth/AuthManagerAuthPlugin.php231
-rw-r--r--www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php429
-rw-r--r--www/wiki/includes/auth/AuthenticationProvider.php98
-rw-r--r--www/wiki/includes/auth/AuthenticationRequest.php379
-rw-r--r--www/wiki/includes/auth/AuthenticationResponse.php219
-rw-r--r--www/wiki/includes/auth/ButtonAuthenticationRequest.php108
-rw-r--r--www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php111
-rw-r--r--www/wiki/includes/auth/ConfirmLinkAuthenticationRequest.php80
-rw-r--r--www/wiki/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php158
-rw-r--r--www/wiki/includes/auth/CreateFromLoginAuthenticationRequest.php96
-rw-r--r--www/wiki/includes/auth/CreatedAccountAuthenticationRequest.php48
-rw-r--r--www/wiki/includes/auth/CreationReasonAuthenticationRequest.php24
-rw-r--r--www/wiki/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php70
-rw-r--r--www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php180
-rw-r--r--www/wiki/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php324
-rw-r--r--www/wiki/includes/auth/PasswordAuthenticationRequest.php85
-rw-r--r--www/wiki/includes/auth/PasswordDomainAuthenticationRequest.php85
-rw-r--r--www/wiki/includes/auth/PreAuthenticationProvider.php148
-rw-r--r--www/wiki/includes/auth/PrimaryAuthenticationProvider.php400
-rw-r--r--www/wiki/includes/auth/RememberMeAuthenticationRequest.php65
-rw-r--r--www/wiki/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php133
-rw-r--r--www/wiki/includes/auth/SecondaryAuthenticationProvider.php258
-rw-r--r--www/wiki/includes/auth/TemporaryPasswordAuthenticationRequest.php101
-rw-r--r--www/wiki/includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php477
-rw-r--r--www/wiki/includes/auth/ThrottlePreAuthenticationProvider.php180
-rw-r--r--www/wiki/includes/auth/Throttler.php208
-rw-r--r--www/wiki/includes/auth/UserDataAuthenticationRequest.php89
-rw-r--r--www/wiki/includes/auth/UsernameAuthenticationRequest.php39
34 files changed, 7774 insertions, 0 deletions
diff --git a/www/wiki/includes/auth/AbstractAuthenticationProvider.php b/www/wiki/includes/auth/AbstractAuthenticationProvider.php
new file mode 100644
index 00000000..58cec118
--- /dev/null
+++ b/www/wiki/includes/auth/AbstractAuthenticationProvider.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Config;
+use Psr\Log\LoggerInterface;
+
+/**
+ * A base class that implements some of the boilerplate for an AuthenticationProvider
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractAuthenticationProvider implements AuthenticationProvider {
+ /** @var LoggerInterface */
+ protected $logger;
+ /** @var AuthManager */
+ protected $manager;
+ /** @var Config */
+ protected $config;
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ public function setManager( AuthManager $manager ) {
+ $this->manager = $manager;
+ }
+
+ public function setConfig( Config $config ) {
+ $this->config = $config;
+ }
+
+ /**
+ * @inheritDoc
+ * @note Override this if it makes sense to support more than one instance
+ */
+ public function getUniqueId() {
+ return static::class;
+ }
+}
diff --git a/www/wiki/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php b/www/wiki/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php
new file mode 100644
index 00000000..f5bfc2a2
--- /dev/null
+++ b/www/wiki/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Password;
+use PasswordFactory;
+use Status;
+
+/**
+ * Basic framework for a primary authentication provider that uses passwords
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractPasswordPrimaryAuthenticationProvider
+ extends AbstractPrimaryAuthenticationProvider
+{
+ /** @var bool Whether this provider should ABSTAIN (false) or FAIL (true) on password failure */
+ protected $authoritative;
+
+ private $passwordFactory = null;
+
+ /**
+ * @param array $params Settings
+ * - authoritative: Whether this provider should ABSTAIN (false) or FAIL
+ * (true) on password failure
+ */
+ public function __construct( array $params = [] ) {
+ $this->authoritative = !isset( $params['authoritative'] ) || (bool)$params['authoritative'];
+ }
+
+ /**
+ * Get the PasswordFactory
+ * @return PasswordFactory
+ */
+ protected function getPasswordFactory() {
+ if ( $this->passwordFactory === null ) {
+ $this->passwordFactory = new PasswordFactory();
+ $this->passwordFactory->init( $this->config );
+ }
+ return $this->passwordFactory;
+ }
+
+ /**
+ * Get a Password object from the hash
+ * @param string $hash
+ * @return Password
+ */
+ protected function getPassword( $hash ) {
+ $passwordFactory = $this->getPasswordFactory();
+ try {
+ return $passwordFactory->newFromCiphertext( $hash );
+ } catch ( \PasswordError $e ) {
+ $class = static::class;
+ $this->logger->debug( "Invalid password hash in {$class}::getPassword()" );
+ return $passwordFactory->newFromCiphertext( null );
+ }
+ }
+
+ /**
+ * Return the appropriate response for failure
+ * @param PasswordAuthenticationRequest $req
+ * @return AuthenticationResponse
+ */
+ protected function failResponse( PasswordAuthenticationRequest $req ) {
+ if ( $this->authoritative ) {
+ return AuthenticationResponse::newFail(
+ wfMessage( $req->password === '' ? 'wrongpasswordempty' : 'wrongpassword' )
+ );
+ } else {
+ return AuthenticationResponse::newAbstain();
+ }
+ }
+
+ /**
+ * Check that the password is valid
+ *
+ * This should be called *before* validating the password. If the result is
+ * not ok, login should fail immediately.
+ *
+ * @param string $username
+ * @param string $password
+ * @return Status
+ */
+ protected function checkPasswordValidity( $username, $password ) {
+ return \User::newFromName( $username )->checkPasswordValidity( $password );
+ }
+
+ /**
+ * Check if the password should be reset
+ *
+ * This should be called after a successful login. It sets 'reset-pass'
+ * authentication data if necessary, see
+ * ResetPassSecondaryAuthenticationProvider.
+ *
+ * @param string $username
+ * @param Status $status From $this->checkPasswordValidity()
+ * @param mixed $data Passed through to $this->getPasswordResetData()
+ */
+ protected function setPasswordResetFlag( $username, Status $status, $data = null ) {
+ $reset = $this->getPasswordResetData( $username, $data );
+
+ if ( !$reset && $this->config->get( 'InvalidPasswordReset' ) && !$status->isGood() ) {
+ $reset = (object)[
+ 'msg' => $status->getMessage( 'resetpass-validity-soft' ),
+ 'hard' => false,
+ ];
+ }
+
+ if ( $reset ) {
+ $this->manager->setAuthenticationSessionData( 'reset-pass', $reset );
+ }
+ }
+
+ /**
+ * Get password reset data, if any
+ *
+ * @param string $username
+ * @param mixed $data
+ * @return object|null { 'hard' => bool, 'msg' => Message }
+ */
+ protected function getPasswordResetData( $username, $data ) {
+ return null;
+ }
+
+ /**
+ * Get expiration date for a new password, if any
+ *
+ * @param string $username
+ * @return string|null
+ */
+ protected function getNewPasswordExpiry( $username ) {
+ $days = $this->config->get( 'PasswordExpirationDays' );
+ $expires = $days ? wfTimestamp( TS_MW, time() + $days * 86400 ) : null;
+
+ // Give extensions a chance to force an expiration
+ \Hooks::run( 'ResetPasswordExpiration', [ \User::newFromName( $username ), &$expires ] );
+
+ return $expires;
+ }
+
+ public function getAuthenticationRequests( $action, array $options ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_LOGIN:
+ case AuthManager::ACTION_REMOVE:
+ case AuthManager::ACTION_CREATE:
+ case AuthManager::ACTION_CHANGE:
+ return [ new PasswordAuthenticationRequest() ];
+ default:
+ return [];
+ }
+ }
+}
diff --git a/www/wiki/includes/auth/AbstractPreAuthenticationProvider.php b/www/wiki/includes/auth/AbstractPreAuthenticationProvider.php
new file mode 100644
index 00000000..d997dbbc
--- /dev/null
+++ b/www/wiki/includes/auth/AbstractPreAuthenticationProvider.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * A base class that implements some of the boilerplate for a PreAuthenticationProvider
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractPreAuthenticationProvider extends AbstractAuthenticationProvider
+ implements PreAuthenticationProvider
+{
+
+ public function getAuthenticationRequests( $action, array $options ) {
+ return [];
+ }
+
+ public function testForAuthentication( array $reqs ) {
+ return \StatusValue::newGood();
+ }
+
+ public function postAuthentication( $user, AuthenticationResponse $response ) {
+ }
+
+ public function testForAccountCreation( $user, $creator, array $reqs ) {
+ return \StatusValue::newGood();
+ }
+
+ public function testUserForCreation( $user, $autocreate, array $options = [] ) {
+ return \StatusValue::newGood();
+ }
+
+ public function postAccountCreation( $user, $creator, AuthenticationResponse $response ) {
+ }
+
+ public function testForAccountLink( $user ) {
+ return \StatusValue::newGood();
+ }
+
+ public function postAccountLink( $user, AuthenticationResponse $response ) {
+ }
+
+}
diff --git a/www/wiki/includes/auth/AbstractPrimaryAuthenticationProvider.php b/www/wiki/includes/auth/AbstractPrimaryAuthenticationProvider.php
new file mode 100644
index 00000000..ca947b61
--- /dev/null
+++ b/www/wiki/includes/auth/AbstractPrimaryAuthenticationProvider.php
@@ -0,0 +1,118 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use User;
+
+/**
+ * A base class that implements some of the boilerplate for a PrimaryAuthenticationProvider
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractPrimaryAuthenticationProvider extends AbstractAuthenticationProvider
+ implements PrimaryAuthenticationProvider
+{
+
+ public function continuePrimaryAuthentication( array $reqs ) {
+ throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+ }
+
+ public function postAuthentication( $user, AuthenticationResponse $response ) {
+ }
+
+ public function testUserCanAuthenticate( $username ) {
+ // Assume it can authenticate if it exists
+ return $this->testUserExists( $username );
+ }
+
+ /**
+ * @inheritDoc
+ * @note Reimplement this if you do anything other than
+ * User::getCanonicalName( $req->username ) to determine the user being
+ * authenticated.
+ */
+ public function providerNormalizeUsername( $username ) {
+ $name = User::getCanonicalName( $username );
+ return $name === false ? null : $name;
+ }
+
+ /**
+ * @inheritDoc
+ * @note Reimplement this if self::getAuthenticationRequests( AuthManager::ACTION_REMOVE )
+ * doesn't return requests that will revoke all access for the user.
+ */
+ public function providerRevokeAccessForUser( $username ) {
+ $reqs = $this->getAuthenticationRequests(
+ AuthManager::ACTION_REMOVE, [ 'username' => $username ]
+ );
+ foreach ( $reqs as $req ) {
+ $req->username = $username;
+ $req->action = AuthManager::ACTION_REMOVE;
+ $this->providerChangeAuthenticationData( $req );
+ }
+ }
+
+ public function providerAllowsPropertyChange( $property ) {
+ return true;
+ }
+
+ public function testForAccountCreation( $user, $creator, array $reqs ) {
+ return \StatusValue::newGood();
+ }
+
+ public function continuePrimaryAccountCreation( $user, $creator, array $reqs ) {
+ throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+ }
+
+ public function finishAccountCreation( $user, $creator, AuthenticationResponse $response ) {
+ return null;
+ }
+
+ public function postAccountCreation( $user, $creator, AuthenticationResponse $response ) {
+ }
+
+ public function testUserForCreation( $user, $autocreate, array $options = [] ) {
+ return \StatusValue::newGood();
+ }
+
+ public function autoCreatedAccount( $user, $source ) {
+ }
+
+ public function beginPrimaryAccountLink( $user, array $reqs ) {
+ if ( $this->accountCreationType() === self::TYPE_LINK ) {
+ throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+ } else {
+ throw new \BadMethodCallException(
+ __METHOD__ . ' should not be called on a non-link provider.'
+ );
+ }
+ }
+
+ public function continuePrimaryAccountLink( $user, array $reqs ) {
+ throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+ }
+
+ public function postAccountLink( $user, AuthenticationResponse $response ) {
+ }
+
+}
diff --git a/www/wiki/includes/auth/AbstractSecondaryAuthenticationProvider.php b/www/wiki/includes/auth/AbstractSecondaryAuthenticationProvider.php
new file mode 100644
index 00000000..4a2accaf
--- /dev/null
+++ b/www/wiki/includes/auth/AbstractSecondaryAuthenticationProvider.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * A base class that implements some of the boilerplate for a SecondaryAuthenticationProvider
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AbstractSecondaryAuthenticationProvider extends AbstractAuthenticationProvider
+ implements SecondaryAuthenticationProvider
+{
+
+ public function continueSecondaryAuthentication( $user, array $reqs ) {
+ throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+ }
+
+ public function postAuthentication( $user, AuthenticationResponse $response ) {
+ }
+
+ public function providerAllowsPropertyChange( $property ) {
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ * @note Reimplement this if self::getAuthenticationRequests( AuthManager::ACTION_REMOVE )
+ * doesn't return requests that will revoke all access for the user.
+ */
+ public function providerRevokeAccessForUser( $username ) {
+ $reqs = $this->getAuthenticationRequests(
+ AuthManager::ACTION_REMOVE, [ 'username' => $username ]
+ );
+ foreach ( $reqs as $req ) {
+ $req->username = $username;
+ $this->providerChangeAuthenticationData( $req );
+ }
+ }
+
+ public function providerAllowsAuthenticationDataChange(
+ AuthenticationRequest $req, $checkData = true
+ ) {
+ return \StatusValue::newGood( 'ignored' );
+ }
+
+ public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
+ }
+
+ public function testForAccountCreation( $user, $creator, array $reqs ) {
+ return \StatusValue::newGood();
+ }
+
+ public function continueSecondaryAccountCreation( $user, $creator, array $reqs ) {
+ throw new \BadMethodCallException( __METHOD__ . ' is not implemented.' );
+ }
+
+ public function postAccountCreation( $user, $creator, AuthenticationResponse $response ) {
+ }
+
+ public function testUserForCreation( $user, $autocreate, array $options = [] ) {
+ return \StatusValue::newGood();
+ }
+
+ public function autoCreatedAccount( $user, $source ) {
+ }
+}
diff --git a/www/wiki/includes/auth/AuthManager.php b/www/wiki/includes/auth/AuthManager.php
new file mode 100644
index 00000000..611a8cdc
--- /dev/null
+++ b/www/wiki/includes/auth/AuthManager.php
@@ -0,0 +1,2455 @@
+<?php
+/**
+ * Authentication (and possibly Authorization in the future) system entry point
+ *
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Config;
+use MediaWiki\MediaWikiServices;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Status;
+use StatusValue;
+use User;
+use WebRequest;
+use Wikimedia\ObjectFactory;
+
+/**
+ * This serves as the entry point to the authentication system.
+ *
+ * In the future, it may also serve as the entry point to the authorization
+ * system.
+ *
+ * If you are looking at this because you are working on an extension that creates its own
+ * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
+ * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
+ * or the createaccount API. Trying to call this class directly will very likely end up in
+ * security vulnerabilities or broken UX in edge cases.
+ *
+ * If you are working on an extension that needs to integrate with the authentication system
+ * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
+ * need to write an AuthenticationProvider.
+ *
+ * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
+ * you are looking for. If you want to change user data, use User::changeAuthenticationData().
+ * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
+ * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
+ * responsibility to ensure that the user can authenticate somehow (see especially
+ * PrimaryAuthenticationProvider::autoCreatedAccount()).
+ * If you are writing code that is not associated with such a provider and needs to create accounts
+ * programmatically for real users, you should rethink your architecture. There is no good way to
+ * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
+ * cannot provide any means for users to access the accounts it would create.
+ *
+ * The two main control flows when using this class are as follows:
+ * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
+ * the requests with data (by using them to build a HTMLForm and have the user fill it, or by
+ * exposing a form specification via the API, so that the client can build it), and pass them to
+ * the appropriate begin* method. That will return either a success/failure response, or more
+ * requests to fill (either by building a form or by redirecting the user to some external
+ * provider which will send the data back), in which case they need to be submitted to the
+ * appropriate continue* method and that step has to be repeated until the response is a success
+ * or failure response. AuthManager will use the session to maintain internal state during the
+ * process.
+ * * Code doing an authentication data change will call getAuthenticationRequests(), select
+ * a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
+ * changeAuthenticationData(). If the data change is user-initiated, the whole process needs
+ * to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
+ * a non-OK status.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
+ */
+class AuthManager implements LoggerAwareInterface {
+ /** Log in with an existing (not necessarily local) user */
+ const ACTION_LOGIN = 'login';
+ /** Continue a login process that was interrupted by the need for user input or communication
+ * with an external provider */
+ const ACTION_LOGIN_CONTINUE = 'login-continue';
+ /** Create a new user */
+ const ACTION_CREATE = 'create';
+ /** Continue a user creation process that was interrupted by the need for user input or
+ * communication with an external provider */
+ const ACTION_CREATE_CONTINUE = 'create-continue';
+ /** Link an existing user to a third-party account */
+ const ACTION_LINK = 'link';
+ /** Continue a user linking process that was interrupted by the need for user input or
+ * communication with an external provider */
+ const ACTION_LINK_CONTINUE = 'link-continue';
+ /** Change a user's credentials */
+ const ACTION_CHANGE = 'change';
+ /** Remove a user's credentials */
+ const ACTION_REMOVE = 'remove';
+ /** Like ACTION_REMOVE but for linking providers only */
+ const ACTION_UNLINK = 'unlink';
+
+ /** Security-sensitive operations are ok. */
+ const SEC_OK = 'ok';
+ /** Security-sensitive operations should re-authenticate. */
+ const SEC_REAUTH = 'reauth';
+ /** Security-sensitive should not be performed. */
+ const SEC_FAIL = 'fail';
+
+ /** Auto-creation is due to SessionManager */
+ const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
+
+ /** @var AuthManager|null */
+ private static $instance = null;
+
+ /** @var WebRequest */
+ private $request;
+
+ /** @var Config */
+ private $config;
+
+ /** @var LoggerInterface */
+ private $logger;
+
+ /** @var AuthenticationProvider[] */
+ private $allAuthenticationProviders = [];
+
+ /** @var PreAuthenticationProvider[] */
+ private $preAuthenticationProviders = null;
+
+ /** @var PrimaryAuthenticationProvider[] */
+ private $primaryAuthenticationProviders = null;
+
+ /** @var SecondaryAuthenticationProvider[] */
+ private $secondaryAuthenticationProviders = null;
+
+ /** @var CreatedAccountAuthenticationRequest[] */
+ private $createdAccountAuthenticationRequests = [];
+
+ /**
+ * Get the global AuthManager
+ * @return AuthManager
+ */
+ public static function singleton() {
+ if ( self::$instance === null ) {
+ self::$instance = new self(
+ \RequestContext::getMain()->getRequest(),
+ MediaWikiServices::getInstance()->getMainConfig()
+ );
+ }
+ return self::$instance;
+ }
+
+ /**
+ * @param WebRequest $request
+ * @param Config $config
+ */
+ public function __construct( WebRequest $request, Config $config ) {
+ $this->request = $request;
+ $this->config = $config;
+ $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
+ }
+
+ /**
+ * @param LoggerInterface $logger
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @return WebRequest
+ */
+ public function getRequest() {
+ return $this->request;
+ }
+
+ /**
+ * Force certain PrimaryAuthenticationProviders
+ * @deprecated For backwards compatibility only
+ * @param PrimaryAuthenticationProvider[] $providers
+ * @param string $why
+ */
+ public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
+ $this->logger->warning( "Overriding AuthManager primary authn because $why" );
+
+ if ( $this->primaryAuthenticationProviders !== null ) {
+ $this->logger->warning(
+ 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
+ );
+
+ $this->allAuthenticationProviders = array_diff_key(
+ $this->allAuthenticationProviders,
+ $this->primaryAuthenticationProviders
+ );
+ $session = $this->request->getSession();
+ $session->remove( 'AuthManager::authnState' );
+ $session->remove( 'AuthManager::accountCreationState' );
+ $session->remove( 'AuthManager::accountLinkState' );
+ $this->createdAccountAuthenticationRequests = [];
+ }
+
+ $this->primaryAuthenticationProviders = [];
+ foreach ( $providers as $provider ) {
+ if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+ throw new \RuntimeException(
+ 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
+ get_class( $provider )
+ );
+ }
+ $provider->setLogger( $this->logger );
+ $provider->setManager( $this );
+ $provider->setConfig( $this->config );
+ $id = $provider->getUniqueId();
+ if ( isset( $this->allAuthenticationProviders[$id] ) ) {
+ throw new \RuntimeException(
+ "Duplicate specifications for id $id (classes " .
+ get_class( $provider ) . ' and ' .
+ get_class( $this->allAuthenticationProviders[$id] ) . ')'
+ );
+ }
+ $this->allAuthenticationProviders[$id] = $provider;
+ $this->primaryAuthenticationProviders[$id] = $provider;
+ }
+ }
+
+ /**
+ * Call a legacy AuthPlugin method, if necessary
+ * @codeCoverageIgnore
+ * @deprecated For backwards compatibility only, should be avoided in new code
+ * @param string $method AuthPlugin method to call
+ * @param array $params Parameters to pass
+ * @param mixed $return Return value if AuthPlugin wasn't called
+ * @return mixed Return value from the AuthPlugin method, or $return
+ */
+ public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
+ global $wgAuth;
+
+ if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
+ return call_user_func_array( [ $wgAuth, $method ], $params );
+ } else {
+ return $return;
+ }
+ }
+
+ /**
+ * @name Authentication
+ * @{
+ */
+
+ /**
+ * Indicate whether user authentication is possible
+ *
+ * It may not be if the session is provided by something like OAuth
+ * for which each individual request includes authentication data.
+ *
+ * @return bool
+ */
+ public function canAuthenticateNow() {
+ return $this->request->getSession()->canSetUser();
+ }
+
+ /**
+ * Start an authentication flow
+ *
+ * In addition to the AuthenticationRequests returned by
+ * $this->getAuthenticationRequests(), a client might include a
+ * CreateFromLoginAuthenticationRequest from a previous login attempt to
+ * preserve state.
+ *
+ * Instead of the AuthenticationRequests returned by
+ * $this->getAuthenticationRequests(), a client might pass a
+ * CreatedAccountAuthenticationRequest from an account creation that just
+ * succeeded to log in to the just-created account.
+ *
+ * @param AuthenticationRequest[] $reqs
+ * @param string $returnToUrl Url that REDIRECT responses should eventually
+ * return to.
+ * @return AuthenticationResponse See self::continueAuthentication()
+ */
+ public function beginAuthentication( array $reqs, $returnToUrl ) {
+ $session = $this->request->getSession();
+ if ( !$session->canSetUser() ) {
+ // Caller should have called canAuthenticateNow()
+ $session->remove( 'AuthManager::authnState' );
+ throw new \LogicException( 'Authentication is not possible now' );
+ }
+
+ $guessUserName = null;
+ foreach ( $reqs as $req ) {
+ $req->returnToUrl = $returnToUrl;
+ // @codeCoverageIgnoreStart
+ if ( $req->username !== null && $req->username !== '' ) {
+ if ( $guessUserName === null ) {
+ $guessUserName = $req->username;
+ } elseif ( $guessUserName !== $req->username ) {
+ $guessUserName = null;
+ break;
+ }
+ }
+ // @codeCoverageIgnoreEnd
+ }
+
+ // Check for special-case login of a just-created account
+ $req = AuthenticationRequest::getRequestByClass(
+ $reqs, CreatedAccountAuthenticationRequest::class
+ );
+ if ( $req ) {
+ if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
+ throw new \LogicException(
+ 'CreatedAccountAuthenticationRequests are only valid on ' .
+ 'the same AuthManager that created the account'
+ );
+ }
+
+ $user = User::newFromName( $req->username );
+ // @codeCoverageIgnoreStart
+ if ( !$user ) {
+ throw new \UnexpectedValueException(
+ "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
+ );
+ } elseif ( $user->getId() != $req->id ) {
+ throw new \UnexpectedValueException(
+ "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
+ );
+ }
+ // @codeCoverageIgnoreEnd
+
+ $this->logger->info( 'Logging in {user} after account creation', [
+ 'user' => $user->getName(),
+ ] );
+ $ret = AuthenticationResponse::newPass( $user->getName() );
+ $this->setSessionDataForUser( $user );
+ $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
+ $session->remove( 'AuthManager::authnState' );
+ \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
+ return $ret;
+ }
+
+ $this->removeAuthenticationSessionData( null );
+
+ foreach ( $this->getPreAuthenticationProviders() as $provider ) {
+ $status = $provider->testForAuthentication( $reqs );
+ if ( !$status->isGood() ) {
+ $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
+ $ret = AuthenticationResponse::newFail(
+ Status::wrap( $status )->getMessage()
+ );
+ $this->callMethodOnProviders( 7, 'postAuthentication',
+ [ User::newFromName( $guessUserName ) ?: null, $ret ]
+ );
+ \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
+ return $ret;
+ }
+ }
+
+ $state = [
+ 'reqs' => $reqs,
+ 'returnToUrl' => $returnToUrl,
+ 'guessUserName' => $guessUserName,
+ 'primary' => null,
+ 'primaryResponse' => null,
+ 'secondary' => [],
+ 'maybeLink' => [],
+ 'continueRequests' => [],
+ ];
+
+ // Preserve state from a previous failed login
+ $req = AuthenticationRequest::getRequestByClass(
+ $reqs, CreateFromLoginAuthenticationRequest::class
+ );
+ if ( $req ) {
+ $state['maybeLink'] = $req->maybeLink;
+ }
+
+ $session = $this->request->getSession();
+ $session->setSecret( 'AuthManager::authnState', $state );
+ $session->persist();
+
+ return $this->continueAuthentication( $reqs );
+ }
+
+ /**
+ * Continue an authentication flow
+ *
+ * Return values are interpreted as follows:
+ * - status FAIL: Authentication failed. If $response->createRequest is
+ * set, that may be passed to self::beginAuthentication() or to
+ * self::beginAccountCreation() to preserve state.
+ * - status REDIRECT: The client should be redirected to the contained URL,
+ * new AuthenticationRequests should be made (if any), then
+ * AuthManager::continueAuthentication() should be called.
+ * - status UI: The client should be presented with a user interface for
+ * the fields in the specified AuthenticationRequests, then new
+ * AuthenticationRequests should be made, then
+ * AuthManager::continueAuthentication() should be called.
+ * - status RESTART: The user logged in successfully with a third-party
+ * service, but the third-party credentials aren't attached to any local
+ * account. This could be treated as a UI or a FAIL.
+ * - status PASS: Authentication was successful.
+ *
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse
+ */
+ public function continueAuthentication( array $reqs ) {
+ $session = $this->request->getSession();
+ try {
+ if ( !$session->canSetUser() ) {
+ // Caller should have called canAuthenticateNow()
+ // @codeCoverageIgnoreStart
+ throw new \LogicException( 'Authentication is not possible now' );
+ // @codeCoverageIgnoreEnd
+ }
+
+ $state = $session->getSecret( 'AuthManager::authnState' );
+ if ( !is_array( $state ) ) {
+ return AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-authn-not-in-progress' )
+ );
+ }
+ $state['continueRequests'] = [];
+
+ $guessUserName = $state['guessUserName'];
+
+ foreach ( $reqs as $req ) {
+ $req->returnToUrl = $state['returnToUrl'];
+ }
+
+ // Step 1: Choose an primary authentication provider, and call it until it succeeds.
+
+ if ( $state['primary'] === null ) {
+ // We haven't picked a PrimaryAuthenticationProvider yet
+ // @codeCoverageIgnoreStart
+ $guessUserName = null;
+ foreach ( $reqs as $req ) {
+ if ( $req->username !== null && $req->username !== '' ) {
+ if ( $guessUserName === null ) {
+ $guessUserName = $req->username;
+ } elseif ( $guessUserName !== $req->username ) {
+ $guessUserName = null;
+ break;
+ }
+ }
+ }
+ $state['guessUserName'] = $guessUserName;
+ // @codeCoverageIgnoreEnd
+ $state['reqs'] = $reqs;
+
+ foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
+ $res = $provider->beginPrimaryAuthentication( $reqs );
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS;
+ $state['primary'] = $id;
+ $state['primaryResponse'] = $res;
+ $this->logger->debug( "Primary login with $id succeeded" );
+ break 2;
+ case AuthenticationResponse::FAIL;
+ $this->logger->debug( "Login failed in primary authentication by $id" );
+ if ( $res->createRequest || $state['maybeLink'] ) {
+ $res->createRequest = new CreateFromLoginAuthenticationRequest(
+ $res->createRequest, $state['maybeLink']
+ );
+ }
+ $this->callMethodOnProviders( 7, 'postAuthentication',
+ [ User::newFromName( $guessUserName ) ?: null, $res ]
+ );
+ $session->remove( 'AuthManager::authnState' );
+ \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
+ return $res;
+ case AuthenticationResponse::ABSTAIN;
+ // Continue loop
+ break;
+ case AuthenticationResponse::REDIRECT;
+ case AuthenticationResponse::UI;
+ $this->logger->debug( "Primary login with $id returned $res->status" );
+ $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
+ $state['primary'] = $id;
+ $state['continueRequests'] = $res->neededRequests;
+ $session->setSecret( 'AuthManager::authnState', $state );
+ return $res;
+
+ // @codeCoverageIgnoreStart
+ default:
+ throw new \DomainException(
+ get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
+ );
+ // @codeCoverageIgnoreEnd
+ }
+ }
+ if ( $state['primary'] === null ) {
+ $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
+ $ret = AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-authn-no-primary' )
+ );
+ $this->callMethodOnProviders( 7, 'postAuthentication',
+ [ User::newFromName( $guessUserName ) ?: null, $ret ]
+ );
+ $session->remove( 'AuthManager::authnState' );
+ return $ret;
+ }
+ } elseif ( $state['primaryResponse'] === null ) {
+ $provider = $this->getAuthenticationProvider( $state['primary'] );
+ if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+ // Configuration changed? Force them to start over.
+ // @codeCoverageIgnoreStart
+ $ret = AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-authn-not-in-progress' )
+ );
+ $this->callMethodOnProviders( 7, 'postAuthentication',
+ [ User::newFromName( $guessUserName ) ?: null, $ret ]
+ );
+ $session->remove( 'AuthManager::authnState' );
+ return $ret;
+ // @codeCoverageIgnoreEnd
+ }
+ $id = $provider->getUniqueId();
+ $res = $provider->continuePrimaryAuthentication( $reqs );
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS;
+ $state['primaryResponse'] = $res;
+ $this->logger->debug( "Primary login with $id succeeded" );
+ break;
+ case AuthenticationResponse::FAIL;
+ $this->logger->debug( "Login failed in primary authentication by $id" );
+ if ( $res->createRequest || $state['maybeLink'] ) {
+ $res->createRequest = new CreateFromLoginAuthenticationRequest(
+ $res->createRequest, $state['maybeLink']
+ );
+ }
+ $this->callMethodOnProviders( 7, 'postAuthentication',
+ [ User::newFromName( $guessUserName ) ?: null, $res ]
+ );
+ $session->remove( 'AuthManager::authnState' );
+ \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
+ return $res;
+ case AuthenticationResponse::REDIRECT;
+ case AuthenticationResponse::UI;
+ $this->logger->debug( "Primary login with $id returned $res->status" );
+ $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
+ $state['continueRequests'] = $res->neededRequests;
+ $session->setSecret( 'AuthManager::authnState', $state );
+ return $res;
+ default:
+ throw new \DomainException(
+ get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
+ );
+ }
+ }
+
+ $res = $state['primaryResponse'];
+ if ( $res->username === null ) {
+ $provider = $this->getAuthenticationProvider( $state['primary'] );
+ if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+ // Configuration changed? Force them to start over.
+ // @codeCoverageIgnoreStart
+ $ret = AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-authn-not-in-progress' )
+ );
+ $this->callMethodOnProviders( 7, 'postAuthentication',
+ [ User::newFromName( $guessUserName ) ?: null, $ret ]
+ );
+ $session->remove( 'AuthManager::authnState' );
+ return $ret;
+ // @codeCoverageIgnoreEnd
+ }
+
+ if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
+ $res->linkRequest &&
+ // don't confuse the user with an incorrect message if linking is disabled
+ $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
+ ) {
+ $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
+ $msg = 'authmanager-authn-no-local-user-link';
+ } else {
+ $msg = 'authmanager-authn-no-local-user';
+ }
+ $this->logger->debug(
+ "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
+ );
+ $ret = AuthenticationResponse::newRestart( wfMessage( $msg ) );
+ $ret->neededRequests = $this->getAuthenticationRequestsInternal(
+ self::ACTION_LOGIN,
+ [],
+ $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
+ );
+ if ( $res->createRequest || $state['maybeLink'] ) {
+ $ret->createRequest = new CreateFromLoginAuthenticationRequest(
+ $res->createRequest, $state['maybeLink']
+ );
+ $ret->neededRequests[] = $ret->createRequest;
+ }
+ $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
+ $session->setSecret( 'AuthManager::authnState', [
+ 'reqs' => [], // Will be filled in later
+ 'primary' => null,
+ 'primaryResponse' => null,
+ 'secondary' => [],
+ 'continueRequests' => $ret->neededRequests,
+ ] + $state );
+ return $ret;
+ }
+
+ // Step 2: Primary authentication succeeded, create the User object
+ // (and add the user locally if necessary)
+
+ $user = User::newFromName( $res->username, 'usable' );
+ if ( !$user ) {
+ $provider = $this->getAuthenticationProvider( $state['primary'] );
+ throw new \DomainException(
+ get_class( $provider ) . " returned an invalid username: {$res->username}"
+ );
+ }
+ if ( $user->getId() === 0 ) {
+ // User doesn't exist locally. Create it.
+ $this->logger->info( 'Auto-creating {user} on login', [
+ 'user' => $user->getName(),
+ ] );
+ $status = $this->autoCreateUser( $user, $state['primary'], false );
+ if ( !$status->isGood() ) {
+ $ret = AuthenticationResponse::newFail(
+ Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
+ );
+ $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
+ $session->remove( 'AuthManager::authnState' );
+ \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
+ return $ret;
+ }
+ }
+
+ // Step 3: Iterate over all the secondary authentication providers.
+
+ $beginReqs = $state['reqs'];
+
+ foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
+ if ( !isset( $state['secondary'][$id] ) ) {
+ // This provider isn't started yet, so we pass it the set
+ // of reqs from beginAuthentication instead of whatever
+ // might have been used by a previous provider in line.
+ $func = 'beginSecondaryAuthentication';
+ $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
+ } elseif ( !$state['secondary'][$id] ) {
+ $func = 'continueSecondaryAuthentication';
+ $res = $provider->continueSecondaryAuthentication( $user, $reqs );
+ } else {
+ continue;
+ }
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS;
+ $this->logger->debug( "Secondary login with $id succeeded" );
+ // fall through
+ case AuthenticationResponse::ABSTAIN;
+ $state['secondary'][$id] = true;
+ break;
+ case AuthenticationResponse::FAIL;
+ $this->logger->debug( "Login failed in secondary authentication by $id" );
+ $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
+ $session->remove( 'AuthManager::authnState' );
+ \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
+ return $res;
+ case AuthenticationResponse::REDIRECT;
+ case AuthenticationResponse::UI;
+ $this->logger->debug( "Secondary login with $id returned " . $res->status );
+ $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
+ $state['secondary'][$id] = false;
+ $state['continueRequests'] = $res->neededRequests;
+ $session->setSecret( 'AuthManager::authnState', $state );
+ return $res;
+
+ // @codeCoverageIgnoreStart
+ default:
+ throw new \DomainException(
+ get_class( $provider ) . "::{$func}() returned $res->status"
+ );
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
+ // Step 4: Authentication complete! Set the user in the session and
+ // clean up.
+
+ $this->logger->info( 'Login for {user} succeeded from {clientip}', [
+ 'user' => $user->getName(),
+ 'clientip' => $this->request->getIP(),
+ ] );
+ /** @var RememberMeAuthenticationRequest $req */
+ $req = AuthenticationRequest::getRequestByClass(
+ $beginReqs, RememberMeAuthenticationRequest::class
+ );
+ $this->setSessionDataForUser( $user, $req && $req->rememberMe );
+ $ret = AuthenticationResponse::newPass( $user->getName() );
+ $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
+ $session->remove( 'AuthManager::authnState' );
+ $this->removeAuthenticationSessionData( null );
+ \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
+ return $ret;
+ } catch ( \Exception $ex ) {
+ $session->remove( 'AuthManager::authnState' );
+ throw $ex;
+ }
+ }
+
+ /**
+ * Whether security-sensitive operations should proceed.
+ *
+ * A "security-sensitive operation" is something like a password or email
+ * change, that would normally have a "reenter your password to confirm"
+ * box if we only supported password-based authentication.
+ *
+ * @param string $operation Operation being checked. This should be a
+ * message-key-like string such as 'change-password' or 'change-email'.
+ * @return string One of the SEC_* constants.
+ */
+ public function securitySensitiveOperationStatus( $operation ) {
+ $status = self::SEC_OK;
+
+ $this->logger->debug( __METHOD__ . ": Checking $operation" );
+
+ $session = $this->request->getSession();
+ $aId = $session->getUser()->getId();
+ if ( $aId === 0 ) {
+ // User isn't authenticated. DWIM?
+ $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
+ $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
+ return $status;
+ }
+
+ if ( $session->canSetUser() ) {
+ $id = $session->get( 'AuthManager:lastAuthId' );
+ $last = $session->get( 'AuthManager:lastAuthTimestamp' );
+ if ( $id !== $aId || $last === null ) {
+ $timeSinceLogin = PHP_INT_MAX; // Forever ago
+ } else {
+ $timeSinceLogin = max( 0, time() - $last );
+ }
+
+ $thresholds = $this->config->get( 'ReauthenticateTime' );
+ if ( isset( $thresholds[$operation] ) ) {
+ $threshold = $thresholds[$operation];
+ } elseif ( isset( $thresholds['default'] ) ) {
+ $threshold = $thresholds['default'];
+ } else {
+ throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
+ }
+
+ if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
+ $status = self::SEC_REAUTH;
+ }
+ } else {
+ $timeSinceLogin = -1;
+
+ $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
+ if ( isset( $pass[$operation] ) ) {
+ $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
+ } elseif ( isset( $pass['default'] ) ) {
+ $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
+ } else {
+ throw new \UnexpectedValueException(
+ '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
+ );
+ }
+ }
+
+ \Hooks::run( 'SecuritySensitiveOperationStatus', [
+ &$status, $operation, $session, $timeSinceLogin
+ ] );
+
+ // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
+ if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
+ $status = self::SEC_FAIL;
+ }
+
+ $this->logger->info( __METHOD__ . ": $operation is $status" );
+
+ return $status;
+ }
+
+ /**
+ * Determine whether a username can authenticate
+ *
+ * This is mainly for internal purposes and only takes authentication data into account,
+ * not things like blocks that can change without the authentication system being aware.
+ *
+ * @param string $username MediaWiki username
+ * @return bool
+ */
+ public function userCanAuthenticate( $username ) {
+ foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+ if ( $provider->testUserCanAuthenticate( $username ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Provide normalized versions of the username for security checks
+ *
+ * Since different providers can normalize the input in different ways,
+ * this returns an array of all the different ways the name might be
+ * normalized for authentication.
+ *
+ * The returned strings should not be revealed to the user, as that might
+ * leak private information (e.g. an email address might be normalized to a
+ * username).
+ *
+ * @param string $username
+ * @return string[]
+ */
+ public function normalizeUsername( $username ) {
+ $ret = [];
+ foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+ $normalized = $provider->providerNormalizeUsername( $username );
+ if ( $normalized !== null ) {
+ $ret[$normalized] = true;
+ }
+ }
+ return array_keys( $ret );
+ }
+
+ /**@}*/
+
+ /**
+ * @name Authentication data changing
+ * @{
+ */
+
+ /**
+ * Revoke any authentication credentials for a user
+ *
+ * After this, the user should no longer be able to log in.
+ *
+ * @param string $username
+ */
+ public function revokeAccessForUser( $username ) {
+ $this->logger->info( 'Revoking access for {user}', [
+ 'user' => $username,
+ ] );
+ $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
+ }
+
+ /**
+ * Validate a change of authentication data (e.g. passwords)
+ * @param AuthenticationRequest $req
+ * @param bool $checkData If false, $req hasn't been loaded from the
+ * submission so checks on user-submitted fields should be skipped. $req->username is
+ * considered user-submitted for this purpose, even if it cannot be changed via
+ * $req->loadFromSubmission.
+ * @return Status
+ */
+ public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
+ $any = false;
+ $providers = $this->getPrimaryAuthenticationProviders() +
+ $this->getSecondaryAuthenticationProviders();
+ foreach ( $providers as $provider ) {
+ $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
+ if ( !$status->isGood() ) {
+ return Status::wrap( $status );
+ }
+ $any = $any || $status->value !== 'ignored';
+ }
+ if ( !$any ) {
+ $status = Status::newGood( 'ignored' );
+ $status->warning( 'authmanager-change-not-supported' );
+ return $status;
+ }
+ return Status::newGood();
+ }
+
+ /**
+ * Change authentication data (e.g. passwords)
+ *
+ * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
+ * result in a successful login in the future.
+ *
+ * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
+ * no longer result in a successful login.
+ *
+ * This method should only be called if allowsAuthenticationDataChange( $req, true )
+ * returned success.
+ *
+ * @param AuthenticationRequest $req
+ */
+ public function changeAuthenticationData( AuthenticationRequest $req ) {
+ $this->logger->info( 'Changing authentication data for {user} class {what}', [
+ 'user' => is_string( $req->username ) ? $req->username : '<no name>',
+ 'what' => get_class( $req ),
+ ] );
+
+ $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
+
+ // When the main account's authentication data is changed, invalidate
+ // all BotPasswords too.
+ \BotPassword::invalidateAllPasswordsForUser( $req->username );
+ }
+
+ /**@}*/
+
+ /**
+ * @name Account creation
+ * @{
+ */
+
+ /**
+ * Determine whether accounts can be created
+ * @return bool
+ */
+ public function canCreateAccounts() {
+ foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+ switch ( $provider->accountCreationType() ) {
+ case PrimaryAuthenticationProvider::TYPE_CREATE:
+ case PrimaryAuthenticationProvider::TYPE_LINK:
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determine whether a particular account can be created
+ * @param string $username MediaWiki username
+ * @param array $options
+ * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
+ * - creating: (bool) For internal use only. Never specify this.
+ * @return Status
+ */
+ public function canCreateAccount( $username, $options = [] ) {
+ // Back compat
+ if ( is_int( $options ) ) {
+ $options = [ 'flags' => $options ];
+ }
+ $options += [
+ 'flags' => User::READ_NORMAL,
+ 'creating' => false,
+ ];
+ $flags = $options['flags'];
+
+ if ( !$this->canCreateAccounts() ) {
+ return Status::newFatal( 'authmanager-create-disabled' );
+ }
+
+ if ( $this->userExists( $username, $flags ) ) {
+ return Status::newFatal( 'userexists' );
+ }
+
+ $user = User::newFromName( $username, 'creatable' );
+ if ( !is_object( $user ) ) {
+ return Status::newFatal( 'noname' );
+ } else {
+ $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
+ if ( $user->getId() !== 0 ) {
+ return Status::newFatal( 'userexists' );
+ }
+ }
+
+ // Denied by providers?
+ $providers = $this->getPreAuthenticationProviders() +
+ $this->getPrimaryAuthenticationProviders() +
+ $this->getSecondaryAuthenticationProviders();
+ foreach ( $providers as $provider ) {
+ $status = $provider->testUserForCreation( $user, false, $options );
+ if ( !$status->isGood() ) {
+ return Status::wrap( $status );
+ }
+ }
+
+ return Status::newGood();
+ }
+
+ /**
+ * Basic permissions checks on whether a user can create accounts
+ * @param User $creator User doing the account creation
+ * @return Status
+ */
+ public function checkAccountCreatePermissions( User $creator ) {
+ // Wiki is read-only?
+ if ( wfReadOnly() ) {
+ return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
+ }
+
+ // This is awful, this permission check really shouldn't go through Title.
+ $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
+ ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
+ if ( $permErrors ) {
+ $status = Status::newGood();
+ foreach ( $permErrors as $args ) {
+ call_user_func_array( [ $status, 'fatal' ], $args );
+ }
+ return $status;
+ }
+
+ $block = $creator->isBlockedFromCreateAccount();
+ if ( $block ) {
+ $errorParams = [
+ $block->getTarget(),
+ $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
+ $block->getByName()
+ ];
+
+ if ( $block->getType() === \Block::TYPE_RANGE ) {
+ $errorMessage = 'cantcreateaccount-range-text';
+ $errorParams[] = $this->getRequest()->getIP();
+ } else {
+ $errorMessage = 'cantcreateaccount-text';
+ }
+
+ return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
+ }
+
+ $ip = $this->getRequest()->getIP();
+ if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
+ return Status::newFatal( 'sorbs_create_account_reason' );
+ }
+
+ return Status::newGood();
+ }
+
+ /**
+ * Start an account creation flow
+ *
+ * In addition to the AuthenticationRequests returned by
+ * $this->getAuthenticationRequests(), a client might include a
+ * CreateFromLoginAuthenticationRequest from a previous login attempt. If
+ * <code>
+ * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
+ * </code>
+ * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
+ * should be omitted. If the CreateFromLoginAuthenticationRequest has a
+ * username set, that username must be used for all other requests.
+ *
+ * @param User $creator User doing the account creation
+ * @param AuthenticationRequest[] $reqs
+ * @param string $returnToUrl Url that REDIRECT responses should eventually
+ * return to.
+ * @return AuthenticationResponse
+ */
+ public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
+ $session = $this->request->getSession();
+ if ( !$this->canCreateAccounts() ) {
+ // Caller should have called canCreateAccounts()
+ $session->remove( 'AuthManager::accountCreationState' );
+ throw new \LogicException( 'Account creation is not possible' );
+ }
+
+ try {
+ $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
+ } catch ( \UnexpectedValueException $ex ) {
+ $username = null;
+ }
+ if ( $username === null ) {
+ $this->logger->debug( __METHOD__ . ': No username provided' );
+ return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
+ }
+
+ // Permissions check
+ $status = $this->checkAccountCreatePermissions( $creator );
+ if ( !$status->isGood() ) {
+ $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
+ 'user' => $username,
+ 'creator' => $creator->getName(),
+ 'reason' => $status->getWikiText( null, null, 'en' )
+ ] );
+ return AuthenticationResponse::newFail( $status->getMessage() );
+ }
+
+ $status = $this->canCreateAccount(
+ $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
+ );
+ if ( !$status->isGood() ) {
+ $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
+ 'user' => $username,
+ 'creator' => $creator->getName(),
+ 'reason' => $status->getWikiText( null, null, 'en' )
+ ] );
+ return AuthenticationResponse::newFail( $status->getMessage() );
+ }
+
+ $user = User::newFromName( $username, 'creatable' );
+ foreach ( $reqs as $req ) {
+ $req->username = $username;
+ $req->returnToUrl = $returnToUrl;
+ if ( $req instanceof UserDataAuthenticationRequest ) {
+ $status = $req->populateUser( $user );
+ if ( !$status->isGood() ) {
+ $status = Status::wrap( $status );
+ $session->remove( 'AuthManager::accountCreationState' );
+ $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ 'reason' => $status->getWikiText( null, null, 'en' ),
+ ] );
+ return AuthenticationResponse::newFail( $status->getMessage() );
+ }
+ }
+ }
+
+ $this->removeAuthenticationSessionData( null );
+
+ $state = [
+ 'username' => $username,
+ 'userid' => 0,
+ 'creatorid' => $creator->getId(),
+ 'creatorname' => $creator->getName(),
+ 'reqs' => $reqs,
+ 'returnToUrl' => $returnToUrl,
+ 'primary' => null,
+ 'primaryResponse' => null,
+ 'secondary' => [],
+ 'continueRequests' => [],
+ 'maybeLink' => [],
+ 'ranPreTests' => false,
+ ];
+
+ // Special case: converting a login to an account creation
+ $req = AuthenticationRequest::getRequestByClass(
+ $reqs, CreateFromLoginAuthenticationRequest::class
+ );
+ if ( $req ) {
+ $state['maybeLink'] = $req->maybeLink;
+
+ if ( $req->createRequest ) {
+ $reqs[] = $req->createRequest;
+ $state['reqs'][] = $req->createRequest;
+ }
+ }
+
+ $session->setSecret( 'AuthManager::accountCreationState', $state );
+ $session->persist();
+
+ return $this->continueAccountCreation( $reqs );
+ }
+
+ /**
+ * Continue an account creation flow
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse
+ */
+ public function continueAccountCreation( array $reqs ) {
+ $session = $this->request->getSession();
+ try {
+ if ( !$this->canCreateAccounts() ) {
+ // Caller should have called canCreateAccounts()
+ $session->remove( 'AuthManager::accountCreationState' );
+ throw new \LogicException( 'Account creation is not possible' );
+ }
+
+ $state = $session->getSecret( 'AuthManager::accountCreationState' );
+ if ( !is_array( $state ) ) {
+ return AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-create-not-in-progress' )
+ );
+ }
+ $state['continueRequests'] = [];
+
+ // Step 0: Prepare and validate the input
+
+ $user = User::newFromName( $state['username'], 'creatable' );
+ if ( !is_object( $user ) ) {
+ $session->remove( 'AuthManager::accountCreationState' );
+ $this->logger->debug( __METHOD__ . ': Invalid username', [
+ 'user' => $state['username'],
+ ] );
+ return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
+ }
+
+ if ( $state['creatorid'] ) {
+ $creator = User::newFromId( $state['creatorid'] );
+ } else {
+ $creator = new User;
+ $creator->setName( $state['creatorname'] );
+ }
+
+ // Avoid account creation races on double submissions
+ $cache = \ObjectCache::getLocalClusterInstance();
+ $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
+ if ( !$lock ) {
+ // Don't clear AuthManager::accountCreationState for this code
+ // path because the process that won the race owns it.
+ $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
+ }
+
+ // Permissions check
+ $status = $this->checkAccountCreatePermissions( $creator );
+ if ( !$status->isGood() ) {
+ $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ 'reason' => $status->getWikiText( null, null, 'en' )
+ ] );
+ $ret = AuthenticationResponse::newFail( $status->getMessage() );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $ret;
+ }
+
+ // Load from master for existence check
+ $user->load( User::READ_LOCKING );
+
+ if ( $state['userid'] === 0 ) {
+ if ( $user->getId() != 0 ) {
+ $this->logger->debug( __METHOD__ . ': User exists locally', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $ret;
+ }
+ } else {
+ if ( $user->getId() == 0 ) {
+ $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ 'expected_id' => $state['userid'],
+ ] );
+ throw new \UnexpectedValueException(
+ "User \"{$state['username']}\" should exist now, but doesn't!"
+ );
+ }
+ if ( $user->getId() != $state['userid'] ) {
+ $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ 'expected_id' => $state['userid'],
+ 'actual_id' => $user->getId(),
+ ] );
+ throw new \UnexpectedValueException(
+ "User \"{$state['username']}\" exists, but " .
+ "ID {$user->getId()} != {$state['userid']}!"
+ );
+ }
+ }
+ foreach ( $state['reqs'] as $req ) {
+ if ( $req instanceof UserDataAuthenticationRequest ) {
+ $status = $req->populateUser( $user );
+ if ( !$status->isGood() ) {
+ // This should never happen...
+ $status = Status::wrap( $status );
+ $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ 'reason' => $status->getWikiText( null, null, 'en' ),
+ ] );
+ $ret = AuthenticationResponse::newFail( $status->getMessage() );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $ret;
+ }
+ }
+ }
+
+ foreach ( $reqs as $req ) {
+ $req->returnToUrl = $state['returnToUrl'];
+ $req->username = $state['username'];
+ }
+
+ // Run pre-creation tests, if we haven't already
+ if ( !$state['ranPreTests'] ) {
+ $providers = $this->getPreAuthenticationProviders() +
+ $this->getPrimaryAuthenticationProviders() +
+ $this->getSecondaryAuthenticationProviders();
+ foreach ( $providers as $id => $provider ) {
+ $status = $provider->testForAccountCreation( $user, $creator, $reqs );
+ if ( !$status->isGood() ) {
+ $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $ret = AuthenticationResponse::newFail(
+ Status::wrap( $status )->getMessage()
+ );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $ret;
+ }
+ }
+
+ $state['ranPreTests'] = true;
+ }
+
+ // Step 1: Choose a primary authentication provider and call it until it succeeds.
+
+ if ( $state['primary'] === null ) {
+ // We haven't picked a PrimaryAuthenticationProvider yet
+ foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
+ if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
+ continue;
+ }
+ $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS;
+ $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $state['primary'] = $id;
+ $state['primaryResponse'] = $res;
+ break 2;
+ case AuthenticationResponse::FAIL;
+ $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $res;
+ case AuthenticationResponse::ABSTAIN;
+ // Continue loop
+ break;
+ case AuthenticationResponse::REDIRECT;
+ case AuthenticationResponse::UI;
+ $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
+ $state['primary'] = $id;
+ $state['continueRequests'] = $res->neededRequests;
+ $session->setSecret( 'AuthManager::accountCreationState', $state );
+ return $res;
+
+ // @codeCoverageIgnoreStart
+ default:
+ throw new \DomainException(
+ get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
+ );
+ // @codeCoverageIgnoreEnd
+ }
+ }
+ if ( $state['primary'] === null ) {
+ $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $ret = AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-create-no-primary' )
+ );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $ret;
+ }
+ } elseif ( $state['primaryResponse'] === null ) {
+ $provider = $this->getAuthenticationProvider( $state['primary'] );
+ if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+ // Configuration changed? Force them to start over.
+ // @codeCoverageIgnoreStart
+ $ret = AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-create-not-in-progress' )
+ );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $ret;
+ // @codeCoverageIgnoreEnd
+ }
+ $id = $provider->getUniqueId();
+ $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS;
+ $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $state['primaryResponse'] = $res;
+ break;
+ case AuthenticationResponse::FAIL;
+ $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $res;
+ case AuthenticationResponse::REDIRECT;
+ case AuthenticationResponse::UI;
+ $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
+ $state['continueRequests'] = $res->neededRequests;
+ $session->setSecret( 'AuthManager::accountCreationState', $state );
+ return $res;
+ default:
+ throw new \DomainException(
+ get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
+ );
+ }
+ }
+
+ // Step 2: Primary authentication succeeded, create the User object
+ // and add the user locally.
+
+ if ( $state['userid'] === 0 ) {
+ $this->logger->info( 'Creating user {user} during account creation', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $status = $user->addToDatabase();
+ if ( !$status->isOK() ) {
+ // @codeCoverageIgnoreStart
+ $ret = AuthenticationResponse::newFail( $status->getMessage() );
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ return $ret;
+ // @codeCoverageIgnoreEnd
+ }
+ $this->setDefaultUserOptions( $user, $creator->isAnon() );
+ \Hooks::run( 'LocalUserCreated', [ $user, false ] );
+ $user->saveSettings();
+ $state['userid'] = $user->getId();
+
+ // Update user count
+ \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
+
+ // Watch user's userpage and talk page
+ $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
+
+ // Inform the provider
+ $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
+
+ // Log the creation
+ if ( $this->config->get( 'NewUserLog' ) ) {
+ $isAnon = $creator->isAnon();
+ $logEntry = new \ManualLogEntry(
+ 'newusers',
+ $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
+ );
+ $logEntry->setPerformer( $isAnon ? $user : $creator );
+ $logEntry->setTarget( $user->getUserPage() );
+ /** @var CreationReasonAuthenticationRequest $req */
+ $req = AuthenticationRequest::getRequestByClass(
+ $state['reqs'], CreationReasonAuthenticationRequest::class
+ );
+ $logEntry->setComment( $req ? $req->reason : '' );
+ $logEntry->setParameters( [
+ '4::userid' => $user->getId(),
+ ] );
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
+ }
+ }
+
+ // Step 3: Iterate over all the secondary authentication providers.
+
+ $beginReqs = $state['reqs'];
+
+ foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
+ if ( !isset( $state['secondary'][$id] ) ) {
+ // This provider isn't started yet, so we pass it the set
+ // of reqs from beginAuthentication instead of whatever
+ // might have been used by a previous provider in line.
+ $func = 'beginSecondaryAccountCreation';
+ $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
+ } elseif ( !$state['secondary'][$id] ) {
+ $func = 'continueSecondaryAccountCreation';
+ $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
+ } else {
+ continue;
+ }
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS;
+ $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ // fall through
+ case AuthenticationResponse::ABSTAIN;
+ $state['secondary'][$id] = true;
+ break;
+ case AuthenticationResponse::REDIRECT;
+ case AuthenticationResponse::UI;
+ $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+ $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
+ $state['secondary'][$id] = false;
+ $state['continueRequests'] = $res->neededRequests;
+ $session->setSecret( 'AuthManager::accountCreationState', $state );
+ return $res;
+ case AuthenticationResponse::FAIL;
+ throw new \DomainException(
+ get_class( $provider ) . "::{$func}() returned $res->status." .
+ ' Secondary providers are not allowed to fail account creation, that' .
+ ' should have been done via testForAccountCreation().'
+ );
+ // @codeCoverageIgnoreStart
+ default:
+ throw new \DomainException(
+ get_class( $provider ) . "::{$func}() returned $res->status"
+ );
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
+ $id = $user->getId();
+ $name = $user->getName();
+ $req = new CreatedAccountAuthenticationRequest( $id, $name );
+ $ret = AuthenticationResponse::newPass( $name );
+ $ret->loginRequest = $req;
+ $this->createdAccountAuthenticationRequests[] = $req;
+
+ $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
+ 'user' => $user->getName(),
+ 'creator' => $creator->getName(),
+ ] );
+
+ $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
+ $session->remove( 'AuthManager::accountCreationState' );
+ $this->removeAuthenticationSessionData( null );
+ return $ret;
+ } catch ( \Exception $ex ) {
+ $session->remove( 'AuthManager::accountCreationState' );
+ throw $ex;
+ }
+ }
+
+ /**
+ * Auto-create an account, and log into that account
+ *
+ * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
+ * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
+ * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
+ * the username of a non-existing user from provideSessionInfo(). Calling this method
+ * explicitly (e.g. from a maintenance script) is also fine.
+ *
+ * @param User $user User to auto-create
+ * @param string $source What caused the auto-creation? This must be the ID
+ * of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
+ * @param bool $login Whether to also log the user in
+ * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
+ */
+ public function autoCreateUser( User $user, $source, $login = true ) {
+ if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
+ !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
+ ) {
+ throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
+ }
+
+ $username = $user->getName();
+
+ // Try the local user from the replica DB
+ $localId = User::idFromName( $username );
+ $flags = User::READ_NORMAL;
+
+ // Fetch the user ID from the master, so that we don't try to create the user
+ // when they already exist, due to replication lag
+ // @codeCoverageIgnoreStart
+ if (
+ !$localId &&
+ MediaWikiServices::getInstance()->getDBLoadBalancer()->getReaderIndex() != 0
+ ) {
+ $localId = User::idFromName( $username, User::READ_LATEST );
+ $flags = User::READ_LATEST;
+ }
+ // @codeCoverageIgnoreEnd
+
+ if ( $localId ) {
+ $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
+ 'username' => $username,
+ ] );
+ $user->setId( $localId );
+ $user->loadFromId( $flags );
+ if ( $login ) {
+ $this->setSessionDataForUser( $user );
+ }
+ $status = Status::newGood();
+ $status->warning( 'userexists' );
+ return $status;
+ }
+
+ // Wiki is read-only?
+ if ( wfReadOnly() ) {
+ $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
+ 'username' => $username,
+ 'reason' => wfReadOnlyReason(),
+ ] );
+ $user->setId( 0 );
+ $user->loadFromId();
+ return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
+ }
+
+ // Check the session, if we tried to create this user already there's
+ // no point in retrying.
+ $session = $this->request->getSession();
+ if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
+ $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
+ 'username' => $username,
+ 'sessionid' => $session->getId(),
+ ] );
+ $user->setId( 0 );
+ $user->loadFromId();
+ $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
+ if ( $reason instanceof StatusValue ) {
+ return Status::wrap( $reason );
+ } else {
+ return Status::newFatal( $reason );
+ }
+ }
+
+ // Is the username creatable?
+ if ( !User::isCreatableName( $username ) ) {
+ $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
+ 'username' => $username,
+ ] );
+ $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
+ $user->setId( 0 );
+ $user->loadFromId();
+ return Status::newFatal( 'noname' );
+ }
+
+ // Is the IP user able to create accounts?
+ $anon = new User;
+ if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
+ $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
+ 'username' => $username,
+ 'ip' => $anon->getName(),
+ ] );
+ $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
+ $session->persist();
+ $user->setId( 0 );
+ $user->loadFromId();
+ return Status::newFatal( 'authmanager-autocreate-noperm' );
+ }
+
+ // Avoid account creation races on double submissions
+ $cache = \ObjectCache::getLocalClusterInstance();
+ $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
+ if ( !$lock ) {
+ $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
+ 'user' => $username,
+ ] );
+ $user->setId( 0 );
+ $user->loadFromId();
+ return Status::newFatal( 'usernameinprogress' );
+ }
+
+ // Denied by providers?
+ $options = [
+ 'flags' => User::READ_LATEST,
+ 'creating' => true,
+ ];
+ $providers = $this->getPreAuthenticationProviders() +
+ $this->getPrimaryAuthenticationProviders() +
+ $this->getSecondaryAuthenticationProviders();
+ foreach ( $providers as $provider ) {
+ $status = $provider->testUserForCreation( $user, $source, $options );
+ if ( !$status->isGood() ) {
+ $ret = Status::wrap( $status );
+ $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
+ 'username' => $username,
+ 'reason' => $ret->getWikiText( null, null, 'en' ),
+ ] );
+ $session->set( 'AuthManager::AutoCreateBlacklist', $status );
+ $user->setId( 0 );
+ $user->loadFromId();
+ return $ret;
+ }
+ }
+
+ $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
+ if ( $cache->get( $backoffKey ) ) {
+ $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
+ 'username' => $username,
+ ] );
+ $user->setId( 0 );
+ $user->loadFromId();
+ return Status::newFatal( 'authmanager-autocreate-exception' );
+ }
+
+ // Checks passed, create the user...
+ $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
+ $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
+ 'username' => $username,
+ 'from' => $from,
+ ] );
+
+ // Ignore warnings about master connections/writes...hard to avoid here
+ $trxProfiler = \Profiler::instance()->getTransactionProfiler();
+ $old = $trxProfiler->setSilenced( true );
+ try {
+ $status = $user->addToDatabase();
+ if ( !$status->isOK() ) {
+ // Double-check for a race condition (T70012). We make use of the fact that when
+ // addToDatabase fails due to the user already existing, the user object gets loaded.
+ if ( $user->getId() ) {
+ $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
+ 'username' => $username,
+ ] );
+ if ( $login ) {
+ $this->setSessionDataForUser( $user );
+ }
+ $status = Status::newGood();
+ $status->warning( 'userexists' );
+ } else {
+ $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
+ 'username' => $username,
+ 'msg' => $status->getWikiText( null, null, 'en' )
+ ] );
+ $user->setId( 0 );
+ $user->loadFromId();
+ }
+ return $status;
+ }
+ } catch ( \Exception $ex ) {
+ $trxProfiler->setSilenced( $old );
+ $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
+ 'username' => $username,
+ 'exception' => $ex,
+ ] );
+ // Do not keep throwing errors for a while
+ $cache->set( $backoffKey, 1, 600 );
+ // Bubble up error; which should normally trigger DB rollbacks
+ throw $ex;
+ }
+
+ $this->setDefaultUserOptions( $user, false );
+
+ // Inform the providers
+ $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
+
+ \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
+ \Hooks::run( 'LocalUserCreated', [ $user, true ] );
+ $user->saveSettings();
+
+ // Update user count
+ \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
+ // Watch user's userpage and talk page
+ \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
+ $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
+ } );
+
+ // Log the creation
+ if ( $this->config->get( 'NewUserLog' ) ) {
+ $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $user->getUserPage() );
+ $logEntry->setComment( '' );
+ $logEntry->setParameters( [
+ '4::userid' => $user->getId(),
+ ] );
+ $logEntry->insert();
+ }
+
+ $trxProfiler->setSilenced( $old );
+
+ if ( $login ) {
+ $this->setSessionDataForUser( $user );
+ }
+
+ return Status::newGood();
+ }
+
+ /**@}*/
+
+ /**
+ * @name Account linking
+ * @{
+ */
+
+ /**
+ * Determine whether accounts can be linked
+ * @return bool
+ */
+ public function canLinkAccounts() {
+ foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+ if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Start an account linking flow
+ *
+ * @param User $user User being linked
+ * @param AuthenticationRequest[] $reqs
+ * @param string $returnToUrl Url that REDIRECT responses should eventually
+ * return to.
+ * @return AuthenticationResponse
+ */
+ public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
+ $session = $this->request->getSession();
+ $session->remove( 'AuthManager::accountLinkState' );
+
+ if ( !$this->canLinkAccounts() ) {
+ // Caller should have called canLinkAccounts()
+ throw new \LogicException( 'Account linking is not possible' );
+ }
+
+ if ( $user->getId() === 0 ) {
+ if ( !User::isUsableName( $user->getName() ) ) {
+ $msg = wfMessage( 'noname' );
+ } else {
+ $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
+ }
+ return AuthenticationResponse::newFail( $msg );
+ }
+ foreach ( $reqs as $req ) {
+ $req->username = $user->getName();
+ $req->returnToUrl = $returnToUrl;
+ }
+
+ $this->removeAuthenticationSessionData( null );
+
+ $providers = $this->getPreAuthenticationProviders();
+ foreach ( $providers as $id => $provider ) {
+ $status = $provider->testForAccountLink( $user );
+ if ( !$status->isGood() ) {
+ $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
+ 'user' => $user->getName(),
+ ] );
+ $ret = AuthenticationResponse::newFail(
+ Status::wrap( $status )->getMessage()
+ );
+ $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
+ return $ret;
+ }
+ }
+
+ $state = [
+ 'username' => $user->getName(),
+ 'userid' => $user->getId(),
+ 'returnToUrl' => $returnToUrl,
+ 'primary' => null,
+ 'continueRequests' => [],
+ ];
+
+ $providers = $this->getPrimaryAuthenticationProviders();
+ foreach ( $providers as $id => $provider ) {
+ if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
+ continue;
+ }
+
+ $res = $provider->beginPrimaryAccountLink( $user, $reqs );
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS;
+ $this->logger->info( "Account linked to {user} by $id", [
+ 'user' => $user->getName(),
+ ] );
+ $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
+ return $res;
+
+ case AuthenticationResponse::FAIL;
+ $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
+ 'user' => $user->getName(),
+ ] );
+ $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
+ return $res;
+
+ case AuthenticationResponse::ABSTAIN;
+ // Continue loop
+ break;
+
+ case AuthenticationResponse::REDIRECT;
+ case AuthenticationResponse::UI;
+ $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
+ 'user' => $user->getName(),
+ ] );
+ $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
+ $state['primary'] = $id;
+ $state['continueRequests'] = $res->neededRequests;
+ $session->setSecret( 'AuthManager::accountLinkState', $state );
+ $session->persist();
+ return $res;
+
+ // @codeCoverageIgnoreStart
+ default:
+ throw new \DomainException(
+ get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
+ );
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
+ $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
+ 'user' => $user->getName(),
+ ] );
+ $ret = AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-link-no-primary' )
+ );
+ $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
+ return $ret;
+ }
+
+ /**
+ * Continue an account linking flow
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse
+ */
+ public function continueAccountLink( array $reqs ) {
+ $session = $this->request->getSession();
+ try {
+ if ( !$this->canLinkAccounts() ) {
+ // Caller should have called canLinkAccounts()
+ $session->remove( 'AuthManager::accountLinkState' );
+ throw new \LogicException( 'Account linking is not possible' );
+ }
+
+ $state = $session->getSecret( 'AuthManager::accountLinkState' );
+ if ( !is_array( $state ) ) {
+ return AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-link-not-in-progress' )
+ );
+ }
+ $state['continueRequests'] = [];
+
+ // Step 0: Prepare and validate the input
+
+ $user = User::newFromName( $state['username'], 'usable' );
+ if ( !is_object( $user ) ) {
+ $session->remove( 'AuthManager::accountLinkState' );
+ return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
+ }
+ if ( $user->getId() != $state['userid'] ) {
+ throw new \UnexpectedValueException(
+ "User \"{$state['username']}\" is valid, but " .
+ "ID {$user->getId()} != {$state['userid']}!"
+ );
+ }
+
+ foreach ( $reqs as $req ) {
+ $req->username = $state['username'];
+ $req->returnToUrl = $state['returnToUrl'];
+ }
+
+ // Step 1: Call the primary again until it succeeds
+
+ $provider = $this->getAuthenticationProvider( $state['primary'] );
+ if ( !$provider instanceof PrimaryAuthenticationProvider ) {
+ // Configuration changed? Force them to start over.
+ // @codeCoverageIgnoreStart
+ $ret = AuthenticationResponse::newFail(
+ wfMessage( 'authmanager-link-not-in-progress' )
+ );
+ $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
+ $session->remove( 'AuthManager::accountLinkState' );
+ return $ret;
+ // @codeCoverageIgnoreEnd
+ }
+ $id = $provider->getUniqueId();
+ $res = $provider->continuePrimaryAccountLink( $user, $reqs );
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS;
+ $this->logger->info( "Account linked to {user} by $id", [
+ 'user' => $user->getName(),
+ ] );
+ $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
+ $session->remove( 'AuthManager::accountLinkState' );
+ return $res;
+ case AuthenticationResponse::FAIL;
+ $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
+ 'user' => $user->getName(),
+ ] );
+ $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
+ $session->remove( 'AuthManager::accountLinkState' );
+ return $res;
+ case AuthenticationResponse::REDIRECT;
+ case AuthenticationResponse::UI;
+ $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
+ 'user' => $user->getName(),
+ ] );
+ $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
+ $state['continueRequests'] = $res->neededRequests;
+ $session->setSecret( 'AuthManager::accountLinkState', $state );
+ return $res;
+ default:
+ throw new \DomainException(
+ get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
+ );
+ }
+ } catch ( \Exception $ex ) {
+ $session->remove( 'AuthManager::accountLinkState' );
+ throw $ex;
+ }
+ }
+
+ /**@}*/
+
+ /**
+ * @name Information methods
+ * @{
+ */
+
+ /**
+ * Return the applicable list of AuthenticationRequests
+ *
+ * Possible values for $action:
+ * - ACTION_LOGIN: Valid for passing to beginAuthentication
+ * - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
+ * - ACTION_CREATE: Valid for passing to beginAccountCreation
+ * - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
+ * - ACTION_LINK: Valid for passing to beginAccountLink
+ * - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
+ * - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
+ * - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
+ * - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
+ *
+ * @param string $action One of the AuthManager::ACTION_* constants
+ * @param User|null $user User being acted on, instead of the current user.
+ * @return AuthenticationRequest[]
+ */
+ public function getAuthenticationRequests( $action, User $user = null ) {
+ $options = [];
+ $providerAction = $action;
+
+ // Figure out which providers to query
+ switch ( $action ) {
+ case self::ACTION_LOGIN:
+ case self::ACTION_CREATE:
+ $providers = $this->getPreAuthenticationProviders() +
+ $this->getPrimaryAuthenticationProviders() +
+ $this->getSecondaryAuthenticationProviders();
+ break;
+
+ case self::ACTION_LOGIN_CONTINUE:
+ $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
+ return is_array( $state ) ? $state['continueRequests'] : [];
+
+ case self::ACTION_CREATE_CONTINUE:
+ $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
+ return is_array( $state ) ? $state['continueRequests'] : [];
+
+ case self::ACTION_LINK:
+ $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
+ return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
+ } );
+ break;
+
+ case self::ACTION_UNLINK:
+ $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
+ return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
+ } );
+
+ // To providers, unlink and remove are identical.
+ $providerAction = self::ACTION_REMOVE;
+ break;
+
+ case self::ACTION_LINK_CONTINUE:
+ $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
+ return is_array( $state ) ? $state['continueRequests'] : [];
+
+ case self::ACTION_CHANGE:
+ case self::ACTION_REMOVE:
+ $providers = $this->getPrimaryAuthenticationProviders() +
+ $this->getSecondaryAuthenticationProviders();
+ break;
+
+ // @codeCoverageIgnoreStart
+ default:
+ throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
+ }
+ // @codeCoverageIgnoreEnd
+
+ return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
+ }
+
+ /**
+ * Internal request lookup for self::getAuthenticationRequests
+ *
+ * @param string $providerAction Action to pass to providers
+ * @param array $options Options to pass to providers
+ * @param AuthenticationProvider[] $providers
+ * @param User|null $user
+ * @return AuthenticationRequest[]
+ */
+ private function getAuthenticationRequestsInternal(
+ $providerAction, array $options, array $providers, User $user = null
+ ) {
+ $user = $user ?: \RequestContext::getMain()->getUser();
+ $options['username'] = $user->isAnon() ? null : $user->getName();
+
+ // Query them and merge results
+ $reqs = [];
+ foreach ( $providers as $provider ) {
+ $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
+ foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
+ $id = $req->getUniqueId();
+
+ // If a required request if from a Primary, mark it as "primary-required" instead
+ if ( $isPrimary ) {
+ if ( $req->required ) {
+ $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
+ }
+ }
+
+ if (
+ !isset( $reqs[$id] )
+ || $req->required === AuthenticationRequest::REQUIRED
+ || $reqs[$id] === AuthenticationRequest::OPTIONAL
+ ) {
+ $reqs[$id] = $req;
+ }
+ }
+ }
+
+ // AuthManager has its own req for some actions
+ switch ( $providerAction ) {
+ case self::ACTION_LOGIN:
+ $reqs[] = new RememberMeAuthenticationRequest;
+ break;
+
+ case self::ACTION_CREATE:
+ $reqs[] = new UsernameAuthenticationRequest;
+ $reqs[] = new UserDataAuthenticationRequest;
+ if ( $options['username'] !== null ) {
+ $reqs[] = new CreationReasonAuthenticationRequest;
+ $options['username'] = null; // Don't fill in the username below
+ }
+ break;
+ }
+
+ // Fill in reqs data
+ $this->fillRequests( $reqs, $providerAction, $options['username'], true );
+
+ // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
+ if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
+ $reqs = array_filter( $reqs, function ( $req ) {
+ return $this->allowsAuthenticationDataChange( $req, false )->isGood();
+ } );
+ }
+
+ return array_values( $reqs );
+ }
+
+ /**
+ * Set values in an array of requests
+ * @param AuthenticationRequest[] &$reqs
+ * @param string $action
+ * @param string|null $username
+ * @param bool $forceAction
+ */
+ private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
+ foreach ( $reqs as $req ) {
+ if ( !$req->action || $forceAction ) {
+ $req->action = $action;
+ }
+ if ( $req->username === null ) {
+ $req->username = $username;
+ }
+ }
+ }
+
+ /**
+ * Determine whether a username exists
+ * @param string $username
+ * @param int $flags Bitfield of User:READ_* constants
+ * @return bool
+ */
+ public function userExists( $username, $flags = User::READ_NORMAL ) {
+ foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
+ if ( $provider->testUserExists( $username, $flags ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine whether a user property should be allowed to be changed.
+ *
+ * Supported properties are:
+ * - emailaddress
+ * - realname
+ * - nickname
+ *
+ * @param string $property
+ * @return bool
+ */
+ public function allowsPropertyChange( $property ) {
+ $providers = $this->getPrimaryAuthenticationProviders() +
+ $this->getSecondaryAuthenticationProviders();
+ foreach ( $providers as $provider ) {
+ if ( !$provider->providerAllowsPropertyChange( $property ) ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Get a provider by ID
+ * @note This is public so extensions can check whether their own provider
+ * is installed and so they can read its configuration if necessary.
+ * Other uses are not recommended.
+ * @param string $id
+ * @return AuthenticationProvider|null
+ */
+ public function getAuthenticationProvider( $id ) {
+ // Fast version
+ if ( isset( $this->allAuthenticationProviders[$id] ) ) {
+ return $this->allAuthenticationProviders[$id];
+ }
+
+ // Slow version: instantiate each kind and check
+ $providers = $this->getPrimaryAuthenticationProviders();
+ if ( isset( $providers[$id] ) ) {
+ return $providers[$id];
+ }
+ $providers = $this->getSecondaryAuthenticationProviders();
+ if ( isset( $providers[$id] ) ) {
+ return $providers[$id];
+ }
+ $providers = $this->getPreAuthenticationProviders();
+ if ( isset( $providers[$id] ) ) {
+ return $providers[$id];
+ }
+
+ return null;
+ }
+
+ /**@}*/
+
+ /**
+ * @name Internal methods
+ * @{
+ */
+
+ /**
+ * Store authentication in the current session
+ * @protected For use by AuthenticationProviders
+ * @param string $key
+ * @param mixed $data Must be serializable
+ */
+ public function setAuthenticationSessionData( $key, $data ) {
+ $session = $this->request->getSession();
+ $arr = $session->getSecret( 'authData' );
+ if ( !is_array( $arr ) ) {
+ $arr = [];
+ }
+ $arr[$key] = $data;
+ $session->setSecret( 'authData', $arr );
+ }
+
+ /**
+ * Fetch authentication data from the current session
+ * @protected For use by AuthenticationProviders
+ * @param string $key
+ * @param mixed $default
+ * @return mixed
+ */
+ public function getAuthenticationSessionData( $key, $default = null ) {
+ $arr = $this->request->getSession()->getSecret( 'authData' );
+ if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
+ return $arr[$key];
+ } else {
+ return $default;
+ }
+ }
+
+ /**
+ * Remove authentication data
+ * @protected For use by AuthenticationProviders
+ * @param string|null $key If null, all data is removed
+ */
+ public function removeAuthenticationSessionData( $key ) {
+ $session = $this->request->getSession();
+ if ( $key === null ) {
+ $session->remove( 'authData' );
+ } else {
+ $arr = $session->getSecret( 'authData' );
+ if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
+ unset( $arr[$key] );
+ $session->setSecret( 'authData', $arr );
+ }
+ }
+ }
+
+ /**
+ * Create an array of AuthenticationProviders from an array of ObjectFactory specs
+ * @param string $class
+ * @param array[] $specs
+ * @return AuthenticationProvider[]
+ */
+ protected function providerArrayFromSpecs( $class, array $specs ) {
+ $i = 0;
+ foreach ( $specs as &$spec ) {
+ $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
+ }
+ unset( $spec );
+ usort( $specs, function ( $a, $b ) {
+ return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
+ ?: $a['sort2'] - $b['sort2'];
+ } );
+
+ $ret = [];
+ foreach ( $specs as $spec ) {
+ $provider = ObjectFactory::getObjectFromSpec( $spec );
+ if ( !$provider instanceof $class ) {
+ throw new \RuntimeException(
+ "Expected instance of $class, got " . get_class( $provider )
+ );
+ }
+ $provider->setLogger( $this->logger );
+ $provider->setManager( $this );
+ $provider->setConfig( $this->config );
+ $id = $provider->getUniqueId();
+ if ( isset( $this->allAuthenticationProviders[$id] ) ) {
+ throw new \RuntimeException(
+ "Duplicate specifications for id $id (classes " .
+ get_class( $provider ) . ' and ' .
+ get_class( $this->allAuthenticationProviders[$id] ) . ')'
+ );
+ }
+ $this->allAuthenticationProviders[$id] = $provider;
+ $ret[$id] = $provider;
+ }
+ return $ret;
+ }
+
+ /**
+ * Get the configuration
+ * @return array
+ */
+ private function getConfiguration() {
+ return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
+ }
+
+ /**
+ * Get the list of PreAuthenticationProviders
+ * @return PreAuthenticationProvider[]
+ */
+ protected function getPreAuthenticationProviders() {
+ if ( $this->preAuthenticationProviders === null ) {
+ $conf = $this->getConfiguration();
+ $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
+ PreAuthenticationProvider::class, $conf['preauth']
+ );
+ }
+ return $this->preAuthenticationProviders;
+ }
+
+ /**
+ * Get the list of PrimaryAuthenticationProviders
+ * @return PrimaryAuthenticationProvider[]
+ */
+ protected function getPrimaryAuthenticationProviders() {
+ if ( $this->primaryAuthenticationProviders === null ) {
+ $conf = $this->getConfiguration();
+ $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
+ PrimaryAuthenticationProvider::class, $conf['primaryauth']
+ );
+ }
+ return $this->primaryAuthenticationProviders;
+ }
+
+ /**
+ * Get the list of SecondaryAuthenticationProviders
+ * @return SecondaryAuthenticationProvider[]
+ */
+ protected function getSecondaryAuthenticationProviders() {
+ if ( $this->secondaryAuthenticationProviders === null ) {
+ $conf = $this->getConfiguration();
+ $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
+ SecondaryAuthenticationProvider::class, $conf['secondaryauth']
+ );
+ }
+ return $this->secondaryAuthenticationProviders;
+ }
+
+ /**
+ * Log the user in
+ * @param User $user
+ * @param bool|null $remember
+ */
+ private function setSessionDataForUser( $user, $remember = null ) {
+ $session = $this->request->getSession();
+ $delay = $session->delaySave();
+
+ $session->resetId();
+ $session->resetAllTokens();
+ if ( $session->canSetUser() ) {
+ $session->setUser( $user );
+ }
+ if ( $remember !== null ) {
+ $session->setRememberUser( $remember );
+ }
+ $session->set( 'AuthManager:lastAuthId', $user->getId() );
+ $session->set( 'AuthManager:lastAuthTimestamp', time() );
+ $session->persist();
+
+ \Wikimedia\ScopedCallback::consume( $delay );
+
+ \Hooks::run( 'UserLoggedIn', [ $user ] );
+ }
+
+ /**
+ * @param User $user
+ * @param bool $useContextLang Use 'uselang' to set the user's language
+ */
+ private function setDefaultUserOptions( User $user, $useContextLang ) {
+ global $wgContLang;
+
+ $user->setToken();
+
+ $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
+ $user->setOption( 'language', $lang->getPreferredVariant() );
+
+ if ( $wgContLang->hasVariants() ) {
+ $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
+ }
+ }
+
+ /**
+ * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
+ * @param string $method
+ * @param array $args
+ */
+ private function callMethodOnProviders( $which, $method, array $args ) {
+ $providers = [];
+ if ( $which & 1 ) {
+ $providers += $this->getPreAuthenticationProviders();
+ }
+ if ( $which & 2 ) {
+ $providers += $this->getPrimaryAuthenticationProviders();
+ }
+ if ( $which & 4 ) {
+ $providers += $this->getSecondaryAuthenticationProviders();
+ }
+ foreach ( $providers as $provider ) {
+ call_user_func_array( [ $provider, $method ], $args );
+ }
+ }
+
+ /**
+ * Reset the internal caching for unit testing
+ * @protected Unit tests only
+ */
+ public static function resetCache() {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ // @codeCoverageIgnoreStart
+ throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
+ // @codeCoverageIgnoreEnd
+ }
+
+ self::$instance = null;
+ }
+
+ /**@}*/
+
+}
+
+/**
+ * For really cool vim folding this needs to be at the end:
+ * vim: foldmarker=@{,@} foldmethod=marker
+ */
diff --git a/www/wiki/includes/auth/AuthManagerAuthPlugin.php b/www/wiki/includes/auth/AuthManagerAuthPlugin.php
new file mode 100644
index 00000000..4f84b4c6
--- /dev/null
+++ b/www/wiki/includes/auth/AuthManagerAuthPlugin.php
@@ -0,0 +1,231 @@
+<?php
+/**
+ * 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
+ */
+
+namespace MediaWiki\Auth;
+
+use Psr\Log\LoggerInterface;
+use User;
+
+/**
+ * Backwards-compatibility wrapper for AuthManager via $wgAuth
+ * @since 1.27
+ * @deprecated since 1.27
+ */
+class AuthManagerAuthPlugin extends \AuthPlugin {
+ /** @var string|null */
+ protected $domain = null;
+
+ /** @var LoggerInterface */
+ protected $logger = null;
+
+ public function __construct() {
+ $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' );
+ }
+
+ public function userExists( $name ) {
+ return AuthManager::singleton()->userExists( $name );
+ }
+
+ public function authenticate( $username, $password ) {
+ $data = [
+ 'username' => $username,
+ 'password' => $password,
+ ];
+ if ( $this->domain !== null && $this->domain !== '' ) {
+ $data['domain'] = $this->domain;
+ }
+ $reqs = AuthManager::singleton()->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
+ $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+
+ $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
+ switch ( $res->status ) {
+ case AuthenticationResponse::PASS:
+ return true;
+ case AuthenticationResponse::FAIL:
+ // Hope it's not a PreAuthenticationProvider that failed...
+ $msg = $res->message instanceof \Message ? $res->message : new \Message( $res->message );
+ $this->logger->info( __METHOD__ . ': Authentication failed: ' . $msg->plain() );
+ return false;
+ default:
+ throw new \BadMethodCallException(
+ 'AuthManager does not support such simplified authentication'
+ );
+ }
+ }
+
+ public function modifyUITemplate( &$template, &$type ) {
+ // AuthManager does not support direct UI screwing-around-with
+ }
+
+ public function setDomain( $domain ) {
+ $this->domain = $domain;
+ }
+
+ public function getDomain() {
+ if ( isset( $this->domain ) ) {
+ return $this->domain;
+ } else {
+ return 'invaliddomain';
+ }
+ }
+
+ public function validDomain( $domain ) {
+ $domainList = $this->domainList();
+ return $domainList ? in_array( $domain, $domainList, true ) : $domain === '';
+ }
+
+ public function updateUser( &$user ) {
+ \Hooks::run( 'UserLoggedIn', [ $user ] );
+ return true;
+ }
+
+ public function autoCreate() {
+ return true;
+ }
+
+ public function allowPropChange( $prop = '' ) {
+ return AuthManager::singleton()->allowsPropertyChange( $prop );
+ }
+
+ public function allowPasswordChange() {
+ $reqs = AuthManager::singleton()->getAuthenticationRequests( AuthManager::ACTION_CHANGE );
+ foreach ( $reqs as $req ) {
+ if ( $req instanceof PasswordAuthenticationRequest ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function allowSetLocalPassword() {
+ // There should be a PrimaryAuthenticationProvider that does this, if necessary
+ return false;
+ }
+
+ public function setPassword( $user, $password ) {
+ $data = [
+ 'username' => $user->getName(),
+ 'password' => $password,
+ ];
+ if ( $this->domain !== null && $this->domain !== '' ) {
+ $data['domain'] = $this->domain;
+ }
+ $reqs = AuthManager::singleton()->getAuthenticationRequests( AuthManager::ACTION_CHANGE );
+ $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+ foreach ( $reqs as $req ) {
+ $status = AuthManager::singleton()->allowsAuthenticationDataChange( $req );
+ if ( !$status->isGood() ) {
+ $this->logger->info( __METHOD__ . ': Password change rejected: {reason}', [
+ 'username' => $data['username'],
+ 'reason' => $status->getWikiText( null, null, 'en' ),
+ ] );
+ return false;
+ }
+ }
+ foreach ( $reqs as $req ) {
+ AuthManager::singleton()->changeAuthenticationData( $req );
+ }
+ return true;
+ }
+
+ public function updateExternalDB( $user ) {
+ // This fires the necessary hook
+ $user->saveSettings();
+ return true;
+ }
+
+ public function updateExternalDBGroups( $user, $addgroups, $delgroups = [] ) {
+ throw new \BadMethodCallException(
+ 'Update of user groups via AuthPlugin is not supported with AuthManager.'
+ );
+ }
+
+ public function canCreateAccounts() {
+ return AuthManager::singleton()->canCreateAccounts();
+ }
+
+ public function addUser( $user, $password, $email = '', $realname = '' ) {
+ throw new \BadMethodCallException(
+ 'Creation of users via AuthPlugin is not supported with '
+ . 'AuthManager. Generally, user creation should be left to either '
+ . 'Special:CreateAccount, auto-creation when triggered by a '
+ . 'SessionProvider or PrimaryAuthenticationProvider, or '
+ . 'User::newSystemUser().'
+ );
+ }
+
+ public function strict() {
+ // There should be a PrimaryAuthenticationProvider that does this, if necessary
+ return true;
+ }
+
+ public function strictUserAuth( $username ) {
+ // There should be a PrimaryAuthenticationProvider that does this, if necessary
+ return true;
+ }
+
+ public function initUser( &$user, $autocreate = false ) {
+ \Hooks::run( 'LocalUserCreated', [ $user, $autocreate ] );
+ }
+
+ public function getCanonicalName( $username ) {
+ // AuthManager doesn't support restrictions beyond MediaWiki's
+ return $username;
+ }
+
+ public function getUserInstance( User &$user ) {
+ return new AuthManagerAuthPluginUser( $user );
+ }
+
+ public function domainList() {
+ return [];
+ }
+}
+
+/**
+ * @since 1.27
+ * @deprecated since 1.27
+ */
+class AuthManagerAuthPluginUser extends \AuthPluginUser {
+ /** @var User */
+ private $user;
+
+ function __construct( $user ) {
+ $this->user = $user;
+ }
+
+ public function getId() {
+ return $this->user->getId();
+ }
+
+ public function isLocked() {
+ return $this->user->isLocked();
+ }
+
+ public function isHidden() {
+ return $this->user->isHidden();
+ }
+
+ public function resetAuthToken() {
+ \MediaWiki\Session\SessionManager::singleton()->invalidateSessionsForUser( $this->user );
+ return true;
+ }
+}
diff --git a/www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php b/www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php
new file mode 100644
index 00000000..cd0734d8
--- /dev/null
+++ b/www/wiki/includes/auth/AuthPluginPrimaryAuthenticationProvider.php
@@ -0,0 +1,429 @@
+<?php
+/**
+ * Primary authentication provider wrapper for AuthPlugin
+ *
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use AuthPlugin;
+use User;
+
+/**
+ * Primary authentication provider wrapper for AuthPlugin
+ * @warning If anything depends on the wrapped AuthPlugin being $wgAuth, it won't work with this!
+ * @ingroup Auth
+ * @since 1.27
+ * @deprecated since 1.27
+ */
+class AuthPluginPrimaryAuthenticationProvider
+ extends AbstractPasswordPrimaryAuthenticationProvider
+{
+ private $auth;
+ private $hasDomain;
+ private $requestType = null;
+
+ /**
+ * @param AuthPlugin $auth AuthPlugin to wrap
+ * @param string|null $requestType Class name of the
+ * PasswordAuthenticationRequest to use. If $auth->domainList() returns
+ * more than one domain, this must be a PasswordDomainAuthenticationRequest.
+ */
+ public function __construct( AuthPlugin $auth, $requestType = null ) {
+ parent::__construct();
+
+ if ( $auth instanceof AuthManagerAuthPlugin ) {
+ throw new \InvalidArgumentException(
+ 'Trying to wrap AuthManagerAuthPlugin in AuthPluginPrimaryAuthenticationProvider ' .
+ 'makes no sense.'
+ );
+ }
+
+ $need = count( $auth->domainList() ) > 1
+ ? PasswordDomainAuthenticationRequest::class
+ : PasswordAuthenticationRequest::class;
+ if ( $requestType === null ) {
+ $requestType = $need;
+ } elseif ( $requestType !== $need && !is_subclass_of( $requestType, $need ) ) {
+ throw new \InvalidArgumentException( "$requestType is not a $need" );
+ }
+
+ $this->auth = $auth;
+ $this->requestType = $requestType;
+ $this->hasDomain = (
+ $requestType === PasswordDomainAuthenticationRequest::class ||
+ is_subclass_of( $requestType, PasswordDomainAuthenticationRequest::class )
+ );
+ $this->authoritative = $auth->strict();
+
+ // Registering hooks from core is unusual, but is needed here to be
+ // able to call the AuthPlugin methods those hooks replace.
+ \Hooks::register( 'UserSaveSettings', [ $this, 'onUserSaveSettings' ] );
+ \Hooks::register( 'UserGroupsChanged', [ $this, 'onUserGroupsChanged' ] );
+ \Hooks::register( 'UserLoggedIn', [ $this, 'onUserLoggedIn' ] );
+ \Hooks::register( 'LocalUserCreated', [ $this, 'onLocalUserCreated' ] );
+ }
+
+ /**
+ * Create an appropriate AuthenticationRequest
+ * @return PasswordAuthenticationRequest
+ */
+ protected function makeAuthReq() {
+ $class = $this->requestType;
+ if ( $this->hasDomain ) {
+ return new $class( $this->auth->domainList() );
+ } else {
+ return new $class();
+ }
+ }
+
+ /**
+ * Call $this->auth->setDomain()
+ * @param PasswordAuthenticationRequest $req
+ */
+ protected function setDomain( $req ) {
+ if ( $this->hasDomain ) {
+ $domain = $req->domain;
+ } else {
+ // Just grab the first one.
+ $domainList = $this->auth->domainList();
+ $domain = reset( $domainList );
+ }
+
+ // Special:UserLogin does this. Strange.
+ if ( !$this->auth->validDomain( $domain ) ) {
+ $domain = $this->auth->getDomain();
+ }
+ $this->auth->setDomain( $domain );
+ }
+
+ /**
+ * Hook function to call AuthPlugin::updateExternalDB()
+ * @param User $user
+ * @codeCoverageIgnore
+ */
+ public function onUserSaveSettings( $user ) {
+ // No way to know the domain, just hope the provider handles that.
+ $this->auth->updateExternalDB( $user );
+ }
+
+ /**
+ * Hook function to call AuthPlugin::updateExternalDBGroups()
+ * @param User $user
+ * @param array $added
+ * @param array $removed
+ */
+ public function onUserGroupsChanged( $user, $added, $removed ) {
+ // No way to know the domain, just hope the provider handles that.
+ $this->auth->updateExternalDBGroups( $user, $added, $removed );
+ }
+
+ /**
+ * Hook function to call AuthPlugin::updateUser()
+ * @param User $user
+ */
+ public function onUserLoggedIn( $user ) {
+ $hookUser = $user;
+ // No way to know the domain, just hope the provider handles that.
+ $this->auth->updateUser( $hookUser );
+ if ( $hookUser !== $user ) {
+ throw new \UnexpectedValueException(
+ get_class( $this->auth ) . '::updateUser() tried to replace $user!'
+ );
+ }
+ }
+
+ /**
+ * Hook function to call AuthPlugin::initUser()
+ * @param User $user
+ * @param bool $autocreated
+ */
+ public function onLocalUserCreated( $user, $autocreated ) {
+ // For $autocreated, see self::autoCreatedAccount()
+ if ( !$autocreated ) {
+ $hookUser = $user;
+ // No way to know the domain, just hope the provider handles that.
+ $this->auth->initUser( $hookUser, $autocreated );
+ if ( $hookUser !== $user ) {
+ throw new \UnexpectedValueException(
+ get_class( $this->auth ) . '::initUser() tried to replace $user!'
+ );
+ }
+ }
+ }
+
+ public function getUniqueId() {
+ return parent::getUniqueId() . ':' . get_class( $this->auth );
+ }
+
+ public function getAuthenticationRequests( $action, array $options ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_LOGIN:
+ case AuthManager::ACTION_CREATE:
+ return [ $this->makeAuthReq() ];
+
+ case AuthManager::ACTION_CHANGE:
+ case AuthManager::ACTION_REMOVE:
+ // No way to know the domain, just hope the provider handles that.
+ return $this->auth->allowPasswordChange() ? [ $this->makeAuthReq() ] : [];
+
+ default:
+ return [];
+ }
+ }
+
+ public function beginPrimaryAuthentication( array $reqs ) {
+ $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
+ if ( !$req || $req->username === null || $req->password === null ||
+ ( $this->hasDomain && $req->domain === null )
+ ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $username = User::getCanonicalName( $req->username, 'usable' );
+ if ( $username === false ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $this->setDomain( $req );
+ if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) &&
+ $this->auth->authenticate( $username, $req->password )
+ ) {
+ return AuthenticationResponse::newPass( $username );
+ } else {
+ $this->authoritative = $this->auth->strict() || $this->auth->strictUserAuth( $username );
+ return $this->failResponse( $req );
+ }
+ }
+
+ public function testUserCanAuthenticate( $username ) {
+ $username = User::getCanonicalName( $username, 'usable' );
+ if ( $username === false ) {
+ return false;
+ }
+
+ // We have to check every domain, because at least LdapAuthentication
+ // interprets AuthPlugin::userExists() as applying only to the current
+ // domain.
+ $curDomain = $this->auth->getDomain();
+ $domains = $this->auth->domainList() ?: [ '' ];
+ foreach ( $domains as $domain ) {
+ $this->auth->setDomain( $domain );
+ if ( $this->testUserCanAuthenticateInternal( User::newFromName( $username ) ) ) {
+ $this->auth->setDomain( $curDomain );
+ return true;
+ }
+ }
+ $this->auth->setDomain( $curDomain );
+ return false;
+ }
+
+ /**
+ * @see self::testUserCanAuthenticate
+ * @note The caller is responsible for calling $this->auth->setDomain()
+ * @param User $user
+ * @return bool
+ */
+ private function testUserCanAuthenticateInternal( $user ) {
+ if ( $this->auth->userExists( $user->getName() ) ) {
+ return !$this->auth->getUserInstance( $user )->isLocked();
+ } else {
+ return false;
+ }
+ }
+
+ public function providerRevokeAccessForUser( $username ) {
+ $username = User::getCanonicalName( $username, 'usable' );
+ if ( $username === false ) {
+ return;
+ }
+ $user = User::newFromName( $username );
+ if ( $user ) {
+ // Reset the password on every domain.
+ $curDomain = $this->auth->getDomain();
+ $domains = $this->auth->domainList() ?: [ '' ];
+ $failed = [];
+ foreach ( $domains as $domain ) {
+ $this->auth->setDomain( $domain );
+ if ( $this->testUserCanAuthenticateInternal( $user ) &&
+ !$this->auth->setPassword( $user, null )
+ ) {
+ $failed[] = $domain === '' ? '(default)' : $domain;
+ }
+ }
+ $this->auth->setDomain( $curDomain );
+ if ( $failed ) {
+ throw new \UnexpectedValueException(
+ "AuthPlugin failed to reset password for $username in the following domains: "
+ . implode( ' ', $failed )
+ );
+ }
+ }
+ }
+
+ public function testUserExists( $username, $flags = User::READ_NORMAL ) {
+ $username = User::getCanonicalName( $username, 'usable' );
+ if ( $username === false ) {
+ return false;
+ }
+
+ // We have to check every domain, because at least LdapAuthentication
+ // interprets AuthPlugin::userExists() as applying only to the current
+ // domain.
+ $curDomain = $this->auth->getDomain();
+ $domains = $this->auth->domainList() ?: [ '' ];
+ foreach ( $domains as $domain ) {
+ $this->auth->setDomain( $domain );
+ if ( $this->auth->userExists( $username ) ) {
+ $this->auth->setDomain( $curDomain );
+ return true;
+ }
+ }
+ $this->auth->setDomain( $curDomain );
+ return false;
+ }
+
+ public function providerAllowsPropertyChange( $property ) {
+ // No way to know the domain, just hope the provider handles that.
+ return $this->auth->allowPropChange( $property );
+ }
+
+ public function providerAllowsAuthenticationDataChange(
+ AuthenticationRequest $req, $checkData = true
+ ) {
+ if ( get_class( $req ) !== $this->requestType ) {
+ return \StatusValue::newGood( 'ignored' );
+ }
+
+ // Hope it works, AuthPlugin gives us no way to do this.
+ $curDomain = $this->auth->getDomain();
+ $this->setDomain( $req );
+ try {
+ // If !$checkData the domain might be wrong. Nothing we can do about that.
+ if ( !$this->auth->allowPasswordChange() ) {
+ return \StatusValue::newFatal( 'authmanager-authplugin-setpass-denied' );
+ }
+
+ if ( !$checkData ) {
+ return \StatusValue::newGood();
+ }
+
+ if ( $this->hasDomain ) {
+ if ( $req->domain === null ) {
+ return \StatusValue::newGood( 'ignored' );
+ }
+ if ( !$this->auth->validDomain( $req->domain ) ) {
+ return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' );
+ }
+ }
+
+ $username = User::getCanonicalName( $req->username, 'usable' );
+ if ( $username !== false ) {
+ $sv = \StatusValue::newGood();
+ if ( $req->password !== null ) {
+ if ( $req->password !== $req->retype ) {
+ $sv->fatal( 'badretype' );
+ } else {
+ $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
+ }
+ }
+ return $sv;
+ } else {
+ return \StatusValue::newGood( 'ignored' );
+ }
+ } finally {
+ $this->auth->setDomain( $curDomain );
+ }
+ }
+
+ public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
+ if ( get_class( $req ) === $this->requestType ) {
+ $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
+ if ( $username === false ) {
+ return;
+ }
+
+ if ( $this->hasDomain && $req->domain === null ) {
+ return;
+ }
+
+ $this->setDomain( $req );
+ $user = User::newFromName( $username );
+ if ( !$this->auth->setPassword( $user, $req->password ) ) {
+ // This is totally unfriendly and leaves other
+ // AuthenticationProviders in an uncertain state, but what else
+ // can we do?
+ throw new \ErrorPageError(
+ 'authmanager-authplugin-setpass-failed-title',
+ 'authmanager-authplugin-setpass-failed-message'
+ );
+ }
+ }
+ }
+
+ public function accountCreationType() {
+ // No way to know the domain, just hope the provider handles that.
+ return $this->auth->canCreateAccounts() ? self::TYPE_CREATE : self::TYPE_NONE;
+ }
+
+ public function testForAccountCreation( $user, $creator, array $reqs ) {
+ return \StatusValue::newGood();
+ }
+
+ public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
+ if ( $this->accountCreationType() === self::TYPE_NONE ) {
+ throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
+ }
+
+ $req = AuthenticationRequest::getRequestByClass( $reqs, $this->requestType );
+ if ( !$req || $req->username === null || $req->password === null ||
+ ( $this->hasDomain && $req->domain === null )
+ ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $username = User::getCanonicalName( $req->username, 'usable' );
+ if ( $username === false ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $this->setDomain( $req );
+ if ( $this->auth->addUser(
+ $user, $req->password, $user->getEmail(), $user->getRealName()
+ ) ) {
+ return AuthenticationResponse::newPass();
+ } else {
+ return AuthenticationResponse::newFail(
+ new \Message( 'authmanager-authplugin-create-fail' )
+ );
+ }
+ }
+
+ public function autoCreatedAccount( $user, $source ) {
+ $hookUser = $user;
+ // No way to know the domain, just hope the provider handles that.
+ $this->auth->initUser( $hookUser, true );
+ if ( $hookUser !== $user ) {
+ throw new \UnexpectedValueException(
+ get_class( $this->auth ) . '::initUser() tried to replace $user!'
+ );
+ }
+ }
+}
diff --git a/www/wiki/includes/auth/AuthenticationProvider.php b/www/wiki/includes/auth/AuthenticationProvider.php
new file mode 100644
index 00000000..11f3e226
--- /dev/null
+++ b/www/wiki/includes/auth/AuthenticationProvider.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * Authentication provider interface
+ *
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Config;
+use Psr\Log\LoggerAwareInterface;
+
+/**
+ * An AuthenticationProvider is used by AuthManager when authenticating users.
+ *
+ * This interface should not be implemented directly; use one of its children.
+ *
+ * Authentication providers can be registered via $wgAuthManagerAutoConfig.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+interface AuthenticationProvider extends LoggerAwareInterface {
+
+ /**
+ * Set AuthManager
+ * @param AuthManager $manager
+ */
+ public function setManager( AuthManager $manager );
+
+ /**
+ * Set configuration
+ * @param Config $config
+ */
+ public function setConfig( Config $config );
+
+ /**
+ * Return a unique identifier for this instance
+ *
+ * This must be the same across requests. If multiple instances return the
+ * same ID, exceptions will be thrown from AuthManager.
+ *
+ * @return string
+ */
+ public function getUniqueId();
+
+ /**
+ * Return the applicable list of AuthenticationRequests
+ *
+ * Possible values for $action depend on whether the implementing class is
+ * also a PreAuthenticationProvider, PrimaryAuthenticationProvider, or
+ * SecondaryAuthenticationProvider.
+ * - ACTION_LOGIN: Valid for passing to beginAuthentication. Called on all
+ * providers.
+ * - ACTION_CREATE: Valid for passing to beginAccountCreation. Called on
+ * all providers.
+ * - ACTION_LINK: Valid for passing to beginAccountLink. Called on linking
+ * primary providers only.
+ * - ACTION_CHANGE: Valid for passing to AuthManager::changeAuthenticationData
+ * to change credentials. Called on primary and secondary providers.
+ * - ACTION_REMOVE: Valid for passing to AuthManager::changeAuthenticationData
+ * to remove credentials. Must work without additional user input (i.e.
+ * without calling loadFromSubmission). Called on primary and secondary
+ * providers.
+ *
+ * @see AuthManager::getAuthenticationRequests()
+ * @param string $action
+ * @param array $options Options are:
+ * - username: User name related to the action, or null/unset if anon.
+ * - ACTION_LOGIN: The currently logged-in user, if any.
+ * - ACTION_CREATE: The account creator, if non-anonymous.
+ * - ACTION_LINK: The local user being linked to.
+ * - ACTION_CHANGE: The user having data changed.
+ * - ACTION_REMOVE: The user having data removed.
+ * If you leave the username property of the returned requests empty, this
+ * will automatically be copied there (except for ACTION_CREATE where it
+ * wouldn't really make sense).
+ * @return AuthenticationRequest[]
+ */
+ public function getAuthenticationRequests( $action, array $options );
+
+}
diff --git a/www/wiki/includes/auth/AuthenticationRequest.php b/www/wiki/includes/auth/AuthenticationRequest.php
new file mode 100644
index 00000000..7fc362a2
--- /dev/null
+++ b/www/wiki/includes/auth/AuthenticationRequest.php
@@ -0,0 +1,379 @@
+<?php
+/**
+ * Authentication request value object
+ *
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Message;
+
+/**
+ * This is a value object for authentication requests.
+ *
+ * An AuthenticationRequest represents a set of form fields that are needed on
+ * and provided from a login, account creation, password change or similar form.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+abstract class AuthenticationRequest {
+
+ /** Indicates that the request is not required for authentication to proceed. */
+ const OPTIONAL = 0;
+
+ /** Indicates that the request is required for authentication to proceed.
+ * This will only be used for UI purposes; it is the authentication providers'
+ * responsibility to verify that all required requests are present.
+ */
+ const REQUIRED = 1;
+
+ /** Indicates that the request is required by a primary authentication
+ * provider. Since the user can choose which primary to authenticate with,
+ * the request might or might not end up being actually required. */
+ const PRIMARY_REQUIRED = 2;
+
+ /** @var string|null The AuthManager::ACTION_* constant this request was
+ * created to be used for. The *_CONTINUE constants are not used here, the
+ * corresponding "begin" constant is used instead.
+ */
+ public $action = null;
+
+ /** @var int For login, continue, and link actions, one of self::OPTIONAL,
+ * self::REQUIRED, or self::PRIMARY_REQUIRED */
+ public $required = self::REQUIRED;
+
+ /** @var string|null Return-to URL, in case of redirect */
+ public $returnToUrl = null;
+
+ /** @var string|null Username. See AuthenticationProvider::getAuthenticationRequests()
+ * for details of what this means and how it behaves. */
+ public $username = null;
+
+ /**
+ * Supply a unique key for deduplication
+ *
+ * When the AuthenticationRequests instances returned by the providers are
+ * merged, the value returned here is used for keeping only one copy of
+ * duplicate requests.
+ *
+ * Subclasses should override this if multiple distinct instances would
+ * make sense, i.e. the request class has internal state of some sort.
+ *
+ * This value might be exposed to the user in web forms so it should not
+ * contain private information.
+ *
+ * @return string
+ */
+ public function getUniqueId() {
+ return get_called_class();
+ }
+
+ /**
+ * Fetch input field info
+ *
+ * The field info is an associative array mapping field names to info
+ * arrays. The info arrays have the following keys:
+ * - type: (string) Type of input. Types and equivalent HTML widgets are:
+ * - string: <input type="text">
+ * - password: <input type="password">
+ * - select: <select>
+ * - checkbox: <input type="checkbox">
+ * - multiselect: More a grid of checkboxes than <select multi>
+ * - button: <input type="submit"> (uses 'label' as button text)
+ * - hidden: Not visible to the user, but needs to be preserved for the next request
+ * - null: No widget, just display the 'label' message.
+ * - options: (array) Maps option values to Messages for the
+ * 'select' and 'multiselect' types.
+ * - value: (string) Value (for 'null' and 'hidden') or default value (for other types).
+ * - label: (Message) Text suitable for a label in an HTML form
+ * - help: (Message) Text suitable as a description of what the field is
+ * - optional: (bool) If set and truthy, the field may be left empty
+ * - sensitive: (bool) If set and truthy, the field is considered sensitive. Code using the
+ * request should avoid exposing the value of the field.
+ * - skippable: (bool) If set and truthy, the client is free to hide this
+ * field from the user to streamline the workflow. If all fields are
+ * skippable (except possibly a single button), no user interaction is
+ * required at all.
+ *
+ * All AuthenticationRequests are populated from the same data, so most of the time you'll
+ * want to prefix fields names with something unique to the extension/provider (although
+ * in some cases sharing the field with other requests is the right thing to do, e.g. for
+ * a 'password' field).
+ *
+ * @return array As above
+ */
+ abstract public function getFieldInfo();
+
+ /**
+ * Returns metadata about this request.
+ *
+ * This is mainly for the benefit of API clients which need more detailed render hints
+ * than what's available through getFieldInfo(). Semantics are unspecified and left to the
+ * individual subclasses, but the contents of the array should be primitive types so that they
+ * can be transformed into JSON or similar formats.
+ *
+ * @return array A (possibly nested) array with primitive types
+ */
+ public function getMetadata() {
+ return [];
+ }
+
+ /**
+ * Initialize form submitted form data.
+ *
+ * The default behavior is to to check for each key of self::getFieldInfo()
+ * in the submitted data, and copy the value - after type-appropriate transformations -
+ * to $this->$key. Most subclasses won't need to override this; if you do override it,
+ * make sure to always return false if self::getFieldInfo() returns an empty array.
+ *
+ * @param array $data Submitted data as an associative array (keys will correspond
+ * to getFieldInfo())
+ * @return bool Whether the request data was successfully loaded
+ */
+ public function loadFromSubmission( array $data ) {
+ $fields = array_filter( $this->getFieldInfo(), function ( $info ) {
+ return $info['type'] !== 'null';
+ } );
+ if ( !$fields ) {
+ return false;
+ }
+
+ foreach ( $fields as $field => $info ) {
+ // Checkboxes and buttons are special. Depending on the method used
+ // to populate $data, they might be unset meaning false or they
+ // might be boolean. Further, image buttons might submit the
+ // coordinates of the click rather than the expected value.
+ if ( $info['type'] === 'checkbox' || $info['type'] === 'button' ) {
+ $this->$field = isset( $data[$field] ) && $data[$field] !== false
+ || isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false;
+ if ( !$this->$field && empty( $info['optional'] ) ) {
+ return false;
+ }
+ continue;
+ }
+
+ // Multiselect are too, slightly
+ if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) {
+ $data[$field] = [];
+ }
+
+ if ( !isset( $data[$field] ) ) {
+ return false;
+ }
+ if ( $data[$field] === '' || $data[$field] === [] ) {
+ if ( empty( $info['optional'] ) ) {
+ return false;
+ }
+ } else {
+ switch ( $info['type'] ) {
+ case 'select':
+ if ( !isset( $info['options'][$data[$field]] ) ) {
+ return false;
+ }
+ break;
+
+ case 'multiselect':
+ $data[$field] = (array)$data[$field];
+ $allowed = array_keys( $info['options'] );
+ if ( array_diff( $data[$field], $allowed ) !== [] ) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ $this->$field = $data[$field];
+ }
+
+ return true;
+ }
+
+ /**
+ * Describe the credentials represented by this request
+ *
+ * This is used on requests returned by
+ * AuthenticationProvider::getAuthenticationRequests() for ACTION_LINK
+ * and ACTION_REMOVE and for requests returned in
+ * AuthenticationResponse::$linkRequest to create useful user interfaces.
+ *
+ * @return Message[] with the following keys:
+ * - provider: A Message identifying the service that provides
+ * the credentials, e.g. the name of the third party authentication
+ * service.
+ * - account: A Message identifying the credentials themselves,
+ * e.g. the email address used with the third party authentication
+ * service.
+ */
+ public function describeCredentials() {
+ return [
+ 'provider' => new \RawMessage( '$1', [ get_called_class() ] ),
+ 'account' => new \RawMessage( '$1', [ $this->getUniqueId() ] ),
+ ];
+ }
+
+ /**
+ * Update a set of requests with form submit data, discarding ones that fail
+ * @param AuthenticationRequest[] $reqs
+ * @param array $data
+ * @return AuthenticationRequest[]
+ */
+ public static function loadRequestsFromSubmission( array $reqs, array $data ) {
+ return array_values( array_filter( $reqs, function ( $req ) use ( $data ) {
+ return $req->loadFromSubmission( $data );
+ } ) );
+ }
+
+ /**
+ * Select a request by class name.
+ * @param AuthenticationRequest[] $reqs
+ * @param string $class Class name
+ * @param bool $allowSubclasses If true, also returns any request that's a subclass of the given
+ * class.
+ * @return AuthenticationRequest|null Returns null if there is not exactly
+ * one matching request.
+ */
+ public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) {
+ $requests = array_filter( $reqs, function ( $req ) use ( $class, $allowSubclasses ) {
+ if ( $allowSubclasses ) {
+ return is_a( $req, $class, false );
+ } else {
+ return get_class( $req ) === $class;
+ }
+ } );
+ return count( $requests ) === 1 ? reset( $requests ) : null;
+ }
+
+ /**
+ * Get the username from the set of requests
+ *
+ * Only considers requests that have a "username" field.
+ *
+ * @param AuthenticationRequest[] $reqs
+ * @return string|null
+ * @throws \UnexpectedValueException If multiple different usernames are present.
+ */
+ public static function getUsernameFromRequests( array $reqs ) {
+ $username = null;
+ $otherClass = null;
+ foreach ( $reqs as $req ) {
+ $info = $req->getFieldInfo();
+ if ( $info && array_key_exists( 'username', $info ) && $req->username !== null ) {
+ if ( $username === null ) {
+ $username = $req->username;
+ $otherClass = get_class( $req );
+ } elseif ( $username !== $req->username ) {
+ $requestClass = get_class( $req );
+ throw new \UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from "
+ . "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" );
+ }
+ }
+ }
+ return $username;
+ }
+
+ /**
+ * Merge the output of multiple AuthenticationRequest::getFieldInfo() calls.
+ * @param AuthenticationRequest[] $reqs
+ * @return array
+ * @throws \UnexpectedValueException If fields cannot be merged
+ */
+ public static function mergeFieldInfo( array $reqs ) {
+ $merged = [];
+
+ // fields that are required by some primary providers but not others are not actually required
+ $primaryRequests = array_filter( $reqs, function ( $req ) {
+ return $req->required === AuthenticationRequest::PRIMARY_REQUIRED;
+ } );
+ $sharedRequiredPrimaryFields = array_reduce( $primaryRequests, function ( $shared, $req ) {
+ $required = array_keys( array_filter( $req->getFieldInfo(), function ( $options ) {
+ return empty( $options['optional'] );
+ } ) );
+ if ( $shared === null ) {
+ return $required;
+ } else {
+ return array_intersect( $shared, $required );
+ }
+ }, null );
+
+ foreach ( $reqs as $req ) {
+ $info = $req->getFieldInfo();
+ if ( !$info ) {
+ continue;
+ }
+
+ foreach ( $info as $name => $options ) {
+ if (
+ // If the request isn't required, its fields aren't required either.
+ $req->required === self::OPTIONAL
+ // If there is a primary not requiring this field, no matter how many others do,
+ // authentication can proceed without it.
+ || $req->required === self::PRIMARY_REQUIRED
+ && !in_array( $name, $sharedRequiredPrimaryFields, true )
+ ) {
+ $options['optional'] = true;
+ } else {
+ $options['optional'] = !empty( $options['optional'] );
+ }
+
+ $options['sensitive'] = !empty( $options['sensitive'] );
+
+ if ( !array_key_exists( $name, $merged ) ) {
+ $merged[$name] = $options;
+ } elseif ( $merged[$name]['type'] !== $options['type'] ) {
+ throw new \UnexpectedValueException( "Field type conflict for \"$name\", " .
+ "\"{$merged[$name]['type']}\" vs \"{$options['type']}\""
+ );
+ } else {
+ if ( isset( $options['options'] ) ) {
+ if ( isset( $merged[$name]['options'] ) ) {
+ $merged[$name]['options'] += $options['options'];
+ } else {
+ // @codeCoverageIgnoreStart
+ $merged[$name]['options'] = $options['options'];
+ // @codeCoverageIgnoreEnd
+ }
+ }
+
+ $merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
+ $merged[$name]['sensitive'] = $merged[$name]['sensitive'] || $options['sensitive'];
+
+ // No way to merge 'value', 'image', 'help', or 'label', so just use
+ // the value from the first request.
+ }
+ }
+ }
+
+ return $merged;
+ }
+
+ /**
+ * Implementing this mainly for use from the unit tests.
+ * @param array $data
+ * @return AuthenticationRequest
+ */
+ public static function __set_state( $data ) {
+ $ret = new static();
+ foreach ( $data as $k => $v ) {
+ $ret->$k = $v;
+ }
+ return $ret;
+ }
+}
diff --git a/www/wiki/includes/auth/AuthenticationResponse.php b/www/wiki/includes/auth/AuthenticationResponse.php
new file mode 100644
index 00000000..956c9850
--- /dev/null
+++ b/www/wiki/includes/auth/AuthenticationResponse.php
@@ -0,0 +1,219 @@
+<?php
+/**
+ * Authentication response value object
+ *
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Message;
+
+/**
+ * This is a value object to hold authentication response data
+ *
+ * An AuthenticationResponse represents both the status of the authentication
+ * (success, failure, in progress) and it its state (what data is needed to continue).
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class AuthenticationResponse {
+ /** Indicates that the authentication succeeded. */
+ const PASS = 'PASS';
+
+ /** Indicates that the authentication failed. */
+ const FAIL = 'FAIL';
+
+ /** Indicates that third-party authentication succeeded but no user exists.
+ * Either treat this like a UI response or pass $this->createRequest to
+ * AuthManager::beginCreateAccount(). For use by AuthManager only (providers
+ * should just return a PASS with no username).
+ */
+ const RESTART = 'RESTART';
+
+ /** Indicates that the authentication provider does not handle this request. */
+ const ABSTAIN = 'ABSTAIN';
+
+ /** Indicates that the authentication needs further user input of some sort. */
+ const UI = 'UI';
+
+ /** Indicates that the authentication needs to be redirected to a third party to proceed. */
+ const REDIRECT = 'REDIRECT';
+
+ /** @var string One of the constants above */
+ public $status;
+
+ /** @var string|null URL to redirect to for a REDIRECT response */
+ public $redirectTarget = null;
+
+ /**
+ * @var mixed Data for a REDIRECT response that a client might use to
+ * query the remote site via its API rather than by following $redirectTarget.
+ * Value must be something acceptable to ApiResult::addValue().
+ */
+ public $redirectApiData = null;
+
+ /**
+ * @var AuthenticationRequest[] Needed AuthenticationRequests to continue
+ * after a UI or REDIRECT response. This plays the same role when continuing
+ * authentication as AuthManager::getAuthenticationRequests() does when
+ * beginning it.
+ */
+ public $neededRequests = [];
+
+ /** @var Message|null I18n message to display in case of UI or FAIL */
+ public $message = null;
+
+ /** @var string Whether the $message is an error or warning message, for styling reasons */
+ public $messageType = 'warning';
+
+ /**
+ * @var string|null Local user name from authentication.
+ * May be null if the authentication passed but no local user is known.
+ */
+ public $username = null;
+
+ /**
+ * @var AuthenticationRequest|null
+ *
+ * Returned with a PrimaryAuthenticationProvider login FAIL or a PASS with
+ * no username, this can be set to a request that should result in a PASS when
+ * passed to that provider's PrimaryAuthenticationProvider::beginPrimaryAccountCreation().
+ * The client will be able to send that back for expedited account creation where only
+ * the username needs to be filled.
+ *
+ * Returned with an AuthManager login FAIL or RESTART, this holds a
+ * CreateFromLoginAuthenticationRequest that may be passed to
+ * AuthManager::beginCreateAccount(), possibly in place of any
+ * "primary-required" requests. It may also be passed to
+ * AuthManager::beginAuthentication() to preserve the list of
+ * accounts which can be linked after success (see $linkRequest).
+ */
+ public $createRequest = null;
+
+ /**
+ * @var AuthenticationRequest|null When returned with a PrimaryAuthenticationProvider
+ * login PASS with no username, the request this holds will be passed to
+ * AuthManager::changeAuthenticationData() once the local user has been determined and the
+ * user has confirmed the account ownership (by reviewing the information given by
+ * $linkRequest->describeCredentials()). The provider should handle that
+ * changeAuthenticationData() call by doing the actual linking.
+ */
+ public $linkRequest = null;
+
+ /**
+ * @var AuthenticationRequest|null Returned with an AuthManager account
+ * creation PASS, this holds a request to pass to AuthManager::beginAuthentication()
+ * to immediately log into the created account. All provider methods except
+ * postAuthentication will be skipped.
+ */
+ public $loginRequest = null;
+
+ /**
+ * @param string|null $username Local username
+ * @return AuthenticationResponse
+ * @see AuthenticationResponse::PASS
+ */
+ public static function newPass( $username = null ) {
+ $ret = new AuthenticationResponse;
+ $ret->status = self::PASS;
+ $ret->username = $username;
+ return $ret;
+ }
+
+ /**
+ * @param Message $msg
+ * @return AuthenticationResponse
+ * @see AuthenticationResponse::FAIL
+ */
+ public static function newFail( Message $msg ) {
+ $ret = new AuthenticationResponse;
+ $ret->status = self::FAIL;
+ $ret->message = $msg;
+ $ret->messageType = 'error';
+ return $ret;
+ }
+
+ /**
+ * @param Message $msg
+ * @return AuthenticationResponse
+ * @see AuthenticationResponse::RESTART
+ */
+ public static function newRestart( Message $msg ) {
+ $ret = new AuthenticationResponse;
+ $ret->status = self::RESTART;
+ $ret->message = $msg;
+ return $ret;
+ }
+
+ /**
+ * @return AuthenticationResponse
+ * @see AuthenticationResponse::ABSTAIN
+ */
+ public static function newAbstain() {
+ $ret = new AuthenticationResponse;
+ $ret->status = self::ABSTAIN;
+ return $ret;
+ }
+
+ /**
+ * @param AuthenticationRequest[] $reqs AuthenticationRequests needed to continue
+ * @param Message $msg
+ * @param string $msgtype
+ * @return AuthenticationResponse
+ * @see AuthenticationResponse::UI
+ */
+ public static function newUI( array $reqs, Message $msg, $msgtype = 'warning' ) {
+ if ( !$reqs ) {
+ throw new \InvalidArgumentException( '$reqs may not be empty' );
+ }
+ if ( $msgtype !== 'warning' && $msgtype !== 'error' ) {
+ throw new \InvalidArgumentException( $msgtype . ' is not a valid message type.' );
+ }
+
+ $ret = new AuthenticationResponse;
+ $ret->status = self::UI;
+ $ret->neededRequests = $reqs;
+ $ret->message = $msg;
+ $ret->messageType = $msgtype;
+ return $ret;
+ }
+
+ /**
+ * @param AuthenticationRequest[] $reqs AuthenticationRequests needed to continue
+ * @param string $redirectTarget URL
+ * @param mixed $redirectApiData Data suitable for adding to an ApiResult
+ * @return AuthenticationResponse
+ * @see AuthenticationResponse::REDIRECT
+ */
+ public static function newRedirect( array $reqs, $redirectTarget, $redirectApiData = null ) {
+ if ( !$reqs ) {
+ throw new \InvalidArgumentException( '$reqs may not be empty' );
+ }
+
+ $ret = new AuthenticationResponse;
+ $ret->status = self::REDIRECT;
+ $ret->neededRequests = $reqs;
+ $ret->redirectTarget = $redirectTarget;
+ $ret->redirectApiData = $redirectApiData;
+ return $ret;
+ }
+
+}
diff --git a/www/wiki/includes/auth/ButtonAuthenticationRequest.php b/www/wiki/includes/auth/ButtonAuthenticationRequest.php
new file mode 100644
index 00000000..d274e18f
--- /dev/null
+++ b/www/wiki/includes/auth/ButtonAuthenticationRequest.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Message;
+
+/**
+ * This is an authentication request that just implements a simple button.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class ButtonAuthenticationRequest extends AuthenticationRequest {
+ /** @var string */
+ protected $name;
+
+ /** @var Message */
+ protected $label;
+
+ /** @var Message */
+ protected $help;
+
+ /**
+ * @param string $name Button name
+ * @param Message $label Button label
+ * @param Message $help Button help
+ * @param bool $required The button is required for authentication to proceed.
+ */
+ public function __construct( $name, Message $label, Message $help, $required = false ) {
+ $this->name = $name;
+ $this->label = $label;
+ $this->help = $help;
+ $this->required = $required ? self::REQUIRED : self::OPTIONAL;
+ }
+
+ public function getUniqueId() {
+ return parent::getUniqueId() . ':' . $this->name;
+ }
+
+ public function getFieldInfo() {
+ return [
+ $this->name => [
+ 'type' => 'button',
+ 'label' => $this->label,
+ 'help' => $this->help,
+ ]
+ ];
+ }
+
+ /**
+ * Fetch a ButtonAuthenticationRequest or subclass by name
+ * @param AuthenticationRequest[] $reqs Requests to search
+ * @param string $name Name to look for
+ * @return ButtonAuthenticationRequest|null Returns null if there is not
+ * exactly one matching request.
+ */
+ public static function getRequestByName( array $reqs, $name ) {
+ $requests = array_filter( $reqs, function ( $req ) use ( $name ) {
+ return $req instanceof ButtonAuthenticationRequest && $req->name === $name;
+ } );
+ return count( $requests ) === 1 ? reset( $requests ) : null;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @param array $data
+ * @return AuthenticationRequest|static
+ */
+ public static function __set_state( $data ) {
+ if ( !isset( $data['label'] ) ) {
+ $data['label'] = new \RawMessage( '$1', $data['name'] );
+ } elseif ( is_string( $data['label'] ) ) {
+ $data['label'] = new \Message( $data['label'] );
+ } elseif ( is_array( $data['label'] ) ) {
+ $data['label'] = call_user_func_array( 'Message::newFromKey', $data['label'] );
+ }
+ if ( !isset( $data['help'] ) ) {
+ $data['help'] = new \RawMessage( '$1', $data['name'] );
+ } elseif ( is_string( $data['help'] ) ) {
+ $data['help'] = new \Message( $data['help'] );
+ } elseif ( is_array( $data['help'] ) ) {
+ $data['help'] = call_user_func_array( 'Message::newFromKey', $data['help'] );
+ }
+ $ret = new static( $data['name'], $data['label'], $data['help'] );
+ foreach ( $data as $k => $v ) {
+ $ret->$k = $v;
+ }
+ return $ret;
+ }
+}
diff --git a/www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php b/www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php
new file mode 100644
index 00000000..7488fbaa
--- /dev/null
+++ b/www/wiki/includes/auth/CheckBlocksSecondaryAuthenticationProvider.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use Config;
+use StatusValue;
+
+/**
+ * Check if the user is blocked, and prevent authentication if so.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class CheckBlocksSecondaryAuthenticationProvider extends AbstractSecondaryAuthenticationProvider {
+
+ /** @var bool */
+ protected $blockDisablesLogin = null;
+
+ /**
+ * @param array $params
+ * - blockDisablesLogin: (bool) Whether blocked accounts can log in,
+ * defaults to $wgBlockDisablesLogin
+ */
+ public function __construct( $params = [] ) {
+ if ( isset( $params['blockDisablesLogin'] ) ) {
+ $this->blockDisablesLogin = (bool)$params['blockDisablesLogin'];
+ }
+ }
+
+ public function setConfig( Config $config ) {
+ parent::setConfig( $config );
+
+ if ( $this->blockDisablesLogin === null ) {
+ $this->blockDisablesLogin = $this->config->get( 'BlockDisablesLogin' );
+ }
+ }
+
+ public function getAuthenticationRequests( $action, array $options ) {
+ return [];
+ }
+
+ public function beginSecondaryAuthentication( $user, array $reqs ) {
+ if ( !$this->blockDisablesLogin ) {
+ return AuthenticationResponse::newAbstain();
+ } elseif ( $user->isBlocked() ) {
+ return AuthenticationResponse::newFail(
+ new \Message( 'login-userblocked', [ $user->getName() ] )
+ );
+ } else {
+ return AuthenticationResponse::newPass();
+ }
+ }
+
+ public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ public function testUserForCreation( $user, $autocreate, array $options = [] ) {
+ $block = $user->isBlockedFromCreateAccount();
+ if ( $block ) {
+ if ( $block->mReason ) {
+ $reason = $block->mReason;
+ } else {
+ $msg = \Message::newFromKey( 'blockednoreason' );
+ if ( !\RequestContext::getMain()->getUser()->isSafeToLoad() ) {
+ $msg->inContentLanguage();
+ }
+ $reason = $msg->text();
+ }
+
+ $errorParams = [
+ $block->getTarget(),
+ $reason,
+ $block->getByName()
+ ];
+
+ if ( $block->getType() === \Block::TYPE_RANGE ) {
+ $errorMessage = 'cantcreateaccount-range-text';
+ $errorParams[] = $this->manager->getRequest()->getIP();
+ } else {
+ $errorMessage = 'cantcreateaccount-text';
+ }
+
+ return StatusValue::newFatal(
+ new \Message( $errorMessage, $errorParams )
+ );
+ } else {
+ return StatusValue::newGood();
+ }
+ }
+
+}
diff --git a/www/wiki/includes/auth/ConfirmLinkAuthenticationRequest.php b/www/wiki/includes/auth/ConfirmLinkAuthenticationRequest.php
new file mode 100644
index 00000000..b82914f5
--- /dev/null
+++ b/www/wiki/includes/auth/ConfirmLinkAuthenticationRequest.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+class ConfirmLinkAuthenticationRequest extends AuthenticationRequest {
+ /** @var AuthenticationRequest[] */
+ protected $linkRequests;
+
+ /** @var string[] List of unique IDs of the confirmed accounts. */
+ public $confirmedLinkIDs = [];
+
+ /**
+ * @param AuthenticationRequest[] $linkRequests A list of autolink requests
+ * which need to be confirmed.
+ */
+ public function __construct( array $linkRequests ) {
+ if ( !$linkRequests ) {
+ throw new \InvalidArgumentException( '$linkRequests must not be empty' );
+ }
+ $this->linkRequests = $linkRequests;
+ }
+
+ public function getFieldInfo() {
+ $options = [];
+ foreach ( $this->linkRequests as $req ) {
+ $description = $req->describeCredentials();
+ $options[$req->getUniqueId()] = wfMessage(
+ 'authprovider-confirmlink-option',
+ $description['provider']->text(), $description['account']->text()
+ );
+ }
+ return [
+ 'confirmedLinkIDs' => [
+ 'type' => 'multiselect',
+ 'options' => $options,
+ 'label' => wfMessage( 'authprovider-confirmlink-request-label' ),
+ 'help' => wfMessage( 'authprovider-confirmlink-request-help' ),
+ 'optional' => true,
+ ]
+ ];
+ }
+
+ public function getUniqueId() {
+ return parent::getUniqueId() . ':' . implode( '|', array_map( function ( $req ) {
+ return $req->getUniqueId();
+ }, $this->linkRequests ) );
+ }
+
+ /**
+ * Implementing this mainly for use from the unit tests.
+ * @param array $data
+ * @return AuthenticationRequest
+ */
+ public static function __set_state( $data ) {
+ $ret = new static( $data['linkRequests'] );
+ foreach ( $data as $k => $v ) {
+ $ret->$k = $v;
+ }
+ return $ret;
+ }
+}
diff --git a/www/wiki/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php b/www/wiki/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php
new file mode 100644
index 00000000..7f121cde
--- /dev/null
+++ b/www/wiki/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use User;
+
+/**
+ * Links third-party authentication to the user's account
+ *
+ * If the user logged into linking provider accounts that aren't linked to a
+ * local user, this provider will prompt the user to link them after a
+ * successful login or account creation.
+ *
+ * To avoid confusing behavior, this provider should be later in the
+ * configuration list than any provider that can abort the authentication
+ * process, so that it is only invoked for successful authentication.
+ */
+class ConfirmLinkSecondaryAuthenticationProvider extends AbstractSecondaryAuthenticationProvider {
+
+ public function getAuthenticationRequests( $action, array $options ) {
+ return [];
+ }
+
+ public function beginSecondaryAuthentication( $user, array $reqs ) {
+ return $this->beginLinkAttempt( $user, 'AuthManager::authnState' );
+ }
+
+ public function continueSecondaryAuthentication( $user, array $reqs ) {
+ return $this->continueLinkAttempt( $user, 'AuthManager::authnState', $reqs );
+ }
+
+ public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+ return $this->beginLinkAttempt( $user, 'AuthManager::accountCreationState' );
+ }
+
+ public function continueSecondaryAccountCreation( $user, $creator, array $reqs ) {
+ return $this->continueLinkAttempt( $user, 'AuthManager::accountCreationState', $reqs );
+ }
+
+ /**
+ * Begin the link attempt
+ * @param User $user
+ * @param string $key Session key to look in
+ * @return AuthenticationResponse
+ */
+ protected function beginLinkAttempt( $user, $key ) {
+ $session = $this->manager->getRequest()->getSession();
+ $state = $session->getSecret( $key );
+ if ( !is_array( $state ) ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $maybeLink = array_filter( $state['maybeLink'], function ( $req ) use ( $user ) {
+ if ( !$req->action ) {
+ $req->action = AuthManager::ACTION_CHANGE;
+ }
+ $req->username = $user->getName();
+ return $this->manager->allowsAuthenticationDataChange( $req )->isGood();
+ } );
+ if ( !$maybeLink ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $req = new ConfirmLinkAuthenticationRequest( $maybeLink );
+ return AuthenticationResponse::newUI(
+ [ $req ],
+ wfMessage( 'authprovider-confirmlink-message' ),
+ 'warning'
+ );
+ }
+
+ /**
+ * Continue the link attempt
+ * @param User $user
+ * @param string $key Session key to look in
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse
+ */
+ protected function continueLinkAttempt( $user, $key, array $reqs ) {
+ $req = ButtonAuthenticationRequest::getRequestByName( $reqs, 'linkOk' );
+ if ( $req ) {
+ return AuthenticationResponse::newPass();
+ }
+
+ $req = AuthenticationRequest::getRequestByClass( $reqs, ConfirmLinkAuthenticationRequest::class );
+ if ( !$req ) {
+ // WTF? Retry.
+ return $this->beginLinkAttempt( $user, $key );
+ }
+
+ $session = $this->manager->getRequest()->getSession();
+ $state = $session->getSecret( $key );
+ if ( !is_array( $state ) ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $maybeLink = [];
+ foreach ( $state['maybeLink'] as $linkReq ) {
+ $maybeLink[$linkReq->getUniqueId()] = $linkReq;
+ }
+ if ( !$maybeLink ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $state['maybeLink'] = [];
+ $session->setSecret( $key, $state );
+
+ $statuses = [];
+ $anyFailed = false;
+ foreach ( $req->confirmedLinkIDs as $id ) {
+ if ( isset( $maybeLink[$id] ) ) {
+ $req = $maybeLink[$id];
+ $req->username = $user->getName();
+ if ( !$req->action ) {
+ // Make sure the action is set, but don't override it if
+ // the provider filled it in.
+ $req->action = AuthManager::ACTION_CHANGE;
+ }
+ $status = $this->manager->allowsAuthenticationDataChange( $req );
+ $statuses[] = [ $req, $status ];
+ if ( $status->isGood() ) {
+ $this->manager->changeAuthenticationData( $req );
+ } else {
+ $anyFailed = true;
+ }
+ }
+ }
+ if ( !$anyFailed ) {
+ return AuthenticationResponse::newPass();
+ }
+
+ $combinedStatus = \Status::newGood();
+ foreach ( $statuses as $data ) {
+ list( $req, $status ) = $data;
+ $descriptionInfo = $req->describeCredentials();
+ $description = wfMessage(
+ 'authprovider-confirmlink-option',
+ $descriptionInfo['provider']->text(), $descriptionInfo['account']->text()
+ )->text();
+ if ( $status->isGood() ) {
+ $combinedStatus->error( wfMessage( 'authprovider-confirmlink-success-line', $description ) );
+ } else {
+ $combinedStatus->error( wfMessage(
+ 'authprovider-confirmlink-failed-line', $description, $status->getMessage()->text()
+ ) );
+ }
+ }
+ return AuthenticationResponse::newUI(
+ [
+ new ButtonAuthenticationRequest(
+ 'linkOk', wfMessage( 'ok' ), wfMessage( 'authprovider-confirmlink-ok-help' )
+ )
+ ],
+ $combinedStatus->getMessage( 'authprovider-confirmlink-failed' ),
+ 'error'
+ );
+ }
+}
diff --git a/www/wiki/includes/auth/CreateFromLoginAuthenticationRequest.php b/www/wiki/includes/auth/CreateFromLoginAuthenticationRequest.php
new file mode 100644
index 00000000..db827972
--- /dev/null
+++ b/www/wiki/includes/auth/CreateFromLoginAuthenticationRequest.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * This transfers state between the login and account creation flows.
+ *
+ * AuthManager::getAuthenticationRequests() won't return this type, but it
+ * may be passed to AuthManager::beginAuthentication() or
+ * AuthManager::beginAccountCreation() anyway.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class CreateFromLoginAuthenticationRequest extends AuthenticationRequest {
+ public $required = self::OPTIONAL;
+
+ /** @var AuthenticationRequest|null */
+ public $createRequest;
+
+ /** @var AuthenticationRequest[] */
+ public $maybeLink = [];
+
+ /**
+ * @param AuthenticationRequest|null $createRequest A request to use to
+ * begin creating the account
+ * @param AuthenticationRequest[] $maybeLink Additional accounts to link
+ * after creation.
+ */
+ public function __construct(
+ AuthenticationRequest $createRequest = null, array $maybeLink = []
+ ) {
+ $this->createRequest = $createRequest;
+ $this->maybeLink = $maybeLink;
+ $this->username = $createRequest ? $createRequest->username : null;
+ }
+
+ public function getFieldInfo() {
+ return [];
+ }
+
+ public function loadFromSubmission( array $data ) {
+ return true;
+ }
+
+ /**
+ * Indicate whether this request contains any state for the specified
+ * action.
+ * @param string $action One of the AuthManager::ACTION_* constants
+ * @return bool
+ */
+ public function hasStateForAction( $action ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_LOGIN:
+ return (bool)$this->maybeLink;
+ case AuthManager::ACTION_CREATE:
+ return $this->maybeLink || $this->createRequest;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Indicate whether this request contains state for the specified
+ * action sufficient to replace other primary-required requests.
+ * @param string $action One of the AuthManager::ACTION_* constants
+ * @return bool
+ */
+ public function hasPrimaryStateForAction( $action ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_CREATE:
+ return (bool)$this->createRequest;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/www/wiki/includes/auth/CreatedAccountAuthenticationRequest.php b/www/wiki/includes/auth/CreatedAccountAuthenticationRequest.php
new file mode 100644
index 00000000..48a6e1d3
--- /dev/null
+++ b/www/wiki/includes/auth/CreatedAccountAuthenticationRequest.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * Returned from account creation to allow for logging into the created account
+ * @ingroup Auth
+ * @since 1.27
+ */
+class CreatedAccountAuthenticationRequest extends AuthenticationRequest {
+
+ public $required = self::OPTIONAL;
+
+ /** @var int User id */
+ public $id;
+
+ public function getFieldInfo() {
+ return [];
+ }
+
+ /**
+ * @param int $id User id
+ * @param string $name Username
+ */
+ public function __construct( $id, $name ) {
+ $this->id = $id;
+ $this->username = $name;
+ }
+}
diff --git a/www/wiki/includes/auth/CreationReasonAuthenticationRequest.php b/www/wiki/includes/auth/CreationReasonAuthenticationRequest.php
new file mode 100644
index 00000000..146470ed
--- /dev/null
+++ b/www/wiki/includes/auth/CreationReasonAuthenticationRequest.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+/**
+ * Authentication request for the reason given for account creation.
+ * Used in logs and for notification.
+ */
+class CreationReasonAuthenticationRequest extends AuthenticationRequest {
+ /** @var string Account creation reason (only used when creating for someone else) */
+ public $reason;
+
+ public $required = self::OPTIONAL;
+
+ public function getFieldInfo() {
+ return [
+ 'reason' => [
+ 'type' => 'string',
+ 'label' => wfMessage( 'createacct-reason' ),
+ 'help' => wfMessage( 'createacct-reason-help' ),
+ ],
+ ];
+ }
+}
diff --git a/www/wiki/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php b/www/wiki/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php
new file mode 100644
index 00000000..a4855318
--- /dev/null
+++ b/www/wiki/includes/auth/EmailNotificationSecondaryAuthenticationProvider.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace MediaWiki\Auth;
+
+use Config;
+
+/**
+ * Handles email notification / email address confirmation for account creation.
+ *
+ * Set 'no-email' to true (via AuthManager::setAuthenticationSessionData) to skip this provider.
+ * Primary providers doing so are expected to take care of email address confirmation.
+ */
+class EmailNotificationSecondaryAuthenticationProvider
+ extends AbstractSecondaryAuthenticationProvider
+{
+ /** @var bool */
+ protected $sendConfirmationEmail;
+
+ /**
+ * @param array $params
+ * - sendConfirmationEmail: (bool) send an email asking the user to confirm their email
+ * address after a successful registration
+ */
+ public function __construct( $params = [] ) {
+ if ( isset( $params['sendConfirmationEmail'] ) ) {
+ $this->sendConfirmationEmail = (bool)$params['sendConfirmationEmail'];
+ }
+ }
+
+ public function setConfig( Config $config ) {
+ parent::setConfig( $config );
+
+ if ( $this->sendConfirmationEmail === null ) {
+ $this->sendConfirmationEmail = $this->config->get( 'EnableEmail' )
+ && $this->config->get( 'EmailAuthentication' );
+ }
+ }
+
+ public function getAuthenticationRequests( $action, array $options ) {
+ return [];
+ }
+
+ public function beginSecondaryAuthentication( $user, array $reqs ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+ if (
+ $this->sendConfirmationEmail
+ && $user->getEmail()
+ && !$this->manager->getAuthenticationSessionData( 'no-email' )
+ ) {
+ // TODO show 'confirmemail_oncreate'/'confirmemail_sendfailed' message
+ wfGetDB( DB_MASTER )->onTransactionIdle(
+ function () use ( $user ) {
+ $user = $user->getInstanceForUpdate();
+ $status = $user->sendConfirmationMail();
+ $user->saveSettings();
+ if ( !$status->isGood() ) {
+ $this->logger->warning( 'Could not send confirmation email: ' .
+ $status->getWikiText( false, false, 'en' ) );
+ }
+ },
+ __METHOD__
+ );
+ }
+
+ return AuthenticationResponse::newPass();
+ }
+}
diff --git a/www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php b/www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php
new file mode 100644
index 00000000..95fe3ab8
--- /dev/null
+++ b/www/wiki/includes/auth/LegacyHookPreAuthenticationProvider.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use LoginForm;
+use StatusValue;
+use User;
+
+/**
+ * A pre-authentication provider to call some legacy hooks.
+ * @ingroup Auth
+ * @since 1.27
+ * @deprecated since 1.27
+ */
+class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvider {
+
+ public function testForAuthentication( array $reqs ) {
+ $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+ if ( $req ) {
+ $user = User::newFromName( $req->username );
+ $password = $req->password;
+ } else {
+ $user = null;
+ foreach ( $reqs as $req ) {
+ if ( $req->username !== null ) {
+ $user = User::newFromName( $req->username );
+ break;
+ }
+ }
+ if ( !$user ) {
+ $this->logger->debug( __METHOD__ . ': No username in $reqs, skipping hooks' );
+ return StatusValue::newGood();
+ }
+
+ // Something random for the 'AbortLogin' hook.
+ $password = wfRandomString( 32 );
+ }
+
+ $msg = null;
+ if ( !\Hooks::run( 'LoginUserMigrated', [ $user, &$msg ] ) ) {
+ return $this->makeFailResponse(
+ $user, LoginForm::USER_MIGRATED, $msg, 'LoginUserMigrated'
+ );
+ }
+
+ $abort = LoginForm::ABORTED;
+ $msg = null;
+ if ( !\Hooks::run( 'AbortLogin', [ $user, $password, &$abort, &$msg ] ) ) {
+ return $this->makeFailResponse( $user, $abort, $msg, 'AbortLogin' );
+ }
+
+ return StatusValue::newGood();
+ }
+
+ public function testForAccountCreation( $user, $creator, array $reqs ) {
+ $abortError = '';
+ $abortStatus = null;
+ if ( !\Hooks::run( 'AbortNewAccount', [ $user, &$abortError, &$abortStatus ] ) ) {
+ // Hook point to add extra creation throttles and blocks
+ $this->logger->debug( __METHOD__ . ': a hook blocked creation' );
+ if ( $abortStatus === null ) {
+ // Report back the old string as a raw message status.
+ // This will report the error back as 'createaccount-hook-aborted'
+ // with the given string as the message.
+ // To return a different error code, return a StatusValue object.
+ $msg = wfMessage( 'createaccount-hook-aborted' )->rawParams( $abortError );
+ return StatusValue::newFatal( $msg );
+ } else {
+ // For MediaWiki 1.23+ and updated hooks, return the Status object
+ // returned from the hook.
+ $ret = StatusValue::newGood();
+ $ret->merge( $abortStatus );
+ return $ret;
+ }
+ }
+
+ return StatusValue::newGood();
+ }
+
+ public function testUserForCreation( $user, $autocreate, array $options = [] ) {
+ if ( $autocreate !== false ) {
+ $abortError = '';
+ if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortError ] ) ) {
+ // Hook point to add extra creation throttles and blocks
+ $this->logger->debug( __METHOD__ . ": a hook blocked auto-creation: $abortError\n" );
+ return $this->makeFailResponse(
+ $user, LoginForm::ABORTED, $abortError, 'AbortAutoAccount'
+ );
+ }
+ }
+
+ return StatusValue::newGood();
+ }
+
+ /**
+ * Construct an appropriate failure response
+ * @param User $user
+ * @param int $constant One of the LoginForm::… constants
+ * @param string|null $msg Optional message key, will be derived from $constant otherwise
+ * @param string $hook Name of the hook for error logging and exception messages
+ * @return StatusValue
+ */
+ private function makeFailResponse( User $user, $constant, $msg, $hook ) {
+ switch ( $constant ) {
+ case LoginForm::SUCCESS:
+ // WTF?
+ $this->logger->debug( "$hook is SUCCESS?!" );
+ return StatusValue::newGood();
+
+ case LoginForm::NEED_TOKEN:
+ return StatusValue::newFatal( $msg ?: 'nocookiesforlogin' );
+
+ case LoginForm::WRONG_TOKEN:
+ return StatusValue::newFatal( $msg ?: 'sessionfailure' );
+
+ case LoginForm::NO_NAME:
+ case LoginForm::ILLEGAL:
+ return StatusValue::newFatal( $msg ?: 'noname' );
+
+ case LoginForm::WRONG_PLUGIN_PASS:
+ case LoginForm::WRONG_PASS:
+ return StatusValue::newFatal( $msg ?: 'wrongpassword' );
+
+ case LoginForm::NOT_EXISTS:
+ return StatusValue::newFatal( $msg ?: 'nosuchusershort', wfEscapeWikiText( $user->getName() ) );
+
+ case LoginForm::EMPTY_PASS:
+ return StatusValue::newFatal( $msg ?: 'wrongpasswordempty' );
+
+ case LoginForm::RESET_PASS:
+ return StatusValue::newFatal( $msg ?: 'resetpass_announce' );
+
+ case LoginForm::THROTTLED:
+ $throttle = $this->config->get( 'PasswordAttemptThrottle' );
+ return StatusValue::newFatal(
+ $msg ?: 'login-throttled',
+ \Message::durationParam( $throttle['seconds'] )
+ );
+
+ case LoginForm::USER_BLOCKED:
+ return StatusValue::newFatal(
+ $msg ?: 'login-userblocked', wfEscapeWikiText( $user->getName() )
+ );
+
+ case LoginForm::ABORTED:
+ return StatusValue::newFatal(
+ $msg ?: 'login-abort-generic', wfEscapeWikiText( $user->getName() )
+ );
+
+ case LoginForm::USER_MIGRATED:
+ $error = $msg ?: 'login-migrated-generic';
+ return call_user_func_array( 'StatusValue::newFatal', (array)$error );
+
+ // @codeCoverageIgnoreStart
+ case LoginForm::CREATE_BLOCKED: // Can never happen
+ default:
+ throw new \DomainException( __METHOD__ . ": Unhandled case value from $hook" );
+ }
+ // @codeCoverageIgnoreEnd
+ }
+}
diff --git a/www/wiki/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php b/www/wiki/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php
new file mode 100644
index 00000000..86a6aae0
--- /dev/null
+++ b/www/wiki/includes/auth/LocalPasswordPrimaryAuthenticationProvider.php
@@ -0,0 +1,324 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use User;
+
+/**
+ * A primary authentication provider that uses the password field in the 'user' table.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class LocalPasswordPrimaryAuthenticationProvider
+ extends AbstractPasswordPrimaryAuthenticationProvider
+{
+
+ /** @var bool If true, this instance is for legacy logins only. */
+ protected $loginOnly = false;
+
+ /**
+ * @param array $params Settings
+ * - loginOnly: If true, the local passwords are for legacy logins only:
+ * the local password will be invalidated when authentication is changed
+ * and new users will not have a valid local password set.
+ */
+ public function __construct( $params = [] ) {
+ parent::__construct( $params );
+ $this->loginOnly = !empty( $params['loginOnly'] );
+ }
+
+ protected function getPasswordResetData( $username, $row ) {
+ $now = wfTimestamp();
+ $expiration = wfTimestampOrNull( TS_UNIX, $row->user_password_expires );
+ if ( $expiration === null || $expiration >= $now ) {
+ return null;
+ }
+
+ $grace = $this->config->get( 'PasswordExpireGrace' );
+ if ( $expiration + $grace < $now ) {
+ $data = [
+ 'hard' => true,
+ 'msg' => \Status::newFatal( 'resetpass-expired' )->getMessage(),
+ ];
+ } else {
+ $data = [
+ 'hard' => false,
+ 'msg' => \Status::newFatal( 'resetpass-expired-soft' )->getMessage(),
+ ];
+ }
+
+ return (object)$data;
+ }
+
+ public function beginPrimaryAuthentication( array $reqs ) {
+ $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+ if ( !$req ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ if ( $req->username === null || $req->password === null ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $username = User::getCanonicalName( $req->username, 'usable' );
+ if ( $username === false ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $fields = [
+ 'user_id', 'user_password', 'user_password_expires',
+ ];
+
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow(
+ 'user',
+ $fields,
+ [ 'user_name' => $username ],
+ __METHOD__
+ );
+ if ( !$row ) {
+ // Do not reveal whether its bad username or
+ // bad password to prevent username enumeration
+ // on private wikis. (T134100)
+ return $this->failResponse( $req );
+ }
+
+ $oldRow = clone $row;
+ // Check for *really* old password hashes that don't even have a type
+ // The old hash format was just an md5 hex hash, with no type information
+ if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
+ if ( $this->config->get( 'PasswordSalt' ) ) {
+ $row->user_password = ":B:{$row->user_id}:{$row->user_password}";
+ } else {
+ $row->user_password = ":A:{$row->user_password}";
+ }
+ }
+
+ $status = $this->checkPasswordValidity( $username, $req->password );
+ if ( !$status->isOK() ) {
+ // Fatal, can't log in
+ return AuthenticationResponse::newFail( $status->getMessage() );
+ }
+
+ $pwhash = $this->getPassword( $row->user_password );
+ if ( !$pwhash->equals( $req->password ) ) {
+ if ( $this->config->get( 'LegacyEncoding' ) ) {
+ // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
+ // Check for this with iconv
+ $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $req->password );
+ if ( $cp1252Password === $req->password || !$pwhash->equals( $cp1252Password ) ) {
+ return $this->failResponse( $req );
+ }
+ } else {
+ return $this->failResponse( $req );
+ }
+ }
+
+ // @codeCoverageIgnoreStart
+ if ( $this->getPasswordFactory()->needsUpdate( $pwhash ) ) {
+ $newHash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
+ \DeferredUpdates::addCallableUpdate( function () use ( $newHash, $oldRow ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update(
+ 'user',
+ [ 'user_password' => $newHash->toString() ],
+ [
+ 'user_id' => $oldRow->user_id,
+ 'user_password' => $oldRow->user_password
+ ],
+ __METHOD__
+ );
+ } );
+ }
+ // @codeCoverageIgnoreEnd
+
+ $this->setPasswordResetFlag( $username, $status, $row );
+
+ return AuthenticationResponse::newPass( $username );
+ }
+
+ public function testUserCanAuthenticate( $username ) {
+ $username = User::getCanonicalName( $username, 'usable' );
+ if ( $username === false ) {
+ return false;
+ }
+
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow(
+ 'user',
+ [ 'user_password' ],
+ [ 'user_name' => $username ],
+ __METHOD__
+ );
+ if ( !$row ) {
+ return false;
+ }
+
+ // Check for *really* old password hashes that don't even have a type
+ // The old hash format was just an md5 hex hash, with no type information
+ if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
+ return true;
+ }
+
+ return !$this->getPassword( $row->user_password ) instanceof \InvalidPassword;
+ }
+
+ public function testUserExists( $username, $flags = User::READ_NORMAL ) {
+ $username = User::getCanonicalName( $username, 'usable' );
+ if ( $username === false ) {
+ return false;
+ }
+
+ list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
+ return (bool)wfGetDB( $db )->selectField(
+ [ 'user' ],
+ [ 'user_id' ],
+ [ 'user_name' => $username ],
+ __METHOD__,
+ $options
+ );
+ }
+
+ public function providerAllowsAuthenticationDataChange(
+ AuthenticationRequest $req, $checkData = true
+ ) {
+ // We only want to blank the password if something else will accept the
+ // new authentication data, so return 'ignore' here.
+ if ( $this->loginOnly ) {
+ return \StatusValue::newGood( 'ignored' );
+ }
+
+ if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
+ if ( !$checkData ) {
+ return \StatusValue::newGood();
+ }
+
+ $username = User::getCanonicalName( $req->username, 'usable' );
+ if ( $username !== false ) {
+ $row = wfGetDB( DB_MASTER )->selectRow(
+ 'user',
+ [ 'user_id' ],
+ [ 'user_name' => $username ],
+ __METHOD__
+ );
+ if ( $row ) {
+ $sv = \StatusValue::newGood();
+ if ( $req->password !== null ) {
+ if ( $req->password !== $req->retype ) {
+ $sv->fatal( 'badretype' );
+ } else {
+ $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
+ }
+ }
+ return $sv;
+ }
+ }
+ }
+
+ return \StatusValue::newGood( 'ignored' );
+ }
+
+ public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
+ $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
+ if ( $username === false ) {
+ return;
+ }
+
+ $pwhash = null;
+
+ if ( get_class( $req ) === PasswordAuthenticationRequest::class ) {
+ if ( $this->loginOnly ) {
+ $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
+ $expiry = null;
+ } else {
+ $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
+ $expiry = $this->getNewPasswordExpiry( $username );
+ }
+ }
+
+ if ( $pwhash ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update(
+ 'user',
+ [
+ 'user_password' => $pwhash->toString(),
+ 'user_password_expires' => $dbw->timestampOrNull( $expiry ),
+ ],
+ [ 'user_name' => $username ],
+ __METHOD__
+ );
+ }
+ }
+
+ public function accountCreationType() {
+ return $this->loginOnly ? self::TYPE_NONE : self::TYPE_CREATE;
+ }
+
+ public function testForAccountCreation( $user, $creator, array $reqs ) {
+ $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+
+ $ret = \StatusValue::newGood();
+ if ( !$this->loginOnly && $req && $req->username !== null && $req->password !== null ) {
+ if ( $req->password !== $req->retype ) {
+ $ret->fatal( 'badretype' );
+ } else {
+ $ret->merge(
+ $this->checkPasswordValidity( $user->getName(), $req->password )
+ );
+ }
+ }
+ return $ret;
+ }
+
+ public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
+ if ( $this->accountCreationType() === self::TYPE_NONE ) {
+ throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
+ }
+
+ $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+ if ( $req ) {
+ if ( $req->username !== null && $req->password !== null ) {
+ // Nothing we can do besides claim it, because the user isn't in
+ // the DB yet
+ if ( $req->username !== $user->getName() ) {
+ $req = clone $req;
+ $req->username = $user->getName();
+ }
+ $ret = AuthenticationResponse::newPass( $req->username );
+ $ret->createRequest = $req;
+ return $ret;
+ }
+ }
+ return AuthenticationResponse::newAbstain();
+ }
+
+ public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
+ if ( $this->accountCreationType() === self::TYPE_NONE ) {
+ throw new \BadMethodCallException( 'Shouldn\'t call this when accountCreationType() is NONE' );
+ }
+
+ // Now that the user is in the DB, set the password on it.
+ $this->providerChangeAuthenticationData( $res->createRequest );
+
+ return null;
+ }
+}
diff --git a/www/wiki/includes/auth/PasswordAuthenticationRequest.php b/www/wiki/includes/auth/PasswordAuthenticationRequest.php
new file mode 100644
index 00000000..8550f3e2
--- /dev/null
+++ b/www/wiki/includes/auth/PasswordAuthenticationRequest.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * This is a value object for authentication requests with a username and password
+ * @ingroup Auth
+ * @since 1.27
+ */
+class PasswordAuthenticationRequest extends AuthenticationRequest {
+ /** @var string Password */
+ public $password = null;
+
+ /** @var string Password, again */
+ public $retype = null;
+
+ public function getFieldInfo() {
+ if ( $this->action === AuthManager::ACTION_REMOVE ) {
+ return [];
+ }
+
+ // for password change it's nice to make extra clear that we are asking for the new password
+ $forNewPassword = $this->action === AuthManager::ACTION_CHANGE;
+ $passwordLabel = $forNewPassword ? 'newpassword' : 'userlogin-yourpassword';
+ $retypeLabel = $forNewPassword ? 'retypenew' : 'yourpasswordagain';
+
+ $ret = [
+ 'username' => [
+ 'type' => 'string',
+ 'label' => wfMessage( 'userlogin-yourname' ),
+ 'help' => wfMessage( 'authmanager-username-help' ),
+ ],
+ 'password' => [
+ 'type' => 'password',
+ 'label' => wfMessage( $passwordLabel ),
+ 'help' => wfMessage( 'authmanager-password-help' ),
+ 'sensitive' => true,
+ ],
+ ];
+
+ switch ( $this->action ) {
+ case AuthManager::ACTION_CHANGE:
+ case AuthManager::ACTION_REMOVE:
+ unset( $ret['username'] );
+ break;
+ }
+
+ if ( $this->action !== AuthManager::ACTION_LOGIN ) {
+ $ret['retype'] = [
+ 'type' => 'password',
+ 'label' => wfMessage( $retypeLabel ),
+ 'help' => wfMessage( 'authmanager-retype-help' ),
+ 'sensitive' => true,
+ ];
+ }
+
+ return $ret;
+ }
+
+ public function describeCredentials() {
+ return [
+ 'provider' => wfMessage( 'authmanager-provider-password' ),
+ 'account' => new \RawMessage( '$1', [ $this->username ] ),
+ ];
+ }
+}
diff --git a/www/wiki/includes/auth/PasswordDomainAuthenticationRequest.php b/www/wiki/includes/auth/PasswordDomainAuthenticationRequest.php
new file mode 100644
index 00000000..3db7e212
--- /dev/null
+++ b/www/wiki/includes/auth/PasswordDomainAuthenticationRequest.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * This is a value object for authentication requests with a username, password, and domain
+ * @ingroup Auth
+ * @since 1.27
+ */
+class PasswordDomainAuthenticationRequest extends PasswordAuthenticationRequest {
+ /** @var string[] Domains available */
+ private $domainList;
+
+ /** @var string Domain */
+ public $domain = null;
+
+ /**
+ * @param string[] $domainList List of available domains
+ */
+ public function __construct( array $domainList ) {
+ $this->domainList = $domainList;
+ }
+
+ public function getFieldInfo() {
+ $ret = parent::getFieldInfo();
+
+ // Only add a domain field if we have the username field included
+ if ( isset( $ret['username'] ) ) {
+ $ret['domain'] = [
+ 'type' => 'select',
+ 'options' => [],
+ 'label' => wfMessage( 'yourdomainname' ),
+ 'help' => wfMessage( 'authmanager-domain-help' ),
+ ];
+ foreach ( $this->domainList as $domain ) {
+ $ret['domain']['options'][$domain] = new \RawMessage( '$1', [ $domain ] );
+ }
+ }
+
+ return $ret;
+ }
+
+ public function describeCredentials() {
+ return [
+ 'provider' => wfMessage( 'authmanager-provider-password-domain' ),
+ 'account' => wfMessage(
+ 'authmanager-account-password-domain', [ $this->username, $this->domain ]
+ ),
+ ];
+ }
+
+ /**
+ * @codeCoverageIgnore
+ * @param array $data
+ * @return AuthenticationRequest|static
+ */
+ public static function __set_state( $data ) {
+ $ret = new static( $data['domainList'] );
+ foreach ( $data as $k => $v ) {
+ if ( $k !== 'domainList' ) {
+ $ret->$k = $v;
+ }
+ }
+ return $ret;
+ }
+}
diff --git a/www/wiki/includes/auth/PreAuthenticationProvider.php b/www/wiki/includes/auth/PreAuthenticationProvider.php
new file mode 100644
index 00000000..8590cbd1
--- /dev/null
+++ b/www/wiki/includes/auth/PreAuthenticationProvider.php
@@ -0,0 +1,148 @@
+<?php
+/**
+ * Pre-authentication provider interface
+ *
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use StatusValue;
+use User;
+
+/**
+ * A pre-authentication provider can prevent authentication early on.
+ *
+ * A PreAuthenticationProvider is used to supply arbitrary checks to be
+ * performed before the PrimaryAuthenticationProviders are consulted during the
+ * login / account creation / account linking process. Possible uses include
+ * checking that a per-IP throttle has not been reached or that a captcha has been solved.
+ *
+ * This interface also provides callbacks that are invoked after login / account creation
+ * / account linking succeeded or failed.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
+ */
+interface PreAuthenticationProvider extends AuthenticationProvider {
+
+ /**
+ * Determine whether an authentication may begin
+ *
+ * Called from AuthManager::beginAuthentication()
+ *
+ * @param AuthenticationRequest[] $reqs
+ * @return StatusValue
+ */
+ public function testForAuthentication( array $reqs );
+
+ /**
+ * Post-login callback
+ *
+ * This will be called at the end of a login attempt. It will not be called for unfinished
+ * login attempts that fail by the session timing out.
+ *
+ * @note Under certain circumstances, this can be called even when testForAuthentication
+ * was not; see AuthenticationRequest::$loginRequest.
+ * @param User|null $user User that was attempted to be logged in, if known.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param AuthenticationResponse $response Authentication response that will be returned
+ * (PASS or FAIL)
+ */
+ public function postAuthentication( $user, AuthenticationResponse $response );
+
+ /**
+ * Determine whether an account creation may begin
+ *
+ * Called from AuthManager::beginAccountCreation()
+ *
+ * @note No need to test if the account exists, AuthManager checks that
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs
+ * @return StatusValue
+ */
+ public function testForAccountCreation( $user, $creator, array $reqs );
+
+ /**
+ * Determine whether an account may be created
+ *
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param bool|string $autocreate False if this is not an auto-creation, or
+ * the source of the auto-creation passed to AuthManager::autoCreateUser().
+ * @param array $options
+ * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
+ * - creating: (bool) If false (or missing), this call is only testing if
+ * a user could be created. If set, this (non-autocreation) is for
+ * actually creating an account and will be followed by a call to
+ * testForAccountCreation(). In this case, the provider might return
+ * StatusValue::newGood() here and let the later call to
+ * testForAccountCreation() do a more thorough test.
+ * @return StatusValue
+ */
+ public function testUserForCreation( $user, $autocreate, array $options = [] );
+
+ /**
+ * Post-creation callback
+ *
+ * This will be called at the end of an account creation attempt. It will not be called if
+ * the account creation process results in a session timeout (possibly after a successful
+ * user creation, while a secondary provider is waiting for a response).
+ *
+ * @param User $user User that was attempted to be created.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationResponse $response Authentication response that will be returned
+ * (PASS or FAIL)
+ */
+ public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
+
+ /**
+ * Determine whether an account may linked to another authentication method
+ *
+ * @param User $user User being linked.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @return StatusValue
+ */
+ public function testForAccountLink( $user );
+
+ /**
+ * Post-link callback
+ *
+ * This will be called at the end of an account linking attempt.
+ *
+ * @param User $user User that was attempted to be linked.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param AuthenticationResponse $response Authentication response that will be returned
+ * (PASS or FAIL)
+ */
+ public function postAccountLink( $user, AuthenticationResponse $response );
+
+}
diff --git a/www/wiki/includes/auth/PrimaryAuthenticationProvider.php b/www/wiki/includes/auth/PrimaryAuthenticationProvider.php
new file mode 100644
index 00000000..5d82f899
--- /dev/null
+++ b/www/wiki/includes/auth/PrimaryAuthenticationProvider.php
@@ -0,0 +1,400 @@
+<?php
+/**
+ * Primary authentication provider interface
+ *
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use StatusValue;
+use User;
+
+/**
+ * A primary authentication provider is responsible for associating the submitted
+ * authentication data with a MediaWiki account.
+ *
+ * When multiple primary authentication providers are configured for a site, they
+ * act as alternatives; the first one that recognizes the data will handle it,
+ * and further primary providers are not called (although they all get a chance
+ * to prevent actions).
+ *
+ * For login, the PrimaryAuthenticationProvider takes form data and determines
+ * which authenticated user (if any) corresponds to that form data. It might
+ * do this on the basis of a username and password in that data, or by
+ * interacting with an external authentication service (e.g. using OpenID),
+ * or by some other mechanism.
+ *
+ * (A PrimaryAuthenticationProvider would not be appropriate for something like
+ * HTTP authentication, OAuth, or SSL client certificates where each HTTP
+ * request contains all the information needed to identify the user. In that
+ * case you'll want to be looking at a \MediaWiki\Session\SessionProvider
+ * instead.)
+ *
+ * For account creation, the PrimaryAuthenticationProvider takes form data and
+ * stores some authentication details which will allow it to verify a login by
+ * that user in the future. This might for example involve saving it in the
+ * database in a table that can be joined to the user table, or sending it to
+ * some external service for account creation, or authenticating the user with
+ * some remote service and then recording that the remote identity is linked to
+ * the local account.
+ * The creation of the local user (i.e. calling User::addToDatabase()) is handled
+ * by AuthManager once the primary authentication provider returns a PASS
+ * from begin/continueAccountCreation; do not try to do it yourself.
+ *
+ * For account linking, the PrimaryAuthenticationProvider verifies the user's
+ * identity at some external service (typically by redirecting the user and
+ * asking the external service to verify) and then records which local account
+ * is linked to which remote accounts. It should keep track of this and be able
+ * to enumerate linked accounts via getAuthenticationRequests(ACTION_REMOVE).
+ *
+ * This interface also provides methods for changing authentication data such
+ * as passwords, and callbacks that are invoked after login / account creation
+ * / account linking succeeded or failed.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
+ */
+interface PrimaryAuthenticationProvider extends AuthenticationProvider {
+ /** Provider can create accounts */
+ const TYPE_CREATE = 'create';
+ /** Provider can link to existing accounts elsewhere */
+ const TYPE_LINK = 'link';
+ /** Provider cannot create or link to accounts */
+ const TYPE_NONE = 'none';
+
+ /**
+ * @inheritDoc
+ *
+ * Of the requests returned by this method, exactly one should have
+ * {@link AuthenticationRequest::$required} set to REQUIRED.
+ */
+ public function getAuthenticationRequests( $action, array $options );
+
+ /**
+ * Start an authentication flow
+ *
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user is authenticated. Secondary providers will now run.
+ * - FAIL: The user is not authenticated. Fail the authentication process.
+ * - ABSTAIN: These $reqs are not handled. Some other primary provider may handle it.
+ * - UI: The $reqs are accepted, no other primary provider will run.
+ * Additional AuthenticationRequests are needed to complete the process.
+ * - REDIRECT: The $reqs are accepted, no other primary provider will run.
+ * Redirection to a third party is needed to complete the process.
+ */
+ public function beginPrimaryAuthentication( array $reqs );
+
+ /**
+ * Continue an authentication flow
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user is authenticated. Secondary providers will now run.
+ * - FAIL: The user is not authenticated. Fail the authentication process.
+ * - UI: Additional AuthenticationRequests are needed to complete the process.
+ * - REDIRECT: Redirection to a third party is needed to complete the process.
+ */
+ public function continuePrimaryAuthentication( array $reqs );
+
+ /**
+ * Post-login callback
+ *
+ * This will be called at the end of any login attempt, regardless of whether this provider was
+ * the one that handled it. It will not be called for unfinished login attempts that fail by
+ * the session timing out.
+ *
+ * @param User|null $user User that was attempted to be logged in, if known.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param AuthenticationResponse $response Authentication response that will be returned
+ * (PASS or FAIL)
+ */
+ public function postAuthentication( $user, AuthenticationResponse $response );
+
+ /**
+ * Test whether the named user exists
+ *
+ * Single-sign-on providers can use this to reserve a username for autocreation.
+ *
+ * @param string $username MediaWiki username
+ * @param int $flags Bitfield of User:READ_* constants
+ * @return bool
+ */
+ public function testUserExists( $username, $flags = User::READ_NORMAL );
+
+ /**
+ * Test whether the named user can authenticate with this provider
+ *
+ * Should return true if the provider has any data for this user which can be used to
+ * authenticate it, even if the user is temporarily prevented from authentication somehow.
+ *
+ * @param string $username MediaWiki username
+ * @return bool
+ */
+ public function testUserCanAuthenticate( $username );
+
+ /**
+ * Normalize the username for authentication
+ *
+ * Any two inputs that would result in the same user being authenticated
+ * should return the same string here, while inputs that would result in
+ * different users should return different strings.
+ *
+ * If possible, the best thing to do here is to return the canonicalized
+ * name of the local user account that would be used. If not, return
+ * something that would be invalid as a local username (e.g. wrap an email
+ * address in "<>", or append "#servicename" to the username passed to a
+ * third-party service).
+ *
+ * If the provider doesn't use a username at all in its
+ * AuthenticationRequests, return null. If the name is syntactically
+ * invalid, it's probably best to return null.
+ *
+ * @param string $username
+ * @return string|null
+ */
+ public function providerNormalizeUsername( $username );
+
+ /**
+ * Revoke the user's credentials
+ *
+ * This may cause the user to no longer exist for the provider, or the user
+ * may continue to exist in a "disabled" state.
+ *
+ * The intention is that the named account will never again be usable for
+ * normal login (i.e. there is no way to undo the revocation of access).
+ *
+ * @param string $username
+ */
+ public function providerRevokeAccessForUser( $username );
+
+ /**
+ * Determine whether a property can change
+ * @see AuthManager::allowsPropertyChange()
+ * @param string $property
+ * @return bool
+ */
+ public function providerAllowsPropertyChange( $property );
+
+ /**
+ * Validate a change of authentication data (e.g. passwords)
+ *
+ * Return StatusValue::newGood( 'ignored' ) if you don't support this
+ * AuthenticationRequest type.
+ *
+ * @param AuthenticationRequest $req
+ * @param bool $checkData If false, $req hasn't been loaded from the
+ * submission so checks on user-submitted fields should be skipped.
+ * $req->username is considered user-submitted for this purpose, even
+ * if it cannot be changed via $req->loadFromSubmission.
+ * @return StatusValue
+ */
+ public function providerAllowsAuthenticationDataChange(
+ AuthenticationRequest $req, $checkData = true
+ );
+
+ /**
+ * Change or remove authentication data (e.g. passwords)
+ *
+ * If $req was returned for AuthManager::ACTION_CHANGE, the corresponding
+ * credentials should result in a successful login in the future.
+ *
+ * If $req was returned for AuthManager::ACTION_REMOVE, the corresponding
+ * credentials should no longer result in a successful login.
+ *
+ * It can be assumed that providerAllowsAuthenticationDataChange with $checkData === true
+ * was called before this, and passed. This method should never fail (other than throwing an
+ * exception).
+ *
+ * @param AuthenticationRequest $req
+ */
+ public function providerChangeAuthenticationData( AuthenticationRequest $req );
+
+ /**
+ * Fetch the account-creation type
+ * @return string One of the TYPE_* constants
+ */
+ public function accountCreationType();
+
+ /**
+ * Determine whether an account creation may begin
+ *
+ * Called from AuthManager::beginAccountCreation()
+ *
+ * @note No need to test if the account exists, AuthManager checks that
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs
+ * @return StatusValue
+ */
+ public function testForAccountCreation( $user, $creator, array $reqs );
+
+ /**
+ * Start an account creation flow
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user may be created. Secondary providers will now run.
+ * - FAIL: The user may not be created. Fail the creation process.
+ * - ABSTAIN: These $reqs are not handled. Some other primary provider may handle it.
+ * - UI: The $reqs are accepted, no other primary provider will run.
+ * Additional AuthenticationRequests are needed to complete the process.
+ * - REDIRECT: The $reqs are accepted, no other primary provider will run.
+ * Redirection to a third party is needed to complete the process.
+ */
+ public function beginPrimaryAccountCreation( $user, $creator, array $reqs );
+
+ /**
+ * Continue an account creation flow
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user may be created. Secondary providers will now run.
+ * - FAIL: The user may not be created. Fail the creation process.
+ * - UI: Additional AuthenticationRequests are needed to complete the process.
+ * - REDIRECT: Redirection to a third party is needed to complete the process.
+ */
+ public function continuePrimaryAccountCreation( $user, $creator, array $reqs );
+
+ /**
+ * Post-creation callback
+ *
+ * Called after the user is added to the database, before secondary
+ * authentication providers are run. Only called if this provider was the one that issued
+ * a PASS.
+ *
+ * @param User $user User being created (has been added to the database now).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationResponse $response PASS response returned earlier
+ * @return string|null 'newusers' log subtype to use for logging the
+ * account creation. If null, either 'create' or 'create2' will be used
+ * depending on $creator.
+ */
+ public function finishAccountCreation( $user, $creator, AuthenticationResponse $response );
+
+ /**
+ * Post-creation callback
+ *
+ * This will be called at the end of any account creation attempt, regardless of whether this
+ * provider was the one that handled it. It will not be called if the account creation process
+ * results in a session timeout (possibly after a successful user creation, while a secondary
+ * provider is waiting for a response).
+ *
+ * @param User $user User that was attempted to be created.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationResponse $response Authentication response that will be returned
+ * (PASS or FAIL)
+ */
+ public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
+
+ /**
+ * Determine whether an account may be created
+ *
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param bool|string $autocreate False if this is not an auto-creation, or
+ * the source of the auto-creation passed to AuthManager::autoCreateUser().
+ * @param array $options
+ * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
+ * - creating: (bool) If false (or missing), this call is only testing if
+ * a user could be created. If set, this (non-autocreation) is for
+ * actually creating an account and will be followed by a call to
+ * testForAccountCreation(). In this case, the provider might return
+ * StatusValue::newGood() here and let the later call to
+ * testForAccountCreation() do a more thorough test.
+ * @return StatusValue
+ */
+ public function testUserForCreation( $user, $autocreate, array $options = [] );
+
+ /**
+ * Post-auto-creation callback
+ * @param User $user User being created (has been added to the database now).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param string $source The source of the auto-creation passed to
+ * AuthManager::autoCreateUser().
+ */
+ public function autoCreatedAccount( $user, $source );
+
+ /**
+ * Start linking an account to an existing user
+ * @param User $user User being linked.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user is linked.
+ * - FAIL: The user is not linked. Fail the linking process.
+ * - ABSTAIN: These $reqs are not handled. Some other primary provider may handle it.
+ * - UI: The $reqs are accepted, no other primary provider will run.
+ * Additional AuthenticationRequests are needed to complete the process.
+ * - REDIRECT: The $reqs are accepted, no other primary provider will run.
+ * Redirection to a third party is needed to complete the process.
+ */
+ public function beginPrimaryAccountLink( $user, array $reqs );
+
+ /**
+ * Continue linking an account to an existing user
+ * @param User $user User being linked.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse Expected responses:
+ * - PASS: The user is linked.
+ * - FAIL: The user is not linked. Fail the linking process.
+ * - UI: Additional AuthenticationRequests are needed to complete the process.
+ * - REDIRECT: Redirection to a third party is needed to complete the process.
+ */
+ public function continuePrimaryAccountLink( $user, array $reqs );
+
+ /**
+ * Post-link callback
+ *
+ * This will be called at the end of any account linking attempt, regardless of whether this
+ * provider was the one that handled it.
+ *
+ * @param User $user User that was attempted to be linked.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param AuthenticationResponse $response Authentication response that will be returned
+ * (PASS or FAIL)
+ */
+ public function postAccountLink( $user, AuthenticationResponse $response );
+
+}
diff --git a/www/wiki/includes/auth/RememberMeAuthenticationRequest.php b/www/wiki/includes/auth/RememberMeAuthenticationRequest.php
new file mode 100644
index 00000000..06060b16
--- /dev/null
+++ b/www/wiki/includes/auth/RememberMeAuthenticationRequest.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\Session\SessionManager;
+use MediaWiki\Session\SessionProvider;
+
+/**
+ * This is an authentication request added by AuthManager to show a "remember
+ * me" checkbox. When checked, it will take more time for the authenticated session to expire.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class RememberMeAuthenticationRequest extends AuthenticationRequest {
+
+ public $required = self::OPTIONAL;
+
+ /** @var int How long the user will be remembered, in seconds */
+ protected $expiration = null;
+
+ /** @var bool */
+ public $rememberMe = false;
+
+ public function __construct() {
+ /** @var SessionProvider $provider */
+ $provider = SessionManager::getGlobalSession()->getProvider();
+ $this->expiration = $provider->getRememberUserDuration();
+ }
+
+ public function getFieldInfo() {
+ if ( !$this->expiration ) {
+ return [];
+ }
+
+ $expirationDays = ceil( $this->expiration / ( 3600 * 24 ) );
+ return [
+ 'rememberMe' => [
+ 'type' => 'checkbox',
+ 'label' => wfMessage( 'userlogin-remembermypassword' )->numParams( $expirationDays ),
+ 'help' => wfMessage( 'authmanager-userlogin-remembermypassword-help' ),
+ 'optional' => true,
+ 'skippable' => true,
+ ]
+ ];
+ }
+}
diff --git a/www/wiki/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php b/www/wiki/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php
new file mode 100644
index 00000000..45ac3aa0
--- /dev/null
+++ b/www/wiki/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * Reset the local password, if signalled via $this->manager->setAuthenticationSessionData()
+ *
+ * The authentication data key is 'reset-pass'; the data is an object with the
+ * following properties:
+ * - msg: Message object to display to the user
+ * - hard: Boolean, if true the reset cannot be skipped.
+ * - req: Optional PasswordAuthenticationRequest to use to actually reset the
+ * password. Won't be displayed to the user.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class ResetPasswordSecondaryAuthenticationProvider extends AbstractSecondaryAuthenticationProvider {
+
+ public function getAuthenticationRequests( $action, array $options ) {
+ return [];
+ }
+
+ public function beginSecondaryAuthentication( $user, array $reqs ) {
+ return $this->tryReset( $user, $reqs );
+ }
+
+ public function continueSecondaryAuthentication( $user, array $reqs ) {
+ return $this->tryReset( $user, $reqs );
+ }
+
+ public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+ return $this->tryReset( $user, $reqs );
+ }
+
+ public function continueSecondaryAccountCreation( $user, $creator, array $reqs ) {
+ return $this->tryReset( $user, $reqs );
+ }
+
+ /**
+ * Try to reset the password
+ * @param \User $user
+ * @param AuthenticationRequest[] $reqs
+ * @return AuthenticationResponse
+ */
+ protected function tryReset( \User $user, array $reqs ) {
+ $data = $this->manager->getAuthenticationSessionData( 'reset-pass' );
+ if ( !$data ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ if ( is_array( $data ) ) {
+ $data = (object)$data;
+ }
+ if ( !is_object( $data ) ) {
+ throw new \UnexpectedValueException( 'reset-pass is not valid' );
+ }
+
+ if ( !isset( $data->msg ) ) {
+ throw new \UnexpectedValueException( 'reset-pass msg is missing' );
+ } elseif ( !$data->msg instanceof \Message ) {
+ throw new \UnexpectedValueException( 'reset-pass msg is not valid' );
+ } elseif ( !isset( $data->hard ) ) {
+ throw new \UnexpectedValueException( 'reset-pass hard is missing' );
+ } elseif ( isset( $data->req ) && (
+ !$data->req instanceof PasswordAuthenticationRequest ||
+ !array_key_exists( 'retype', $data->req->getFieldInfo() )
+ ) ) {
+ throw new \UnexpectedValueException( 'reset-pass req is not valid' );
+ }
+
+ if ( !$data->hard ) {
+ $req = ButtonAuthenticationRequest::getRequestByName( $reqs, 'skipReset' );
+ if ( $req ) {
+ $this->manager->removeAuthenticationSessionData( 'reset-pass' );
+ return AuthenticationResponse::newPass();
+ }
+ }
+
+ $needReq = isset( $data->req ) ? $data->req : new PasswordAuthenticationRequest();
+ if ( !$needReq->action ) {
+ $needReq->action = AuthManager::ACTION_CHANGE;
+ }
+ $needReq->required = $data->hard ? AuthenticationRequest::REQUIRED
+ : AuthenticationRequest::OPTIONAL;
+ $needReqs = [ $needReq ];
+ if ( !$data->hard ) {
+ $needReqs[] = new ButtonAuthenticationRequest(
+ 'skipReset',
+ wfMessage( 'authprovider-resetpass-skip-label' ),
+ wfMessage( 'authprovider-resetpass-skip-help' )
+ );
+ }
+
+ $req = AuthenticationRequest::getRequestByClass( $reqs, get_class( $needReq ) );
+ if ( !$req || !array_key_exists( 'retype', $req->getFieldInfo() ) ) {
+ return AuthenticationResponse::newUI( $needReqs, $data->msg, 'warning' );
+ }
+
+ if ( $req->password !== $req->retype ) {
+ return AuthenticationResponse::newUI( $needReqs, new \Message( 'badretype' ), 'error' );
+ }
+
+ $req->username = $user->getName();
+ $status = $this->manager->allowsAuthenticationDataChange( $req );
+ if ( !$status->isGood() ) {
+ return AuthenticationResponse::newUI( $needReqs, $status->getMessage(), 'error' );
+ }
+ $this->manager->changeAuthenticationData( $req );
+
+ $this->manager->removeAuthenticationSessionData( 'reset-pass' );
+ return AuthenticationResponse::newPass();
+ }
+}
diff --git a/www/wiki/includes/auth/SecondaryAuthenticationProvider.php b/www/wiki/includes/auth/SecondaryAuthenticationProvider.php
new file mode 100644
index 00000000..c55e65d5
--- /dev/null
+++ b/www/wiki/includes/auth/SecondaryAuthenticationProvider.php
@@ -0,0 +1,258 @@
+<?php
+/**
+ * Secondary authentication provider interface
+ *
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use StatusValue;
+use User;
+
+/**
+ * A secondary provider mostly acts when the submitted authentication data has
+ * already been associated to a MediaWiki user account.
+ *
+ * For login, a secondary provider performs additional authentication steps
+ * after a PrimaryAuthenticationProvider has identified which MediaWiki user is
+ * trying to log in. For example, it might implement a password reset, request
+ * the second factor for two-factor auth, or prevent the login if the account is blocked.
+ *
+ * For account creation, a secondary provider performs optional extra steps after
+ * a PrimaryAuthenticationProvider has created the user; for example, it can collect
+ * further user information such as a biography.
+ *
+ * (For account linking, secondary providers are not involved.)
+ *
+ * This interface also provides methods for changing authentication data such
+ * as a second-factor token, and callbacks that are invoked after login / account creation
+ * succeeded or failed.
+ *
+ * @ingroup Auth
+ * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
+ */
+interface SecondaryAuthenticationProvider extends AuthenticationProvider {
+
+ /**
+ * Start an authentication flow
+ *
+ * Note that this may be called for a user even if
+ * beginSecondaryAccountCreation() was never called. The module should take
+ * the opportunity to do any necessary setup in that case.
+ *
+ * @param User $user User being authenticated. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs
+ * @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 );
+
+ /**
+ * 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
+ * @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 );
+
+ /**
+ * Post-login callback
+ *
+ * This will be called at the end of a login attempt. It will not be called for unfinished
+ * login attempts that fail by the session timing out.
+ *
+ * @param User|null $user User that was attempted to be logged in, if known.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param AuthenticationResponse $response Authentication response that will be returned
+ * (PASS or FAIL)
+ */
+ public function postAuthentication( $user, AuthenticationResponse $response );
+
+ /**
+ * Revoke the user's credentials
+ *
+ * This may cause the user to no longer exist for the provider, or the user
+ * may continue to exist in a "disabled" state.
+ *
+ * The intention is that the named account will never again be usable for
+ * normal login (i.e. there is no way to undo the revocation of access).
+ *
+ * @param string $username
+ */
+ public function providerRevokeAccessForUser( $username );
+
+ /**
+ * Determine whether a property can change
+ * @see AuthManager::allowsPropertyChange()
+ * @param string $property
+ * @return bool
+ */
+ public function providerAllowsPropertyChange( $property );
+
+ /**
+ * Validate a change of authentication data (e.g. passwords)
+ *
+ * Return StatusValue::newGood( 'ignored' ) if you don't support this
+ * AuthenticationRequest type.
+ *
+ * @param AuthenticationRequest $req
+ * @param bool $checkData If false, $req hasn't been loaded from the
+ * submission so checks on user-submitted fields should be skipped.
+ * $req->username is considered user-submitted for this purpose, even
+ * if it cannot be changed via $req->loadFromSubmission.
+ * @return StatusValue
+ */
+ public function providerAllowsAuthenticationDataChange(
+ AuthenticationRequest $req, $checkData = true
+ );
+
+ /**
+ * Change or remove authentication data (e.g. passwords)
+ *
+ * If $req was returned for AuthManager::ACTION_CHANGE, the corresponding
+ * credentials should result in a successful login in the future.
+ *
+ * If $req was returned for AuthManager::ACTION_REMOVE, the corresponding
+ * credentials should no longer result in a successful login.
+ *
+ * It can be assumed that providerAllowsAuthenticationDataChange with $checkData === true
+ * was called before this, and passed. This method should never fail (other than throwing an
+ * exception).
+ *
+ * @param AuthenticationRequest $req
+ */
+ public function providerChangeAuthenticationData( AuthenticationRequest $req );
+
+ /**
+ * Determine whether an account creation may begin
+ *
+ * Called from AuthManager::beginAccountCreation()
+ *
+ * @note No need to test if the account exists, AuthManager checks that
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs
+ * @return StatusValue
+ */
+ public function testForAccountCreation( $user, $creator, array $reqs );
+
+ /**
+ * Start an account creation flow
+ *
+ * @note There is no guarantee this will be called in a successful account
+ * creation process as the user can just abandon the process at any time
+ * after the primary provider has issued a PASS and still have a valid
+ * account. Be prepared to handle any database inconsistencies that result
+ * from this or continueSecondaryAccountCreation() not being called.
+ * @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 User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs
+ * @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, $creator, array $reqs );
+
+ /**
+ * Continue an authentication 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 User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationRequest[] $reqs
+ * @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 continueSecondaryAccountCreation( $user, $creator, array $reqs );
+
+ /**
+ * Post-creation callback
+ *
+ * This will be called at the end of an account creation attempt. It will not be called if
+ * the account creation process results in a session timeout (possibly after a successful
+ * user creation, while a secondary provider is waiting for a response).
+ *
+ * @param User $user User that was attempted to be created.
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param User $creator User doing the creation. This may become a
+ * "UserValue" in the future, or User may be refactored into such.
+ * @param AuthenticationResponse $response Authentication response that will be returned
+ * (PASS or FAIL)
+ */
+ public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
+
+ /**
+ * Determine whether an account may be created
+ *
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param bool|string $autocreate False if this is not an auto-creation, or
+ * the source of the auto-creation passed to AuthManager::autoCreateUser().
+ * @param array $options
+ * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
+ * - creating: (bool) If false (or missing), this call is only testing if
+ * a user could be created. If set, this (non-autocreation) is for
+ * actually creating an account and will be followed by a call to
+ * testForAccountCreation(). In this case, the provider might return
+ * StatusValue::newGood() here and let the later call to
+ * testForAccountCreation() do a more thorough test.
+ * @return StatusValue
+ */
+ public function testUserForCreation( $user, $autocreate, array $options = [] );
+
+ /**
+ * Post-auto-creation callback
+ * @param User $user User being created (has been added to the database now).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @param string $source The source of the auto-creation passed to
+ * AuthManager::autoCreateUser().
+ */
+ public function autoCreatedAccount( $user, $source );
+
+}
diff --git a/www/wiki/includes/auth/TemporaryPasswordAuthenticationRequest.php b/www/wiki/includes/auth/TemporaryPasswordAuthenticationRequest.php
new file mode 100644
index 00000000..bc7c779d
--- /dev/null
+++ b/www/wiki/includes/auth/TemporaryPasswordAuthenticationRequest.php
@@ -0,0 +1,101 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * This represents the intention to set a temporary password for the user.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class TemporaryPasswordAuthenticationRequest extends AuthenticationRequest {
+ /** @var string|null Temporary password */
+ public $password;
+
+ /** @var bool Email password to the user. */
+ public $mailpassword = false;
+
+ /** @var string Username or IP address of the caller */
+ public $caller;
+
+ public function getFieldInfo() {
+ return [
+ 'mailpassword' => [
+ 'type' => 'checkbox',
+ 'label' => wfMessage( 'createaccountmail' ),
+ 'help' => wfMessage( 'createaccountmail-help' ),
+ ],
+ ];
+ }
+
+ /**
+ * @param string|null $password
+ */
+ public function __construct( $password = null ) {
+ $this->password = $password;
+ if ( $password ) {
+ $this->mailpassword = true;
+ }
+ }
+
+ /**
+ * Return an instance with a new, random password
+ * @return TemporaryPasswordAuthenticationRequest
+ */
+ public static function newRandom() {
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+
+ // get the min password length
+ $minLength = $config->get( 'MinimalPasswordLength' );
+ $policy = $config->get( 'PasswordPolicy' );
+ foreach ( $policy['policies'] as $p ) {
+ if ( isset( $p['MinimalPasswordLength'] ) ) {
+ $minLength = max( $minLength, $p['MinimalPasswordLength'] );
+ }
+ if ( isset( $p['MinimalPasswordLengthToLogin'] ) ) {
+ $minLength = max( $minLength, $p['MinimalPasswordLengthToLogin'] );
+ }
+ }
+
+ $password = \PasswordFactory::generateRandomPasswordString( $minLength );
+
+ return new self( $password );
+ }
+
+ /**
+ * Return an instance with an invalid password
+ * @return TemporaryPasswordAuthenticationRequest
+ */
+ public static function newInvalid() {
+ $request = new self( null );
+ return $request;
+ }
+
+ public function describeCredentials() {
+ return [
+ 'provider' => wfMessage( 'authmanager-provider-temporarypassword' ),
+ 'account' => new \RawMessage( '$1', [ $this->username ] ),
+ ] + parent::describeCredentials();
+ }
+
+}
diff --git a/www/wiki/includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php b/www/wiki/includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php
new file mode 100644
index 00000000..4a2d0094
--- /dev/null
+++ b/www/wiki/includes/auth/TemporaryPasswordPrimaryAuthenticationProvider.php
@@ -0,0 +1,477 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use User;
+
+/**
+ * A primary authentication provider that uses the temporary password field in
+ * the 'user' table.
+ *
+ * A successful login will force a password reset.
+ *
+ * @note For proper operation, this should generally come before any other
+ * password-based authentication providers.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class TemporaryPasswordPrimaryAuthenticationProvider
+ extends AbstractPasswordPrimaryAuthenticationProvider
+{
+ /** @var bool */
+ protected $emailEnabled = null;
+
+ /** @var int */
+ protected $newPasswordExpiry = null;
+
+ /** @var int */
+ protected $passwordReminderResendTime = null;
+
+ /**
+ * @param array $params
+ * - emailEnabled: (bool) must be true for the option to email passwords to be present
+ * - newPasswordExpiry: (int) expiraton time of temporary passwords, in seconds
+ * - passwordReminderResendTime: (int) cooldown period in hours until a password reminder can
+ * be sent to the same user again,
+ */
+ public function __construct( $params = [] ) {
+ parent::__construct( $params );
+
+ if ( isset( $params['emailEnabled'] ) ) {
+ $this->emailEnabled = (bool)$params['emailEnabled'];
+ }
+ if ( isset( $params['newPasswordExpiry'] ) ) {
+ $this->newPasswordExpiry = (int)$params['newPasswordExpiry'];
+ }
+ if ( isset( $params['passwordReminderResendTime'] ) ) {
+ $this->passwordReminderResendTime = $params['passwordReminderResendTime'];
+ }
+ }
+
+ public function setConfig( \Config $config ) {
+ parent::setConfig( $config );
+
+ if ( $this->emailEnabled === null ) {
+ $this->emailEnabled = $this->config->get( 'EnableEmail' );
+ }
+ if ( $this->newPasswordExpiry === null ) {
+ $this->newPasswordExpiry = $this->config->get( 'NewPasswordExpiry' );
+ }
+ if ( $this->passwordReminderResendTime === null ) {
+ $this->passwordReminderResendTime = $this->config->get( 'PasswordReminderResendTime' );
+ }
+ }
+
+ protected function getPasswordResetData( $username, $data ) {
+ // Always reset
+ return (object)[
+ 'msg' => wfMessage( 'resetpass-temp-emailed' ),
+ 'hard' => true,
+ ];
+ }
+
+ public function getAuthenticationRequests( $action, array $options ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_LOGIN:
+ return [ new PasswordAuthenticationRequest() ];
+
+ case AuthManager::ACTION_CHANGE:
+ return [ TemporaryPasswordAuthenticationRequest::newRandom() ];
+
+ case AuthManager::ACTION_CREATE:
+ if ( isset( $options['username'] ) && $this->emailEnabled ) {
+ // Creating an account for someone else
+ return [ TemporaryPasswordAuthenticationRequest::newRandom() ];
+ } else {
+ // It's not terribly likely that an anonymous user will
+ // be creating an account for someone else.
+ return [];
+ }
+
+ case AuthManager::ACTION_REMOVE:
+ return [ new TemporaryPasswordAuthenticationRequest ];
+
+ default:
+ return [];
+ }
+ }
+
+ public function beginPrimaryAuthentication( array $reqs ) {
+ $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
+ if ( !$req || $req->username === null || $req->password === null ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $username = User::getCanonicalName( $req->username, 'usable' );
+ if ( $username === false ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow(
+ 'user',
+ [
+ 'user_id', 'user_newpassword', 'user_newpass_time',
+ ],
+ [ 'user_name' => $username ],
+ __METHOD__
+ );
+ if ( !$row ) {
+ return AuthenticationResponse::newAbstain();
+ }
+
+ $status = $this->checkPasswordValidity( $username, $req->password );
+ if ( !$status->isOK() ) {
+ // Fatal, can't log in
+ return AuthenticationResponse::newFail( $status->getMessage() );
+ }
+
+ $pwhash = $this->getPassword( $row->user_newpassword );
+ if ( !$pwhash->equals( $req->password ) ) {
+ return $this->failResponse( $req );
+ }
+
+ if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
+ return $this->failResponse( $req );
+ }
+
+ // Add an extra log entry since a temporary password is
+ // an unusual way to log in, so its important to keep track
+ // of in case of abuse.
+ $this->logger->info( "{user} successfully logged in using temp password",
+ [
+ 'user' => $username,
+ 'requestIP' => $this->manager->getRequest()->getIP()
+ ]
+ );
+
+ $this->setPasswordResetFlag( $username, $status );
+
+ return AuthenticationResponse::newPass( $username );
+ }
+
+ public function testUserCanAuthenticate( $username ) {
+ $username = User::getCanonicalName( $username, 'usable' );
+ if ( $username === false ) {
+ return false;
+ }
+
+ $dbr = wfGetDB( DB_REPLICA );
+ $row = $dbr->selectRow(
+ 'user',
+ [ 'user_newpassword', 'user_newpass_time' ],
+ [ 'user_name' => $username ],
+ __METHOD__
+ );
+ if ( !$row ) {
+ return false;
+ }
+
+ if ( $this->getPassword( $row->user_newpassword ) instanceof \InvalidPassword ) {
+ return false;
+ }
+
+ if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function testUserExists( $username, $flags = User::READ_NORMAL ) {
+ $username = User::getCanonicalName( $username, 'usable' );
+ if ( $username === false ) {
+ return false;
+ }
+
+ list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
+ return (bool)wfGetDB( $db )->selectField(
+ [ 'user' ],
+ [ 'user_id' ],
+ [ 'user_name' => $username ],
+ __METHOD__,
+ $options
+ );
+ }
+
+ public function providerAllowsAuthenticationDataChange(
+ AuthenticationRequest $req, $checkData = true
+ ) {
+ if ( get_class( $req ) !== TemporaryPasswordAuthenticationRequest::class ) {
+ // We don't really ignore it, but this is what the caller expects.
+ return \StatusValue::newGood( 'ignored' );
+ }
+
+ if ( !$checkData ) {
+ return \StatusValue::newGood();
+ }
+
+ $username = User::getCanonicalName( $req->username, 'usable' );
+ if ( $username === false ) {
+ return \StatusValue::newGood( 'ignored' );
+ }
+
+ $row = wfGetDB( DB_MASTER )->selectRow(
+ 'user',
+ [ 'user_id', 'user_newpass_time' ],
+ [ 'user_name' => $username ],
+ __METHOD__
+ );
+
+ if ( !$row ) {
+ return \StatusValue::newGood( 'ignored' );
+ }
+
+ $sv = \StatusValue::newGood();
+ if ( $req->password !== null ) {
+ $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
+
+ if ( $req->mailpassword ) {
+ if ( !$this->emailEnabled ) {
+ return \StatusValue::newFatal( 'passwordreset-emaildisabled' );
+ }
+
+ // We don't check whether the user has an email address;
+ // that information should not be exposed to the caller.
+
+ // do not allow temporary password creation within
+ // $wgPasswordReminderResendTime from the last attempt
+ if (
+ $this->passwordReminderResendTime
+ && $row->user_newpass_time
+ && time() < wfTimestamp( TS_UNIX, $row->user_newpass_time )
+ + $this->passwordReminderResendTime * 3600
+ ) {
+ // Round the time in hours to 3 d.p., in case someone is specifying
+ // minutes or seconds.
+ return \StatusValue::newFatal( 'throttled-mailpassword',
+ round( $this->passwordReminderResendTime, 3 ) );
+ }
+
+ if ( !$req->caller ) {
+ return \StatusValue::newFatal( 'passwordreset-nocaller' );
+ }
+ if ( !\IP::isValid( $req->caller ) ) {
+ $caller = User::newFromName( $req->caller );
+ if ( !$caller ) {
+ return \StatusValue::newFatal( 'passwordreset-nosuchcaller', $req->caller );
+ }
+ }
+ }
+ }
+ return $sv;
+ }
+
+ public function providerChangeAuthenticationData( AuthenticationRequest $req ) {
+ $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
+ if ( $username === false ) {
+ return;
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $sendMail = false;
+ if ( $req->action !== AuthManager::ACTION_REMOVE &&
+ get_class( $req ) === TemporaryPasswordAuthenticationRequest::class
+ ) {
+ $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
+ $newpassTime = $dbw->timestamp();
+ $sendMail = $req->mailpassword;
+ } else {
+ // Invalidate the temporary password when any other auth is reset, or when removing
+ $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
+ $newpassTime = null;
+ }
+
+ $dbw->update(
+ 'user',
+ [
+ 'user_newpassword' => $pwhash->toString(),
+ 'user_newpass_time' => $newpassTime,
+ ],
+ [ 'user_name' => $username ],
+ __METHOD__
+ );
+
+ if ( $sendMail ) {
+ // Send email after DB commit
+ $dbw->onTransactionIdle(
+ function () use ( $req ) {
+ /** @var TemporaryPasswordAuthenticationRequest $req */
+ $this->sendPasswordResetEmail( $req );
+ },
+ __METHOD__
+ );
+ }
+ }
+
+ public function accountCreationType() {
+ return self::TYPE_CREATE;
+ }
+
+ public function testForAccountCreation( $user, $creator, array $reqs ) {
+ /** @var TemporaryPasswordAuthenticationRequest $req */
+ $req = AuthenticationRequest::getRequestByClass(
+ $reqs, TemporaryPasswordAuthenticationRequest::class
+ );
+
+ $ret = \StatusValue::newGood();
+ if ( $req ) {
+ if ( $req->mailpassword ) {
+ if ( !$this->emailEnabled ) {
+ $ret->merge( \StatusValue::newFatal( 'emaildisabled' ) );
+ } elseif ( !$user->getEmail() ) {
+ $ret->merge( \StatusValue::newFatal( 'noemailcreate' ) );
+ }
+ }
+
+ $ret->merge(
+ $this->checkPasswordValidity( $user->getName(), $req->password )
+ );
+ }
+ return $ret;
+ }
+
+ public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
+ /** @var TemporaryPasswordAuthenticationRequest $req */
+ $req = AuthenticationRequest::getRequestByClass(
+ $reqs, TemporaryPasswordAuthenticationRequest::class
+ );
+ if ( $req ) {
+ if ( $req->username !== null && $req->password !== null ) {
+ // Nothing we can do yet, because the user isn't in the DB yet
+ if ( $req->username !== $user->getName() ) {
+ $req = clone $req;
+ $req->username = $user->getName();
+ }
+
+ if ( $req->mailpassword ) {
+ // prevent EmailNotificationSecondaryAuthenticationProvider from sending another mail
+ $this->manager->setAuthenticationSessionData( 'no-email', true );
+ }
+
+ $ret = AuthenticationResponse::newPass( $req->username );
+ $ret->createRequest = $req;
+ return $ret;
+ }
+ }
+ return AuthenticationResponse::newAbstain();
+ }
+
+ public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
+ /** @var TemporaryPasswordAuthenticationRequest $req */
+ $req = $res->createRequest;
+ $mailpassword = $req->mailpassword;
+ $req->mailpassword = false; // providerChangeAuthenticationData would send the wrong email
+
+ // Now that the user is in the DB, set the password on it.
+ $this->providerChangeAuthenticationData( $req );
+
+ if ( $mailpassword ) {
+ // Send email after DB commit
+ wfGetDB( DB_MASTER )->onTransactionIdle(
+ function () use ( $user, $creator, $req ) {
+ $this->sendNewAccountEmail( $user, $creator, $req->password );
+ },
+ __METHOD__
+ );
+ }
+
+ return $mailpassword ? 'byemail' : null;
+ }
+
+ /**
+ * Check that a temporary password is still valid (hasn't expired).
+ * @param string $timestamp A timestamp in MediaWiki (TS_MW) format
+ * @return bool
+ */
+ protected function isTimestampValid( $timestamp ) {
+ $time = wfTimestampOrNull( TS_MW, $timestamp );
+ if ( $time !== null ) {
+ $expiry = wfTimestamp( TS_UNIX, $time ) + $this->newPasswordExpiry;
+ if ( time() >= $expiry ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Send an email about the new account creation and the temporary password.
+ * @param User $user The new user account
+ * @param User $creatingUser The user who created the account (can be anonymous)
+ * @param string $password The temporary password
+ * @return \Status
+ */
+ protected function sendNewAccountEmail( User $user, User $creatingUser, $password ) {
+ $ip = $creatingUser->getRequest()->getIP();
+ // @codeCoverageIgnoreStart
+ if ( !$ip ) {
+ return \Status::newFatal( 'badipaddress' );
+ }
+ // @codeCoverageIgnoreEnd
+
+ \Hooks::run( 'User::mailPasswordInternal', [ &$creatingUser, &$ip, &$user ] );
+
+ $mainPageUrl = \Title::newMainPage()->getCanonicalURL();
+ $userLanguage = $user->getOption( 'language' );
+ $subjectMessage = wfMessage( 'createaccount-title' )->inLanguage( $userLanguage );
+ $bodyMessage = wfMessage( 'createaccount-text', $ip, $user->getName(), $password,
+ '<' . $mainPageUrl . '>', round( $this->newPasswordExpiry / 86400 ) )
+ ->inLanguage( $userLanguage );
+
+ $status = $user->sendMail( $subjectMessage->text(), $bodyMessage->text() );
+
+ // TODO show 'mailerror' message on error, 'accmailtext' success message otherwise?
+ // @codeCoverageIgnoreStart
+ if ( !$status->isGood() ) {
+ $this->logger->warning( 'Could not send account creation email: ' .
+ $status->getWikiText( false, false, 'en' ) );
+ }
+ // @codeCoverageIgnoreEnd
+
+ return $status;
+ }
+
+ /**
+ * @param TemporaryPasswordAuthenticationRequest $req
+ * @return \Status
+ */
+ protected function sendPasswordResetEmail( TemporaryPasswordAuthenticationRequest $req ) {
+ $user = User::newFromName( $req->username );
+ if ( !$user ) {
+ return \Status::newFatal( 'noname' );
+ }
+ $userLanguage = $user->getOption( 'language' );
+ $callerIsAnon = \IP::isValid( $req->caller );
+ $callerName = $callerIsAnon ? $req->caller : User::newFromName( $req->caller )->getName();
+ $passwordMessage = wfMessage( 'passwordreset-emailelement', $user->getName(),
+ $req->password )->inLanguage( $userLanguage );
+ $emailMessage = wfMessage( $callerIsAnon ? 'passwordreset-emailtext-ip'
+ : 'passwordreset-emailtext-user' )->inLanguage( $userLanguage );
+ $emailMessage->params( $callerName, $passwordMessage->text(), 1,
+ '<' . \Title::newMainPage()->getCanonicalURL() . '>',
+ round( $this->newPasswordExpiry / 86400 ) );
+ $emailTitle = wfMessage( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
+ return $user->sendMail( $emailTitle->text(), $emailMessage->text() );
+ }
+}
diff --git a/www/wiki/includes/auth/ThrottlePreAuthenticationProvider.php b/www/wiki/includes/auth/ThrottlePreAuthenticationProvider.php
new file mode 100644
index 00000000..ae0bc6bb
--- /dev/null
+++ b/www/wiki/includes/auth/ThrottlePreAuthenticationProvider.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use BagOStuff;
+use Config;
+
+/**
+ * A pre-authentication provider to throttle authentication actions.
+ *
+ * Adding this provider will throttle account creations and primary authentication attempts
+ * (more specifically, any authentication that returns FAIL on failure). Secondary authentication
+ * cannot be easily throttled on a framework level (since it would typically return UI on failure);
+ * secondary providers are expected to do their own throttling.
+ * @ingroup Auth
+ * @since 1.27
+ */
+class ThrottlePreAuthenticationProvider extends AbstractPreAuthenticationProvider {
+ /** @var array */
+ protected $throttleSettings;
+
+ /** @var Throttler */
+ protected $accountCreationThrottle;
+
+ /** @var Throttler */
+ protected $passwordAttemptThrottle;
+
+ /** @var BagOStuff */
+ protected $cache;
+
+ /**
+ * @param array $params
+ * - accountCreationThrottle: (array) Condition array for the account creation throttle; an array
+ * of arrays in a format like $wgPasswordAttemptThrottle, passed to the Throttler constructor.
+ * - passwordAttemptThrottle: (array) Condition array for the password attempt throttle, in the
+ * same format as accountCreationThrottle.
+ * - cache: (BagOStuff) Where to store the throttle, defaults to the local cluster instance.
+ */
+ public function __construct( $params = [] ) {
+ $this->throttleSettings = array_intersect_key( $params,
+ [ 'accountCreationThrottle' => true, 'passwordAttemptThrottle' => true ] );
+ $this->cache = isset( $params['cache'] ) ? $params['cache'] :
+ \ObjectCache::getLocalClusterInstance();
+ }
+
+ public function setConfig( Config $config ) {
+ parent::setConfig( $config );
+
+ $accountCreationThrottle = $this->config->get( 'AccountCreationThrottle' );
+ // Handle old $wgAccountCreationThrottle format (number of attempts per 24 hours)
+ if ( !is_array( $accountCreationThrottle ) ) {
+ $accountCreationThrottle = [ [
+ 'count' => $accountCreationThrottle,
+ 'seconds' => 86400,
+ ] ];
+ }
+
+ // @codeCoverageIgnoreStart
+ $this->throttleSettings += [
+ // @codeCoverageIgnoreEnd
+ 'accountCreationThrottle' => $accountCreationThrottle,
+ 'passwordAttemptThrottle' => $this->config->get( 'PasswordAttemptThrottle' ),
+ ];
+
+ if ( !empty( $this->throttleSettings['accountCreationThrottle'] ) ) {
+ $this->accountCreationThrottle = new Throttler(
+ $this->throttleSettings['accountCreationThrottle'], [
+ 'type' => 'acctcreate',
+ 'cache' => $this->cache,
+ ]
+ );
+ }
+ if ( !empty( $this->throttleSettings['passwordAttemptThrottle'] ) ) {
+ $this->passwordAttemptThrottle = new Throttler(
+ $this->throttleSettings['passwordAttemptThrottle'], [
+ 'type' => 'password',
+ 'cache' => $this->cache,
+ ]
+ );
+ }
+ }
+
+ public function testForAccountCreation( $user, $creator, array $reqs ) {
+ if ( !$this->accountCreationThrottle || !$creator->isPingLimitable() ) {
+ return \StatusValue::newGood();
+ }
+
+ $ip = $this->manager->getRequest()->getIP();
+
+ if ( !\Hooks::run( 'ExemptFromAccountCreationThrottle', [ $ip ] ) ) {
+ $this->logger->debug( __METHOD__ . ": a hook allowed account creation w/o throttle\n" );
+ return \StatusValue::newGood();
+ }
+
+ $result = $this->accountCreationThrottle->increase( null, $ip, __METHOD__ );
+ if ( $result ) {
+ $message = wfMessage( 'acct_creation_throttle_hit' )->params( $result['count'] )
+ ->durationParams( $result['wait'] );
+ return \StatusValue::newFatal( $message );
+ }
+
+ return \StatusValue::newGood();
+ }
+
+ public function testForAuthentication( array $reqs ) {
+ if ( !$this->passwordAttemptThrottle ) {
+ return \StatusValue::newGood();
+ }
+
+ $ip = $this->manager->getRequest()->getIP();
+ try {
+ $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
+ } catch ( \UnexpectedValueException $e ) {
+ $username = '';
+ }
+
+ // Get everything this username could normalize to, and throttle each one individually.
+ // If nothing uses usernames, just throttle by IP.
+ $usernames = $this->manager->normalizeUsername( $username );
+ $result = false;
+ foreach ( $usernames as $name ) {
+ $r = $this->passwordAttemptThrottle->increase( $name, $ip, __METHOD__ );
+ if ( $r && ( !$result || $result['wait'] < $r['wait'] ) ) {
+ $result = $r;
+ }
+ }
+
+ if ( $result ) {
+ $message = wfMessage( 'login-throttled' )->durationParams( $result['wait'] );
+ return \StatusValue::newFatal( $message );
+ } else {
+ $this->manager->setAuthenticationSessionData( 'LoginThrottle',
+ [ 'users' => $usernames, 'ip' => $ip ] );
+ return \StatusValue::newGood();
+ }
+ }
+
+ /**
+ * @param null|\User $user
+ * @param AuthenticationResponse $response
+ */
+ public function postAuthentication( $user, AuthenticationResponse $response ) {
+ if ( $response->status !== AuthenticationResponse::PASS ) {
+ return;
+ } elseif ( !$this->passwordAttemptThrottle ) {
+ return;
+ }
+
+ $data = $this->manager->getAuthenticationSessionData( 'LoginThrottle' );
+ if ( !$data ) {
+ // this can occur when login is happening via AuthenticationRequest::$loginRequest
+ // so testForAuthentication is skipped
+ $this->logger->info( 'throttler data not found for {user}', [ 'user' => $user->getName() ] );
+ return;
+ }
+
+ foreach ( $data['users'] as $name ) {
+ $this->passwordAttemptThrottle->clear( $name, $data['ip'] );
+ }
+ }
+}
diff --git a/www/wiki/includes/auth/Throttler.php b/www/wiki/includes/auth/Throttler.php
new file mode 100644
index 00000000..3125bd3f
--- /dev/null
+++ b/www/wiki/includes/auth/Throttler.php
@@ -0,0 +1,208 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use BagOStuff;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+
+/**
+ * A helper class for throttling authentication attempts.
+ * @package MediaWiki\Auth
+ * @ingroup Auth
+ * @since 1.27
+ */
+class Throttler implements LoggerAwareInterface {
+ /** @var string */
+ protected $type;
+ /**
+ * See documentation of $wgPasswordAttemptThrottle for format. Old (pre-1.27) format is not
+ * allowed here.
+ * @var array
+ * @see https://www.mediawiki.org/wiki/Manual:$wgPasswordAttemptThrottle
+ */
+ protected $conditions;
+ /** @var BagOStuff */
+ protected $cache;
+ /** @var LoggerInterface */
+ protected $logger;
+ /** @var int|float */
+ protected $warningLimit;
+
+ /**
+ * @param array $conditions An array of arrays describing throttling conditions.
+ * Defaults to $wgPasswordAttemptThrottle. See documentation of that variable for format.
+ * @param array $params Parameters (all optional):
+ * - type: throttle type, used as a namespace for counters,
+ * - cache: a BagOStuff object where throttle counters are stored.
+ * - warningLimit: the log level will be raised to warning when rejecting an attempt after
+ * no less than this many failures.
+ */
+ public function __construct( array $conditions = null, array $params = [] ) {
+ $invalidParams = array_diff_key( $params,
+ array_fill_keys( [ 'type', 'cache', 'warningLimit' ], true ) );
+ if ( $invalidParams ) {
+ throw new \InvalidArgumentException( 'unrecognized parameters: '
+ . implode( ', ', array_keys( $invalidParams ) ) );
+ }
+
+ if ( $conditions === null ) {
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+ $conditions = $config->get( 'PasswordAttemptThrottle' );
+ $params += [
+ 'type' => 'password',
+ 'cache' => \ObjectCache::getLocalClusterInstance(),
+ 'warningLimit' => 50,
+ ];
+ } else {
+ $params += [
+ 'type' => 'custom',
+ 'cache' => \ObjectCache::getLocalClusterInstance(),
+ 'warningLimit' => INF,
+ ];
+ }
+
+ $this->type = $params['type'];
+ $this->conditions = static::normalizeThrottleConditions( $conditions );
+ $this->cache = $params['cache'];
+ $this->warningLimit = $params['warningLimit'];
+
+ $this->setLogger( LoggerFactory::getInstance( 'throttler' ) );
+ }
+
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * Increase the throttle counter and return whether the attempt should be throttled.
+ *
+ * Should be called before an authentication attempt.
+ *
+ * @param string|null $username
+ * @param string|null $ip
+ * @param string|null $caller The authentication method from which we were called.
+ * @return array|false False if the attempt should not be throttled, an associative array
+ * with three keys otherwise:
+ * - throttleIndex: which throttle condition was met (a key of the conditions array)
+ * - count: throttle count (ie. number of failed attempts)
+ * - wait: time in seconds until authentication can be attempted
+ */
+ public function increase( $username = null, $ip = null, $caller = null ) {
+ if ( $username === null && $ip === null ) {
+ throw new \InvalidArgumentException( 'Either username or IP must be set for throttling' );
+ }
+
+ $userKey = $username ? md5( $username ) : null;
+ foreach ( $this->conditions as $index => $throttleCondition ) {
+ $ipKey = isset( $throttleCondition['allIPs'] ) ? null : $ip;
+ $count = $throttleCondition['count'];
+ $expiry = $throttleCondition['seconds'];
+
+ // a limit of 0 is used as a disable flag in some throttling configuration settings
+ // throttling the whole world is probably a bad idea
+ if ( !$count || $userKey === null && $ipKey === null ) {
+ continue;
+ }
+
+ $throttleKey = $this->cache->makeGlobalKey( 'throttler', $this->type, $index, $ipKey, $userKey );
+ $throttleCount = $this->cache->get( $throttleKey );
+
+ if ( !$throttleCount ) { // counter not started yet
+ $this->cache->add( $throttleKey, 1, $expiry );
+ } elseif ( $throttleCount < $count ) { // throttle limited not yet reached
+ $this->cache->incr( $throttleKey );
+ } else { // throttled
+ $this->logRejection( [
+ 'throttle' => $this->type,
+ 'index' => $index,
+ 'ip' => $ipKey,
+ 'username' => $username,
+ 'count' => $count,
+ 'expiry' => $expiry,
+ // @codeCoverageIgnoreStart
+ 'method' => $caller ?: __METHOD__,
+ // @codeCoverageIgnoreEnd
+ ] );
+
+ return [
+ 'throttleIndex' => $index,
+ 'count' => $count,
+ 'wait' => $expiry,
+ ];
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Clear the throttle counter.
+ *
+ * Should be called after a successful authentication attempt.
+ *
+ * @param string|null $username
+ * @param string|null $ip
+ * @throws \MWException
+ */
+ public function clear( $username = null, $ip = null ) {
+ $userKey = $username ? md5( $username ) : null;
+ foreach ( $this->conditions as $index => $specificThrottle ) {
+ $ipKey = isset( $specificThrottle['allIPs'] ) ? null : $ip;
+ $throttleKey = $this->cache->makeGlobalKey( 'throttler', $this->type, $index, $ipKey, $userKey );
+ $this->cache->delete( $throttleKey );
+ }
+ }
+
+ /**
+ * Handles B/C for $wgPasswordAttemptThrottle.
+ * @param array $throttleConditions
+ * @return array
+ * @see $wgPasswordAttemptThrottle for structure
+ */
+ protected static function normalizeThrottleConditions( $throttleConditions ) {
+ if ( !is_array( $throttleConditions ) ) {
+ return [];
+ }
+ if ( isset( $throttleConditions['count'] ) ) { // old style
+ $throttleConditions = [ $throttleConditions ];
+ }
+ return $throttleConditions;
+ }
+
+ protected function logRejection( array $context ) {
+ $logMsg = 'Throttle {throttle} hit, throttled for {expiry} seconds due to {count} attempts '
+ . 'from username {username} and IP {ip}';
+
+ // If we are hitting a throttle for >= warningLimit attempts, it is much more likely to be
+ // an attack than someone simply forgetting their password, so log it at a higher level.
+ $level = $context['count'] >= $this->warningLimit ? LogLevel::WARNING : LogLevel::INFO;
+
+ // It should be noted that once the throttle is hit, every attempt to login will
+ // generate the log message until the throttle expires, not just the attempt that
+ // puts the throttle over the top.
+ $this->logger->log( $level, $logMsg, $context );
+ }
+
+}
diff --git a/www/wiki/includes/auth/UserDataAuthenticationRequest.php b/www/wiki/includes/auth/UserDataAuthenticationRequest.php
new file mode 100644
index 00000000..35d66523
--- /dev/null
+++ b/www/wiki/includes/auth/UserDataAuthenticationRequest.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+use MediaWiki\MediaWikiServices;
+use StatusValue;
+use User;
+
+/**
+ * This represents additional user data requested on the account creation form
+ *
+ * @ingroup Auth
+ * @since 1.27
+ */
+class UserDataAuthenticationRequest extends AuthenticationRequest {
+ /** @var string|null Email address */
+ public $email;
+
+ /** @var string|null Real name */
+ public $realname;
+
+ public function getFieldInfo() {
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+ $ret = [
+ 'email' => [
+ 'type' => 'string',
+ 'label' => wfMessage( 'authmanager-email-label' ),
+ 'help' => wfMessage( 'authmanager-email-help' ),
+ 'optional' => true,
+ ],
+ 'realname' => [
+ 'type' => 'string',
+ 'label' => wfMessage( 'authmanager-realname-label' ),
+ 'help' => wfMessage( 'authmanager-realname-help' ),
+ 'optional' => true,
+ ],
+ ];
+
+ if ( !$config->get( 'EnableEmail' ) ) {
+ unset( $ret['email'] );
+ }
+
+ if ( in_array( 'realname', $config->get( 'HiddenPrefs' ), true ) ) {
+ unset( $ret['realname'] );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Add data to the User object
+ * @param User $user User being created (not added to the database yet).
+ * This may become a "UserValue" in the future, or User may be refactored
+ * into such.
+ * @return StatusValue
+ */
+ public function populateUser( $user ) {
+ if ( $this->email !== null && $this->email !== '' ) {
+ if ( !\Sanitizer::validateEmail( $this->email ) ) {
+ return StatusValue::newFatal( 'invalidemailaddress' );
+ }
+ $user->setEmail( $this->email );
+ }
+ if ( $this->realname !== null && $this->realname !== '' ) {
+ $user->setRealName( $this->realname );
+ }
+ return StatusValue::newGood();
+ }
+
+}
diff --git a/www/wiki/includes/auth/UsernameAuthenticationRequest.php b/www/wiki/includes/auth/UsernameAuthenticationRequest.php
new file mode 100644
index 00000000..7bf8f130
--- /dev/null
+++ b/www/wiki/includes/auth/UsernameAuthenticationRequest.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * 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
+ * @ingroup Auth
+ */
+
+namespace MediaWiki\Auth;
+
+/**
+ * AuthenticationRequest to ensure something with a username is present
+ * @ingroup Auth
+ * @since 1.27
+ */
+class UsernameAuthenticationRequest extends AuthenticationRequest {
+ public function getFieldInfo() {
+ return [
+ 'username' => [
+ 'type' => 'string',
+ 'label' => wfMessage( 'userlogin-yourname' ),
+ 'help' => wfMessage( 'authmanager-username-help' ),
+ ],
+ ];
+ }
+}