X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Fuser%2FPasswordReset.php;h=2958b1f2d048f6b8e8eaf58f4510180e4b0544d5;hp=fd8eb3fac1677b6eeb3794dce65de67d95eb2542;hb=8e0ef3e187b4b8ebe847dbc8ae1ec2b96ed58ae0;hpb=e3e33ce99c909103b4b2b861c8361729441eccc8 diff --git a/includes/user/PasswordReset.php b/includes/user/PasswordReset.php index fd8eb3fac1..2958b1f2d0 100644 --- a/includes/user/PasswordReset.php +++ b/includes/user/PasswordReset.php @@ -22,9 +22,14 @@ 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,21 +60,45 @@ 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 ); } /** @@ -93,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 ) ) { @@ -133,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' ); @@ -155,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 @@ -170,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 @@ -189,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(); @@ -271,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 ], @@ -291,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 ); + } }