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 ServiceOptions $config;
39 private AuthManager $authManager;
40 private HookRunner $hookRunner;
41 private UserIdentityLookup $userIdentityLookup;
42 private UserFactory $userFactory;
43 private UserNameUtils $userNameUtils;
44 private UserOptionsLookup $userOptionsLookup;
45
50 private MapCacheLRU $permissionCache;
51
55 public const CONSTRUCTOR_OPTIONS = [
58 ];
59
72 public function __construct(
73 ServiceOptions $config,
74 LoggerInterface $logger,
75 AuthManager $authManager,
76 HookContainer $hookContainer,
77 UserIdentityLookup $userIdentityLookup,
78 UserFactory $userFactory,
79 UserNameUtils $userNameUtils,
80 UserOptionsLookup $userOptionsLookup
81 ) {
82 $config->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
83
84 $this->config = $config;
85 $this->logger = $logger;
86
87 $this->authManager = $authManager;
88 $this->hookRunner = new HookRunner( $hookContainer );
89 $this->userIdentityLookup = $userIdentityLookup;
90 $this->userFactory = $userFactory;
91 $this->userNameUtils = $userNameUtils;
92 $this->userOptionsLookup = $userOptionsLookup;
93
94 $this->permissionCache = new MapCacheLRU( 1 );
95 }
96
103 public function isAllowed( User $user ) {
104 return $this->permissionCache->getWithSetCallback(
105 $user->getName(),
106 function () use ( $user ) {
107 return $this->computeIsAllowed( $user );
108 }
109 );
110 }
111
116 public function isEnabled(): StatusValue {
117 $resetRoutes = $this->config->get( MainConfigNames::PasswordResetRoutes );
118 if ( !is_array( $resetRoutes ) || !in_array( true, $resetRoutes, true ) ) {
119 // Maybe password resets are disabled, or there are no allowable routes
120 return StatusValue::newFatal( 'passwordreset-disabled' );
121 }
122
123 $providerStatus = $this->authManager->allowsAuthenticationDataChange(
125 if ( !$providerStatus->isGood() ) {
126 // Maybe the external auth plugin won't allow local password changes
127 return StatusValue::newFatal( 'resetpass_forbidden-reason',
128 $providerStatus->getMessage() );
129 }
130 if ( !$this->config->get( MainConfigNames::EnableEmail ) ) {
131 // Maybe email features have been disabled
132 return StatusValue::newFatal( 'passwordreset-emaildisabled' );
133 }
134 return StatusValue::newGood();
135 }
136
137 private function computeIsAllowed( User $user ): StatusValue {
138 $enabledStatus = $this->isEnabled();
139 if ( !$enabledStatus->isGood() ) {
140 return $enabledStatus;
141 }
142 if ( !$user->isAllowed( 'editmyprivateinfo' ) ) {
143 // Maybe not all users have permission to change private data
144 return StatusValue::newFatal( 'badaccess' );
145 }
146 if ( $this->isBlocked( $user ) ) {
147 // Maybe the user is blocked (check this here rather than relying on the parent
148 // method as we have a more specific error message to use here, and we want to
149 // ignore some types of blocks)
150 return StatusValue::newFatal( 'blocked-mailpassword' );
151 }
152 return StatusValue::newGood();
153 }
154
170 public function execute(
171 User $performingUser,
172 $username = null,
173 $email = null
174 ) {
175 if ( !$this->isAllowed( $performingUser )->isGood() ) {
176 throw new LogicException(
177 'User ' . $performingUser->getName() . ' is not allowed to reset passwords'
178 );
179 }
180
181 // Check against the rate limiter. If the $wgRateLimit is reached, we want to pretend
182 // that the request was good to avoid displaying an error message.
183 if ( $performingUser->pingLimiter( 'mailpassword' ) ) {
184 return StatusValue::newGood();
185 }
186
187 // We need to have a valid IP address for the hook 'User::mailPasswordInternal', but per T20347,
188 // we should send the user's name if they're logged in.
189 $ip = $performingUser->getRequest()->getIP();
190 if ( !$ip ) {
191 return StatusValue::newFatal( 'badipaddress' );
192 }
193
194 $resetRoutes = $this->config->get( MainConfigNames::PasswordResetRoutes )
195 + [ 'username' => false, 'email' => false ];
196 if ( !$resetRoutes['username'] || $username === '' ) {
197 $username = null;
198 }
199 if ( !$resetRoutes['email'] || $email === '' ) {
200 $email = null;
201 }
202
203 if ( $username !== null && !$this->userNameUtils->getCanonical( $username ) ) {
204 return StatusValue::newFatal( 'noname' );
205 }
206 if ( $email !== null && !Sanitizer::validateEmail( $email ) ) {
207 return StatusValue::newFatal( 'passwordreset-invalidemail' );
208 }
209 // At this point, $username and $email are either valid or not provided
210
212 $users = [];
213
214 if ( $username !== null ) {
215 $user = $this->userFactory->newFromName( $username );
216 // User must have an email address to attempt sending a password reset email
217 if ( $user && $user->isRegistered() && $user->getEmail() && (
218 !$this->userOptionsLookup->getBoolOption( $user, 'requireemail' ) ||
219 $user->getEmail() === $email
220 ) ) {
221 // Either providing the email in the form is not required to request a reset,
222 // or the correct email was provided
223 $users[] = $user;
224 }
225
226 } elseif ( $email !== null ) {
227 foreach ( $this->getUsersByEmail( $email ) as $userIdent ) {
228 // Skip users whose preference 'requireemail' is on since the username was not submitted
229 if ( $this->userOptionsLookup->getBoolOption( $userIdent, 'requireemail' ) ) {
230 continue;
231 }
232 $users[] = $this->userFactory->newFromUserIdentity( $userIdent );
233 }
234
235 } else {
236 // The user didn't supply any data
237 return StatusValue::newFatal( 'passwordreset-nodata' );
238 }
239
240 // Check for hooks (captcha etc.), and allow them to modify the list of users
241 $data = [
242 'Username' => $username,
243 'Email' => $email,
244 ];
245
246 $error = [];
247 if ( !$this->hookRunner->onSpecialPasswordResetOnSubmit( $users, $data, $error ) ) {
248 return StatusValue::newFatal( Message::newFromSpecifier( $error ) );
249 }
250
251 if ( !$users ) {
252 // Don't reveal whether a username or email address is in use
253 return StatusValue::newGood();
254 }
255
256 // Get the first element in $users by using `reset` function since
257 // the key '0' might have been unset from $users array by a hook handler.
258 $firstUser = reset( $users );
259
260 $this->hookRunner->onUser__mailPasswordInternal( $performingUser, $ip, $firstUser );
261
262 $result = StatusValue::newGood();
263 $reqs = [];
264 foreach ( $users as $user ) {
265 $req = TemporaryPasswordAuthenticationRequest::newRandom();
266 $req->username = $user->getName();
267 $req->mailpassword = true;
268 $req->caller = $performingUser->getName();
269
270 $status = $this->authManager->allowsAuthenticationDataChange( $req, true );
271 // If the status is good and the value is 'throttled-mailpassword', we want to pretend
272 // that the request was good to avoid displaying an error message and disclose
273 // if a reset password was previously sent.
274 if ( $status->isGood() && $status->getValue() === 'throttled-mailpassword' ) {
275 return StatusValue::newGood();
276 }
277
278 if ( $status->isGood() && $status->getValue() !== 'ignored' ) {
279 $reqs[] = $req;
280 } elseif ( $result->isGood() ) {
281 // only record the first error, to avoid exposing the number of users having the
282 // same email address
283 if ( $status->getValue() === 'ignored' ) {
284 $status = StatusValue::newFatal( 'passwordreset-ignored' );
285 }
286 $result->merge( $status );
287 }
288 }
289
290 $logContext = [
291 'requestingIp' => $ip,
292 'requestingUser' => $performingUser->getName(),
293 'targetUsername' => $username,
294 'targetEmail' => $email,
295 ];
296
297 if ( !$result->isGood() ) {
298 $this->logger->info(
299 "{requestingUser} attempted password reset of {targetUsername} but failed",
300 $logContext + [ 'errors' => $result->getErrors() ]
301 );
302 return $result;
303 }
304
305 DeferredUpdates::addUpdate(
306 new SendPasswordResetEmailUpdate( $this->authManager, $reqs, $logContext ),
307 DeferredUpdates::POSTSEND
308 );
309
310 return StatusValue::newGood();
311 }
312
320 private function isBlocked( User $user ) {
321 $block = $user->getBlock();
322 return $block && $block->appliesToPasswordReset();
323 }
324
332 protected function getUsersByEmail( $email ) {
333 return $this->userIdentityLookup->newSelectQueryBuilder()
334 ->join( 'user', null, [ "actor_user=user_id" ] )
335 ->where( [ 'user_email' => $email ] )
336 ->caller( __METHOD__ )
337 ->fetchUserIdentities();
338 }
339
340}
341
343class_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.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
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:32
Provides access to user options.
Password reset helper for functionality shared by the web UI and the API.
__construct(ServiceOptions $config, LoggerInterface $logger, AuthManager $authManager, HookContainer $hookContainer, UserIdentityLookup $userIdentityLookup, UserFactory $userFactory, UserNameUtils $userNameUtils, 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:1425
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:1396
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1524
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.