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.
* 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.
*/
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
*/
} 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 ) ) {
* @param User $performingUser The user that does the password reset
* @param string|null $username The user whose password is reset
* @param string|null $email Alternative way to specify the user
- * @return StatusValue Will contain the passwords as a username => password array if the
- * $displayPassword flag was set
+ * @return StatusValue
* @throws LogicException When the user is not allowed to perform the action
* @throws MWException On unexpected DB errors
*/
. ' 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' );
$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
}
}
- $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
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();
'requestingUser' => $performingUser->getName(),
'targetUsername' => $username,
'targetEmail' => $email,
- 'actualUser' => $firstUser->getName(),
];
if ( !$result->isGood() ) {
return $result;
}
- $passwords = [];
- foreach ( $reqs as $req ) {
- // This is adding a new temporary password, not intentionally changing anything
- // (even though it might technically invalidate an old temporary password).
- $this->authManager->changeAuthenticationData( $req, /* $isAddition */ true );
- }
-
- $this->logger->info(
- "{requestingUser} did password reset of {actualUser}",
- $logContext
+ DeferredUpdates::addUpdate(
+ new SendPasswordResetEmailUpdate( $this->authManager, $reqs, $logContext ),
+ DeferredUpdates::POSTSEND
);
- return StatusValue::newGood( $passwords );
+ return StatusValue::newGood();
}
/**
*/
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 ],
}
return $users;
}
+
+ /**
+ * User object creation helper for testability
+ * @codeCoverageIgnore
+ *
+ * @param string $username
+ * @return User|false
+ */
+ protected function lookupUser( $username ) {
+ return User::newFromName( $username );
+ }
}