Services: Convert PasswordReset's static to a const now HHVM is gone
[lhc/web/wiklou.git] / includes / user / PasswordReset.php
index aada319..2958b1f 100644 (file)
 
 use MediaWiki\Auth\AuthManager;
 use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
+use MediaWiki\Config\ServiceOptions;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Permissions\PermissionManager;
 use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
 use Psr\Log\LoggerInterface;
-use MediaWiki\Logger\LoggerFactory;
+use Wikimedia\Rdbms\ILoadBalancer;
 
 /**
  * Helper class for the password reset functionality shared by the web UI and the API.
@@ -34,14 +39,19 @@ use MediaWiki\Logger\LoggerFactory;
  * functionality) to be enabled.
  */
 class PasswordReset implements LoggerAwareInterface {
-       /** @var Config */
+       use LoggerAwareTrait;
+
+       /** @var ServiceOptions|Config */
        protected $config;
 
        /** @var AuthManager */
        protected $authManager;
 
-       /** @var LoggerInterface */
-       protected $logger;
+       /** @var PermissionManager */
+       protected $permissionManager;
+
+       /** @var ILoadBalancer */
+       protected $loadBalancer;
 
        /**
         * In-process cache for isAllowed lookups, by username.
@@ -50,28 +60,50 @@ class PasswordReset implements LoggerAwareInterface {
         */
        private $permissionCache;
 
-       public function __construct( Config $config, AuthManager $authManager ) {
-               $this->config = $config;
-               $this->authManager = $authManager;
-               $this->permissionCache = new MapCacheLRU( 1 );
-               $this->logger = LoggerFactory::getInstance( 'authentication' );
-       }
+       public const CONSTRUCTOR_OPTIONS = [
+               'AllowRequiringEmailForResets',
+               'EnableEmail',
+               'PasswordResetRoutes',
+       ];
 
        /**
-        * Set the logger instance to use.
+        * This class is managed by MediaWikiServices, don't instantiate directly.
         *
-        * @param LoggerInterface $logger
-        * @since 1.29
+        * @param ServiceOptions|Config $config
+        * @param AuthManager $authManager
+        * @param PermissionManager $permissionManager
+        * @param ILoadBalancer|null $loadBalancer
+        * @param LoggerInterface|null $logger
         */
-       public function setLogger( LoggerInterface $logger ) {
+       public function __construct(
+               $config,
+               AuthManager $authManager,
+               PermissionManager $permissionManager,
+               ILoadBalancer $loadBalancer = null,
+               LoggerInterface $logger = null
+       ) {
+               $this->config = $config;
+               $this->authManager = $authManager;
+               $this->permissionManager = $permissionManager;
+
+               if ( !$loadBalancer ) {
+                       wfDeprecated( 'Not passing LoadBalancer to ' . __METHOD__, '1.34' );
+                       $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
+               }
+               $this->loadBalancer = $loadBalancer;
+
+               if ( !$logger ) {
+                       wfDeprecated( 'Not passing LoggerInterface to ' . __METHOD__, '1.34' );
+                       $logger = LoggerFactory::getInstance( 'authentication' );
+               }
                $this->logger = $logger;
+
+               $this->permissionCache = new MapCacheLRU( 1 );
        }
 
        /**
         * Check if a given user has permission to use this functionality.
         * @param User $user
-        * @param bool $displayPassword If set, also check whether the user is allowed to reset the
-        *   password of another user and see the temporary password.
         * @since 1.29 Second argument for displayPassword removed.
         * @return StatusValue
         */
@@ -95,7 +127,7 @@ class PasswordReset implements LoggerAwareInterface {
                        } elseif ( !$this->config->get( 'EnableEmail' ) ) {
                                // Maybe email features have been disabled
                                $status = StatusValue::newFatal( 'passwordreset-emaildisabled' );
-                       } elseif ( !$user->isAllowed( 'editmyprivateinfo' ) ) {
+                       } elseif ( !$this->permissionManager->userHasRight( $user, 'editmyprivateinfo' ) ) {
                                // Maybe not all users have permission to change private data
                                $status = StatusValue::newFatal( 'badaccess' );
                        } elseif ( $this->isBlocked( $user ) ) {
@@ -135,12 +167,14 @@ class PasswordReset implements LoggerAwareInterface {
                                . ' is not allowed to reset passwords' );
                }
 
+               $username = $username ?? '';
+               $email = $email ?? '';
+
                $resetRoutes = $this->config->get( 'PasswordResetRoutes' )
                        + [ 'username' => false, 'email' => false ];
                if ( $resetRoutes['username'] && $username ) {
                        $method = 'username';
-                       $users = [ User::newFromName( $username ) ];
-                       $email = null;
+                       $users = [ $this->lookupUser( $username ) ];
                } elseif ( $resetRoutes['email'] && $email ) {
                        if ( !Sanitizer::validateEmail( $email ) ) {
                                return StatusValue::newFatal( 'passwordreset-invalidemail' );
@@ -157,12 +191,33 @@ class PasswordReset implements LoggerAwareInterface {
                $error = [];
                $data = [
                        'Username' => $username,
-                       'Email' => $email,
+                       // Email gets set to null for backward compatibility
+                       'Email' => $method === 'email' ? $email : null,
                ];
                if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) {
                        return StatusValue::newFatal( Message::newFromSpecifier( $error ) );
                }
 
+               $firstUser = $users[0] ?? null;
+               $requireEmail = $this->config->get( 'AllowRequiringEmailForResets' )
+                       && $method === 'username'
+                       && $firstUser
+                       && $firstUser->getBoolOption( 'requireemail' );
+               if ( $requireEmail ) {
+                       if ( $email === '' ) {
+                               return StatusValue::newFatal( 'passwordreset-username-email-required' );
+                       }
+
+                       if ( !Sanitizer::validateEmail( $email ) ) {
+                               return StatusValue::newFatal( 'passwordreset-invalidemail' );
+                       }
+               }
+
+               // Check against the rate limiter
+               if ( $performingUser->pingLimiter( 'mailpassword' ) ) {
+                       return StatusValue::newFatal( 'actionthrottledtext' );
+               }
+
                if ( !$users ) {
                        if ( $method === 'email' ) {
                                // Don't reveal whether or not an email address is in use
@@ -172,18 +227,11 @@ class PasswordReset implements LoggerAwareInterface {
                        }
                }
 
-               $firstUser = $users[0];
-
                if ( !$firstUser instanceof User || !$firstUser->getId() ) {
                        // Don't parse username as wikitext (T67501)
                        return StatusValue::newFatal( wfMessage( 'nosuchuser', wfEscapeWikiText( $username ) ) );
                }
 
-               // Check against the rate limiter
-               if ( $performingUser->pingLimiter( 'mailpassword' ) ) {
-                       return StatusValue::newFatal( 'actionthrottledtext' );
-               }
-
                // All the users will have the same email address
                if ( !$firstUser->getEmail() ) {
                        // This won't be reachable from the email route, so safe to expose the username
@@ -191,6 +239,11 @@ class PasswordReset implements LoggerAwareInterface {
                                wfEscapeWikiText( $firstUser->getName() ) ) );
                }
 
+               if ( $requireEmail && $firstUser->getEmail() !== $email ) {
+                       // Pretend everything's fine to avoid disclosure
+                       return StatusValue::newGood();
+               }
+
                // We need to have a valid IP address for the hook, but per T20347, we should
                // send the user's name if they're logged in.
                $ip = $performingUser->getRequest()->getIP();
@@ -273,7 +326,7 @@ class PasswordReset implements LoggerAwareInterface {
         */
        protected function getUsersByEmail( $email ) {
                $userQuery = User::getQueryInfo();
-               $res = wfGetDB( DB_REPLICA )->select(
+               $res = $this->loadBalancer->getConnectionRef( DB_REPLICA )->select(
                        $userQuery['tables'],
                        $userQuery['fields'],
                        [ 'user_email' => $email ],
@@ -293,4 +346,15 @@ class PasswordReset implements LoggerAwareInterface {
                }
                return $users;
        }
+
+       /**
+        * User object creation helper for testability
+        * @codeCoverageIgnore
+        *
+        * @param string $username
+        * @return User|false
+        */
+       protected function lookupUser( $username ) {
+               return User::newFromName( $username );
+       }
 }