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 PermissionManager */
- private $permissionManager;
+ protected $permissionManager;
- /** @var LoggerInterface */
- protected $logger;
+ /** @var ILoadBalancer */
+ protected $loadBalancer;
/**
* In-process cache for isAllowed lookups, by username.
*/
private $permissionCache;
+ public const CONSTRUCTOR_OPTIONS = [
+ 'AllowRequiringEmailForResets',
+ 'EnableEmail',
+ 'PasswordResetRoutes',
+ ];
+
+ /**
+ * This class is managed by MediaWikiServices, don't instantiate directly.
+ *
+ * @param ServiceOptions|Config $config
+ * @param AuthManager $authManager
+ * @param PermissionManager $permissionManager
+ * @param ILoadBalancer|null $loadBalancer
+ * @param LoggerInterface|null $logger
+ */
public function __construct(
- Config $config,
+ $config,
AuthManager $authManager,
- PermissionManager $permissionManager
+ PermissionManager $permissionManager,
+ ILoadBalancer $loadBalancer = null,
+ LoggerInterface $logger = null
) {
$this->config = $config;
$this->authManager = $authManager;
$this->permissionManager = $permissionManager;
- $this->permissionCache = new MapCacheLRU( 1 );
- $this->logger = LoggerFactory::getInstance( 'authentication' );
- }
- /**
- * Set the logger instance to use.
- *
- * @param LoggerInterface $logger
- * @since 1.29
- */
- public function setLogger( LoggerInterface $logger ) {
+ 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 );
}
/**
* @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 );
+ }
}