38use Psr\Log\LoggerAwareInterface;
39use Psr\Log\LoggerAwareTrait;
40use Psr\Log\LoggerInterface;
67 private $permissionCache;
92 LoggerInterface $logger,
102 $this->config = $config;
103 $this->logger = $logger;
105 $this->authManager = $authManager;
106 $this->hookRunner =
new HookRunner( $hookContainer );
107 $this->dbProvider = $dbProvider;
108 $this->userFactory = $userFactory;
109 $this->userNameUtils = $userNameUtils;
110 $this->userOptionsLookup = $userOptionsLookup;
122 return $this->permissionCache->getWithSetCallback(
124 function () use ( $user ) {
125 return $this->computeIsAllowed( $user );
135 $resetRoutes = $this->config->get(
MainConfigNames::PasswordResetRoutes );
136 if ( !is_array( $resetRoutes ) || !in_array(
true, $resetRoutes,
true ) ) {
138 return StatusValue::newFatal(
'passwordreset-disabled' );
141 $providerStatus = $this->authManager->allowsAuthenticationDataChange(
143 if ( !$providerStatus->isGood() ) {
145 return StatusValue::newFatal(
'resetpass_forbidden-reason',
146 $providerStatus->getMessage() );
150 return StatusValue::newFatal(
'passwordreset-emaildisabled' );
152 return StatusValue::newGood();
159 private function computeIsAllowed( User $user ):
StatusValue {
160 $enabledStatus = $this->isEnabled();
161 if ( !$enabledStatus->isGood() ) {
162 return $enabledStatus;
164 if ( !$user->isAllowed(
'editmyprivateinfo' ) ) {
168 if ( $this->isBlocked( $user ) ) {
191 User $performingUser,
195 if ( !$this->isAllowed( $performingUser )->isGood() ) {
196 throw new LogicException(
197 'User ' . $performingUser->
getName() .
' is not allowed to reset passwords'
203 if ( $performingUser->
pingLimiter(
'mailpassword' ) ) {
204 return StatusValue::newGood();
211 return StatusValue::newFatal(
'badipaddress' );
217 $resetRoutes = $this->config->get( MainConfigNames::PasswordResetRoutes )
218 + [
'username' =>
false,
'email' => false ];
219 if ( $resetRoutes[
'username'] && $username ) {
220 $method =
'username';
221 $users = [ $this->userFactory->newFromName( $username ) ];
222 } elseif ( $resetRoutes[
'email'] && $email ) {
223 if ( !Sanitizer::validateEmail( $email ) ) {
225 return StatusValue::newGood();
229 $users = $this->getUsersByEmail( $email );
232 if ( $this->config->get( MainConfigNames::AllowRequiringEmailForResets ) ) {
233 $optionsLookup = $this->userOptionsLookup;
234 foreach ( $users as $index => $user ) {
235 if ( $optionsLookup->getBoolOption( $user,
'requireemail' ) ) {
236 unset( $users[$index] );
242 return StatusValue::newFatal(
'passwordreset-nodata' );
246 if ( $username && !$this->userNameUtils->getCanonical( $username ) ) {
247 return StatusValue::newFatal(
'noname' );
253 'Username' => $username,
255 'Email' => $method ===
'email' ? $email :
null,
261 $users = array_values( $users );
263 if ( !$this->hookRunner->onSpecialPasswordResetOnSubmit( $users, $data, $error ) ) {
264 return StatusValue::newFatal( Message::newFromSpecifier( $error ) );
269 $firstUser = reset( $users );
271 $requireEmail = $this->config->get( MainConfigNames::AllowRequiringEmailForResets )
272 && $method ===
'username'
274 && $this->userOptionsLookup->getBoolOption( $firstUser,
'requireemail' );
275 if ( $requireEmail && ( $email ===
'' || !Sanitizer::validateEmail( $email ) ) ) {
277 return StatusValue::newGood();
281 if ( $method ===
'email' ) {
283 return StatusValue::newGood();
285 return StatusValue::newFatal(
'noname' );
293 if ( !$firstUser instanceof
User || !$firstUser->
getId() || !$firstUser->getEmail() ) {
294 return StatusValue::newGood();
298 if ( $requireEmail && $firstUser->getEmail() !== $email ) {
299 return StatusValue::newGood();
302 $this->hookRunner->onUser__mailPasswordInternal( $performingUser, $ip, $firstUser );
304 $result = StatusValue::newGood();
306 foreach ( $users as $user ) {
307 $req = TemporaryPasswordAuthenticationRequest::newRandom();
308 $req->username = $user->getName();
309 $req->mailpassword =
true;
310 $req->caller = $performingUser->
getName();
312 $status = $this->authManager->allowsAuthenticationDataChange( $req,
true );
316 if ( $status->isGood() && $status->getValue() ===
'throttled-mailpassword' ) {
317 return StatusValue::newGood();
320 if ( $status->isGood() && $status->getValue() !==
'ignored' ) {
322 } elseif ( $result->isGood() ) {
325 if ( $status->getValue() ===
'ignored' ) {
326 $status = StatusValue::newFatal(
'passwordreset-ignored' );
328 $result->merge( $status );
333 'requestingIp' => $ip,
334 'requestingUser' => $performingUser->
getName(),
335 'targetUsername' => $username,
336 'targetEmail' => $email,
339 if ( !$result->isGood() ) {
341 "{requestingUser} attempted password reset of {actualUser} but failed",
342 $logContext + [
'errors' => $result->getErrors() ]
347 DeferredUpdates::addUpdate(
349 DeferredUpdates::POSTSEND
352 return StatusValue::newGood();
362 private function isBlocked(
User $user ) {
364 return $block && $block->appliesToPasswordReset();
374 $res = User::newQueryBuilder( $this->dbProvider->getReplicaDatabase() )
375 ->where( [
'user_email' => $email ] )
376 ->caller( __METHOD__ )
380 foreach ( $res as $row ) {
381 $users[] = $this->userFactory->newFromRow( $row );
389class_alias( PasswordReset::class,
'PasswordReset' );
Store key-value entries in a size-limited in-memory LRU cache.
A class containing constants representing the names of configuration variables.
const EnableEmail
Name constant for the EnableEmail setting, for use with Config::get()
const PasswordResetRoutes
Name constant for the PasswordResetRoutes setting, for use with Config::get()
const AllowRequiringEmailForResets
Name constant for the AllowRequiringEmailForResets setting, for use with Config::get()
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
static newGood( $value=null)
Factory function for good results.