Optionally require both username and email for password resets
[lhc/web/wiklou.git] / includes / user / PasswordReset.php
index 8ef1d0d..be7ea91 100644 (file)
@@ -61,6 +61,7 @@ class PasswordReset implements LoggerAwareInterface {
        private $permissionCache;
 
        public static $constructorOptions = [
+               'AllowRequiringEmailForResets',
                'EnableEmail',
                'PasswordResetRoutes',
        ];
@@ -166,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' );
@@ -188,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
@@ -203,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
@@ -222,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();
@@ -324,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 );
+       }
 }