MediaWiki master
PasswordReset.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\User;
8
9use Iterator;
10use LogicException;
22use Psr\Log\LoggerAwareInterface;
23use Psr\Log\LoggerAwareTrait;
24use Psr\Log\LoggerInterface;
25use StatusValue;
27
35class PasswordReset implements LoggerAwareInterface {
36 use LoggerAwareTrait;
37
38 private readonly HookRunner $hookRunner;
39
44 private readonly MapCacheLRU $permissionCache;
45
49 public const CONSTRUCTOR_OPTIONS = [
52 ];
53
57 public function __construct(
58 private readonly ServiceOptions $config,
59 LoggerInterface $logger,
60 private readonly AuthManager $authManager,
61 HookContainer $hookContainer,
62 private readonly UserIdentityLookup $userIdentityLookup,
63 private readonly UserFactory $userFactory,
64 private readonly UserNameUtils $userNameUtils,
65 private readonly UserOptionsLookup $userOptionsLookup,
66 ) {
67 $config->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
68
69 $this->logger = $logger;
70 $this->hookRunner = new HookRunner( $hookContainer );
71
72 $this->permissionCache = new MapCacheLRU( 1 );
73 }
74
81 public function isAllowed( User $user ) {
82 return $this->permissionCache->getWithSetCallback(
83 $user->getName(),
84 function () use ( $user ) {
85 return $this->computeIsAllowed( $user );
86 }
87 );
88 }
89
94 public function isEnabled(): StatusValue {
95 $resetRoutes = $this->config->get( MainConfigNames::PasswordResetRoutes );
96 if ( !is_array( $resetRoutes ) || !in_array( true, $resetRoutes, true ) ) {
97 // Maybe password resets are disabled, or there are no allowable routes
98 return StatusValue::newFatal( 'passwordreset-disabled' );
99 }
100
101 $providerStatus = $this->authManager->allowsAuthenticationDataChange(
103 if ( !$providerStatus->isGood() ) {
104 // Maybe the external auth plugin won't allow local password changes
105 return StatusValue::newFatal( 'resetpass_forbidden-reason',
106 $providerStatus->getMessage() );
107 }
108 if ( !$this->config->get( MainConfigNames::EnableEmail ) ) {
109 // Maybe email features have been disabled
110 return StatusValue::newFatal( 'passwordreset-emaildisabled' );
111 }
112 return StatusValue::newGood();
113 }
114
115 private function computeIsAllowed( User $user ): StatusValue {
116 $enabledStatus = $this->isEnabled();
117 if ( !$enabledStatus->isGood() ) {
118 return $enabledStatus;
119 }
120 if ( !$user->isAllowed( 'editmyprivateinfo' ) ) {
121 // Maybe not all users have permission to change private data
122 return StatusValue::newFatal( 'badaccess' );
123 }
124 if ( $this->isBlocked( $user ) ) {
125 // Maybe the user is blocked (check this here rather than relying on the parent
126 // method as we have a more specific error message to use here, and we want to
127 // ignore some types of blocks)
128 return StatusValue::newFatal( 'blocked-mailpassword' );
129 }
130 return StatusValue::newGood();
131 }
132
148 public function execute(
149 User $performingUser,
150 $username = null,
151 $email = null
152 ) {
153 if ( !$this->isAllowed( $performingUser )->isGood() ) {
154 throw new LogicException(
155 'User ' . $performingUser->getName() . ' is not allowed to reset passwords'
156 );
157 }
158
159 // Check against the rate limiter. If the $wgRateLimit is reached, we want to pretend
160 // that the request was good to avoid displaying an error message.
161 if ( $performingUser->pingLimiter( 'mailpassword' ) ) {
162 return StatusValue::newGood();
163 }
164
165 // We need to have a valid IP address for the hook 'User::mailPasswordInternal', but per T20347,
166 // we should send the user's name if they're logged in.
167 $ip = $performingUser->getRequest()->getIP();
168 if ( !$ip ) {
169 return StatusValue::newFatal( 'badipaddress' );
170 }
171
172 $resetRoutes = $this->config->get( MainConfigNames::PasswordResetRoutes )
173 + [ 'username' => false, 'email' => false ];
174 if ( !$resetRoutes['username'] || $username === '' ) {
175 $username = null;
176 }
177 if ( !$resetRoutes['email'] || $email === '' ) {
178 $email = null;
179 }
180
181 if ( $username !== null && !$this->userNameUtils->getCanonical( $username ) ) {
182 return StatusValue::newFatal( 'noname' );
183 }
184 if ( $email !== null && !Sanitizer::validateEmail( $email ) ) {
185 return StatusValue::newFatal( 'passwordreset-invalidemail' );
186 }
187 // At this point, $username and $email are either valid or not provided
188
190 $users = [];
191
192 if ( $username !== null ) {
193 $user = $this->userFactory->newFromName( $username );
194 // User must have an email address to attempt sending a password reset email
195 if ( $user && $user->isRegistered() && $user->getEmail() && (
196 !$this->userOptionsLookup->getBoolOption( $user, 'requireemail' ) ||
197 $user->getEmail() === $email
198 ) ) {
199 // Either providing the email in the form is not required to request a reset,
200 // or the correct email was provided
201 $users[] = $user;
202 }
203
204 } elseif ( $email !== null ) {
205 foreach ( $this->getUsersByEmail( $email ) as $userIdent ) {
206 // Skip users whose preference 'requireemail' is on since the username was not submitted
207 if ( $this->userOptionsLookup->getBoolOption( $userIdent, 'requireemail' ) ) {
208 continue;
209 }
210 $users[] = $this->userFactory->newFromUserIdentity( $userIdent );
211 }
212
213 } else {
214 // The user didn't supply any data
215 return StatusValue::newFatal( 'passwordreset-nodata' );
216 }
217
218 // Check for hooks (captcha etc.), and allow them to modify the list of users
219 $data = [
220 'Username' => $username,
221 'Email' => $email,
222 ];
223
224 $error = [];
225 if ( !$this->hookRunner->onSpecialPasswordResetOnSubmit( $users, $data, $error ) ) {
226 return StatusValue::newFatal( Message::newFromSpecifier( $error ) );
227 }
228
229 if ( !$users ) {
230 // Don't reveal whether a username or email address is in use
231 return StatusValue::newGood();
232 }
233
234 // Get the first element in $users by using `reset` function since
235 // the key '0' might have been unset from $users array by a hook handler.
236 $firstUser = reset( $users );
237
238 $this->hookRunner->onUser__mailPasswordInternal( $performingUser, $ip, $firstUser );
239
240 $result = StatusValue::newGood();
241 $reqs = [];
242 foreach ( $users as $user ) {
243 $req = TemporaryPasswordAuthenticationRequest::newRandom();
244 $req->username = $user->getName();
245 $req->mailpassword = true;
246 $req->caller = $performingUser->getName();
247
248 $status = $this->authManager->allowsAuthenticationDataChange( $req, true );
249 // If the status is good and the value is 'throttled-mailpassword', we want to pretend
250 // that the request was good to avoid displaying an error message and disclose
251 // if a reset password was previously sent.
252 if ( $status->isGood() && $status->getValue() === 'throttled-mailpassword' ) {
253 return StatusValue::newGood();
254 }
255
256 if ( $status->isGood() && $status->getValue() !== 'ignored' ) {
257 $reqs[] = $req;
258 } elseif ( $result->isGood() ) {
259 // only record the first error, to avoid exposing the number of users having the
260 // same email address
261 if ( $status->getValue() === 'ignored' ) {
262 $status = StatusValue::newFatal( 'passwordreset-ignored' );
263 }
264 $result->merge( $status );
265 }
266 }
267
268 $logContext = [
269 'requestingIp' => $ip,
270 'requestingUser' => $performingUser->getName(),
271 'targetUsername' => $username,
272 'targetEmail' => $email,
273 ] + $performingUser->getRequest()->getSecurityLogContext();
274
275 if ( !$result->isGood() ) {
276 $this->logger->info(
277 "{requestingUser} attempted password reset of {targetUsername} but failed",
278 $logContext + [ 'errors' => $result->getErrors() ]
279 );
280 return $result;
281 }
282
283 DeferredUpdates::addUpdate(
284 new SendPasswordResetEmailUpdate( $this->authManager, $reqs, $logContext ),
285 DeferredUpdates::POSTSEND
286 );
287
288 return StatusValue::newGood();
289 }
290
298 private function isBlocked( User $user ) {
299 $block = $user->getBlock();
300 return $block && $block->appliesToPasswordReset();
301 }
302
310 protected function getUsersByEmail( $email ) {
311 return $this->userIdentityLookup->newSelectQueryBuilder()
312 ->join( 'user', null, [ "actor_user=user_id" ] )
313 ->where( [ 'user_email' => $email ] )
314 ->caller( __METHOD__ )
315 ->fetchUserIdentities();
316 }
317
318}
319
321class_alias( PasswordReset::class, 'PasswordReset' );
AuthManager is the authentication system in MediaWiki and serves entry point for authentication.
This represents the intention to set a temporary password for the user.
A class for passing options to services.
Defer callable updates to run later in the PHP process.
Sends emails to all accounts associated with that email to reset the password.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
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()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:34
Provides access to user options.
Password reset helper for functionality shared by the web UI and the API.
__construct(private readonly ServiceOptions $config, LoggerInterface $logger, private readonly AuthManager $authManager, HookContainer $hookContainer, private readonly UserIdentityLookup $userIdentityLookup, private readonly UserFactory $userFactory, private readonly UserNameUtils $userNameUtils, private readonly UserOptionsLookup $userOptionsLookup,)
This class is managed by MediaWikiServices, don't instantiate directly.
execute(User $performingUser, $username=null, $email=null)
Do a password reset.
isAllowed(User $user)
Check if a given user has permission to use this functionality.
Create User objects.
UserNameUtils service.
User class for the MediaWiki software.
Definition User.php:130
getBlock( $freshness=IDBAccessObject::READ_NORMAL, $disableIpBlockExemptChecking=false)
Get the block affecting the user, or null if the user is not blocked.
Definition User.php:1426
getRequest()
Get the WebRequest object to use with this object.
Definition User.php:2197
pingLimiter( $action='edit', $incrBy=1)
Primitive rate limits: enforce maximum actions per time period to put a brake on flooding.
Definition User.php:1397
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1525
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.
Store key-value entries in a size-limited in-memory LRU cache.
Service for looking up UserIdentity.