Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
99.49% |
194 / 195 |
|
94.12% |
16 / 17 |
CRAP | |
0.00% |
0 / 1 |
| AbstractTemporaryPasswordPrimaryAuthenticationProvider | |
99.49% |
194 / 195 |
|
94.12% |
16 / 17 |
76 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
| postInitSetup | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| getPasswordResetData | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| getAuthenticationRequests | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
9 | |||
| beginPrimaryAuthentication | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
9 | |||
| testUserCanAuthenticate | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
| providerAllowsAuthenticationDataChange | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
14 | |||
| providerChangeAuthenticationData | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
6 | |||
| accountCreationType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| testForAccountCreation | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
| beginPrimaryAccountCreation | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
| finishAccountCreation | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
| isTimestampValid | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| maybeSendNewAccountEmail | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| sendNewAccountEmail | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
| maybeSendPasswordResetEmail | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| sendPasswordResetEmail | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
5 | |||
| getTemporaryPassword | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| setTemporaryPassword | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\Auth; |
| 8 | |
| 9 | use MediaWiki\MainConfigNames; |
| 10 | use MediaWiki\Password\InvalidPassword; |
| 11 | use MediaWiki\Password\Password; |
| 12 | use MediaWiki\Password\PasswordFactory; |
| 13 | use MediaWiki\SpecialPage\SpecialPage; |
| 14 | use MediaWiki\Title\Title; |
| 15 | use MediaWiki\User\Options\UserOptionsLookup; |
| 16 | use MediaWiki\User\User; |
| 17 | use MediaWiki\User\UserRigorOptions; |
| 18 | use Wikimedia\IPUtils; |
| 19 | use Wikimedia\Rdbms\IConnectionProvider; |
| 20 | use Wikimedia\Rdbms\IDBAccessObject; |
| 21 | use Wikimedia\Timestamp\TimestampFormat as TS; |
| 22 | |
| 23 | /** |
| 24 | * A primary authentication provider that uses a temporary password. |
| 25 | * |
| 26 | * A successful login will force a password reset. |
| 27 | * |
| 28 | * @note For proper operation, this should generally come before any other |
| 29 | * password-based authentication providers. |
| 30 | * @stable to extend |
| 31 | * @ingroup Auth |
| 32 | * @since 1.43 |
| 33 | */ |
| 34 | abstract class AbstractTemporaryPasswordPrimaryAuthenticationProvider |
| 35 | extends AbstractPasswordPrimaryAuthenticationProvider |
| 36 | { |
| 37 | /** @var bool */ |
| 38 | protected $emailEnabled = null; |
| 39 | |
| 40 | /** @var int */ |
| 41 | protected $newPasswordExpiry = null; |
| 42 | |
| 43 | /** @var int */ |
| 44 | protected $passwordReminderResendTime = null; |
| 45 | |
| 46 | protected IConnectionProvider $dbProvider; |
| 47 | protected UserOptionsLookup $userOptionsLookup; |
| 48 | |
| 49 | /** |
| 50 | * @param IConnectionProvider $dbProvider |
| 51 | * @param UserOptionsLookup $userOptionsLookup |
| 52 | * @param array $params |
| 53 | * - emailEnabled: (bool) must be true for the option to email passwords to be present |
| 54 | * - newPasswordExpiry: (int) expiration time of temporary passwords, in seconds |
| 55 | * - passwordReminderResendTime: (int) cooldown period in hours until a password reminder can |
| 56 | * be sent to the same user again |
| 57 | */ |
| 58 | public function __construct( |
| 59 | IConnectionProvider $dbProvider, |
| 60 | UserOptionsLookup $userOptionsLookup, |
| 61 | $params = [] |
| 62 | ) { |
| 63 | parent::__construct( $params ); |
| 64 | |
| 65 | if ( isset( $params['emailEnabled'] ) ) { |
| 66 | $this->emailEnabled = (bool)$params['emailEnabled']; |
| 67 | } |
| 68 | if ( isset( $params['newPasswordExpiry'] ) ) { |
| 69 | $this->newPasswordExpiry = (int)$params['newPasswordExpiry']; |
| 70 | } |
| 71 | if ( isset( $params['passwordReminderResendTime'] ) ) { |
| 72 | $this->passwordReminderResendTime = $params['passwordReminderResendTime']; |
| 73 | } |
| 74 | $this->dbProvider = $dbProvider; |
| 75 | $this->userOptionsLookup = $userOptionsLookup; |
| 76 | } |
| 77 | |
| 78 | protected function postInitSetup() { |
| 79 | $this->emailEnabled ??= $this->config->get( MainConfigNames::EnableEmail ); |
| 80 | $this->newPasswordExpiry ??= $this->config->get( MainConfigNames::NewPasswordExpiry ); |
| 81 | $this->passwordReminderResendTime ??= |
| 82 | $this->config->get( MainConfigNames::PasswordReminderResendTime ); |
| 83 | } |
| 84 | |
| 85 | /** @inheritDoc */ |
| 86 | protected function getPasswordResetData( $username, $data ) { |
| 87 | // Always reset |
| 88 | return (object)[ |
| 89 | 'msg' => wfMessage( 'resetpass-temp-emailed' ), |
| 90 | 'hard' => true, |
| 91 | ]; |
| 92 | } |
| 93 | |
| 94 | /** @inheritDoc */ |
| 95 | public function getAuthenticationRequests( $action, array $options ) { |
| 96 | switch ( $action ) { |
| 97 | case AuthManager::ACTION_LOGIN: |
| 98 | return [ new PasswordAuthenticationRequest() ]; |
| 99 | |
| 100 | case AuthManager::ACTION_CHANGE: |
| 101 | return [ TemporaryPasswordAuthenticationRequest::newRandom() ]; |
| 102 | |
| 103 | case AuthManager::ACTION_CREATE: |
| 104 | // Allow named users creating a new account to email a temporary password to a given address |
| 105 | // in case they are creating an account for somebody else. |
| 106 | // This isn't a likely scenario for account creations by anonymous or temporary users |
| 107 | // and is therefore disabled for them (T328718). |
| 108 | if ( |
| 109 | isset( $options['username'] ) && |
| 110 | !$this->userNameUtils->isTemp( $options['username'] ) && |
| 111 | $this->emailEnabled |
| 112 | ) { |
| 113 | return [ TemporaryPasswordAuthenticationRequest::newRandom() ]; |
| 114 | } else { |
| 115 | return []; |
| 116 | } |
| 117 | |
| 118 | case AuthManager::ACTION_REMOVE: |
| 119 | return [ new TemporaryPasswordAuthenticationRequest ]; |
| 120 | |
| 121 | default: |
| 122 | return []; |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | /** @inheritDoc */ |
| 127 | public function beginPrimaryAuthentication( array $reqs ) { |
| 128 | $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class ); |
| 129 | if ( !$req || $req->username === null || $req->password === null ) { |
| 130 | return AuthenticationResponse::newAbstain(); |
| 131 | } |
| 132 | |
| 133 | $username = $this->userNameUtils->getCanonical( |
| 134 | $req->username, UserRigorOptions::RIGOR_USABLE ); |
| 135 | if ( $username === false ) { |
| 136 | return AuthenticationResponse::newAbstain(); |
| 137 | } |
| 138 | |
| 139 | [ $tempPassHash, $tempPassTime ] = $this->getTemporaryPassword( $username, IDBAccessObject::READ_LATEST ); |
| 140 | if ( !$tempPassHash ) { |
| 141 | return AuthenticationResponse::newAbstain(); |
| 142 | } |
| 143 | |
| 144 | $status = $this->checkPasswordValidity( $username, $req->password ); |
| 145 | if ( !$status->isOK() ) { |
| 146 | return $this->getFatalPasswordErrorResponse( $username, $status ); |
| 147 | } |
| 148 | |
| 149 | if ( !$tempPassHash->verify( $req->password ) || |
| 150 | !$this->isTimestampValid( $tempPassTime ) |
| 151 | ) { |
| 152 | return $this->failResponse( $req ); |
| 153 | } |
| 154 | |
| 155 | // Add an extra log entry since a temporary password is |
| 156 | // an unusual way to log in, so its important to keep track |
| 157 | // of in case of abuse. |
| 158 | $this->logger->info( "{user} successfully logged in using temp password", |
| 159 | [ |
| 160 | 'provider' => static::class, |
| 161 | 'user' => $username, |
| 162 | 'requestIP' => $this->manager->getRequest()->getIP() |
| 163 | ] |
| 164 | ); |
| 165 | |
| 166 | $this->setPasswordResetFlag( $username, $status ); |
| 167 | |
| 168 | return AuthenticationResponse::newPass( $username ); |
| 169 | } |
| 170 | |
| 171 | /** @inheritDoc */ |
| 172 | public function testUserCanAuthenticate( $username ) { |
| 173 | $username = $this->userNameUtils->getCanonical( $username, UserRigorOptions::RIGOR_USABLE ); |
| 174 | if ( $username === false ) { |
| 175 | return false; |
| 176 | } |
| 177 | |
| 178 | [ $tempPassHash, $tempPassTime ] = $this->getTemporaryPassword( $username ); |
| 179 | return $tempPassHash && |
| 180 | !( $tempPassHash instanceof InvalidPassword ) && |
| 181 | $this->isTimestampValid( $tempPassTime ); |
| 182 | } |
| 183 | |
| 184 | /** @inheritDoc */ |
| 185 | public function providerAllowsAuthenticationDataChange( |
| 186 | AuthenticationRequest $req, $checkData = true |
| 187 | ) { |
| 188 | if ( get_class( $req ) !== TemporaryPasswordAuthenticationRequest::class ) { |
| 189 | // We don't really ignore it, but this is what the caller expects. |
| 190 | return \StatusValue::newGood( 'ignored' ); |
| 191 | } |
| 192 | |
| 193 | if ( !$checkData ) { |
| 194 | return \StatusValue::newGood(); |
| 195 | } |
| 196 | |
| 197 | $username = $this->userNameUtils->getCanonical( |
| 198 | $req->username, UserRigorOptions::RIGOR_USABLE ); |
| 199 | if ( $username === false ) { |
| 200 | return \StatusValue::newGood( 'ignored' ); |
| 201 | } |
| 202 | |
| 203 | [ $tempPassHash, $tempPassTime ] = $this->getTemporaryPassword( $username, IDBAccessObject::READ_LATEST ); |
| 204 | if ( !$tempPassHash ) { |
| 205 | return \StatusValue::newGood( 'ignored' ); |
| 206 | } |
| 207 | |
| 208 | $sv = \StatusValue::newGood(); |
| 209 | if ( $req->password !== null ) { |
| 210 | $sv->merge( $this->checkPasswordValidity( $username, $req->password ) ); |
| 211 | |
| 212 | if ( $req->mailpassword ) { |
| 213 | if ( !$this->emailEnabled ) { |
| 214 | return \StatusValue::newFatal( 'passwordreset-emaildisabled' ); |
| 215 | } |
| 216 | |
| 217 | // We don't check whether the user has an email address; |
| 218 | // that information should not be exposed to the caller. |
| 219 | |
| 220 | // do not allow temporary password creation within |
| 221 | // $wgPasswordReminderResendTime from the last attempt |
| 222 | if ( |
| 223 | $this->passwordReminderResendTime |
| 224 | && $tempPassTime |
| 225 | && time() < (int)wfTimestamp( TS::UNIX, $tempPassTime ) |
| 226 | + $this->passwordReminderResendTime * 3600 |
| 227 | ) { |
| 228 | // Round the time in hours to 3 d.p., in case someone is specifying |
| 229 | // minutes or seconds. |
| 230 | return \StatusValue::newFatal( 'throttled-mailpassword', |
| 231 | round( $this->passwordReminderResendTime, 3 ) ); |
| 232 | } |
| 233 | |
| 234 | if ( !$req->caller ) { |
| 235 | return \StatusValue::newFatal( 'passwordreset-nocaller' ); |
| 236 | } |
| 237 | if ( !IPUtils::isValid( $req->caller ) ) { |
| 238 | $caller = User::newFromName( $req->caller ); |
| 239 | if ( !$caller ) { |
| 240 | return \StatusValue::newFatal( 'passwordreset-nosuchcaller', $req->caller ); |
| 241 | } |
| 242 | } |
| 243 | } |
| 244 | } |
| 245 | return $sv; |
| 246 | } |
| 247 | |
| 248 | public function providerChangeAuthenticationData( AuthenticationRequest $req ) { |
| 249 | $username = $req->username !== null ? |
| 250 | $this->userNameUtils->getCanonical( $req->username, UserRigorOptions::RIGOR_USABLE ) : false; |
| 251 | if ( $username === false ) { |
| 252 | return; |
| 253 | } |
| 254 | |
| 255 | $sendMail = false; |
| 256 | if ( $req->action !== AuthManager::ACTION_REMOVE && |
| 257 | get_class( $req ) === TemporaryPasswordAuthenticationRequest::class |
| 258 | ) { |
| 259 | $tempPassHash = $this->getPasswordFactory()->newFromPlaintext( $req->password ); |
| 260 | $tempPassTime = wfTimestampNow(); |
| 261 | $sendMail = $req->mailpassword; |
| 262 | // Prevent other temp password providers from sending duplicate emails |
| 263 | $req->mailpassword = false; |
| 264 | } else { |
| 265 | // Invalidate the temporary password when any other auth is reset, or when removing |
| 266 | $tempPassHash = PasswordFactory::newInvalidPassword(); |
| 267 | $tempPassTime = null; |
| 268 | } |
| 269 | |
| 270 | $this->setTemporaryPassword( $username, $tempPassHash, $tempPassTime ); |
| 271 | |
| 272 | if ( $sendMail ) { |
| 273 | $this->maybeSendPasswordResetEmail( $req ); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | /** @inheritDoc */ |
| 278 | public function accountCreationType() { |
| 279 | return self::TYPE_CREATE; |
| 280 | } |
| 281 | |
| 282 | /** @inheritDoc */ |
| 283 | public function testForAccountCreation( $user, $creator, array $reqs ) { |
| 284 | /** @var TemporaryPasswordAuthenticationRequest $req */ |
| 285 | $req = AuthenticationRequest::getRequestByClass( |
| 286 | $reqs, TemporaryPasswordAuthenticationRequest::class |
| 287 | ); |
| 288 | |
| 289 | $ret = \StatusValue::newGood(); |
| 290 | if ( $req ) { |
| 291 | if ( $req->mailpassword ) { |
| 292 | if ( !$this->emailEnabled ) { |
| 293 | $ret->merge( \StatusValue::newFatal( 'emaildisabled' ) ); |
| 294 | } elseif ( !$user->getEmail() ) { |
| 295 | $ret->merge( \StatusValue::newFatal( 'noemailcreate' ) ); |
| 296 | } |
| 297 | } |
| 298 | |
| 299 | $ret->merge( |
| 300 | $this->checkPasswordValidity( $user->getName(), $req->password ) |
| 301 | ); |
| 302 | } |
| 303 | return $ret; |
| 304 | } |
| 305 | |
| 306 | /** @inheritDoc */ |
| 307 | public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) { |
| 308 | /** @var TemporaryPasswordAuthenticationRequest $req */ |
| 309 | $req = AuthenticationRequest::getRequestByClass( |
| 310 | $reqs, TemporaryPasswordAuthenticationRequest::class |
| 311 | ); |
| 312 | if ( $req && $req->username !== null && $req->password !== null ) { |
| 313 | // Nothing we can do yet, because the user isn't in the DB yet |
| 314 | if ( $req->username !== $user->getName() ) { |
| 315 | $req = clone $req; |
| 316 | $req->username = $user->getName(); |
| 317 | } |
| 318 | |
| 319 | if ( $req->mailpassword ) { |
| 320 | // prevent EmailNotificationSecondaryAuthenticationProvider from sending another mail |
| 321 | $this->manager->setAuthenticationSessionData( 'no-email', true ); |
| 322 | } |
| 323 | |
| 324 | $ret = AuthenticationResponse::newPass( $req->username ); |
| 325 | $ret->createRequest = $req; |
| 326 | return $ret; |
| 327 | } |
| 328 | return AuthenticationResponse::newAbstain(); |
| 329 | } |
| 330 | |
| 331 | /** @inheritDoc */ |
| 332 | public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) { |
| 333 | /** @var TemporaryPasswordAuthenticationRequest $req */ |
| 334 | $req = $res->createRequest; |
| 335 | $mailpassword = $req->mailpassword; |
| 336 | // Prevent providerChangeAuthenticationData() from sending the wrong email |
| 337 | $req->mailpassword = false; |
| 338 | |
| 339 | // Now that the user is in the DB, set the password on it. |
| 340 | $this->providerChangeAuthenticationData( $req ); |
| 341 | |
| 342 | if ( $mailpassword ) { |
| 343 | $this->maybeSendNewAccountEmail( $user, $creator, $req->password ); |
| 344 | } |
| 345 | |
| 346 | return $mailpassword ? 'byemail' : null; |
| 347 | } |
| 348 | |
| 349 | /** |
| 350 | * Check that a temporary password is still valid (hasn't expired). |
| 351 | * @param string|int|null $timestamp Timestamp in the database's format; null means it doesn't |
| 352 | * expire |
| 353 | * @return bool |
| 354 | */ |
| 355 | protected function isTimestampValid( $timestamp ) { |
| 356 | $time = wfTimestampOrNull( TS::MW, $timestamp ); |
| 357 | if ( $time !== null ) { |
| 358 | $expiry = (int)wfTimestamp( TS::UNIX, $time ) + $this->newPasswordExpiry; |
| 359 | if ( time() >= $expiry ) { |
| 360 | return false; |
| 361 | } |
| 362 | } |
| 363 | return true; |
| 364 | } |
| 365 | |
| 366 | /** |
| 367 | * Wait for the new account to be recorded, and if successful, send an email about the new |
| 368 | * account creation and the temporary password. |
| 369 | * |
| 370 | * If overridden, the override must call sendNewAccountEmail(). |
| 371 | * |
| 372 | * @stable to override |
| 373 | * @param User $user The new user account |
| 374 | * @param User $creatingUser The user who created the account (can be anonymous) |
| 375 | * @param string $password The temporary password |
| 376 | */ |
| 377 | protected function maybeSendNewAccountEmail( User $user, User $creatingUser, $password ): void { |
| 378 | // Send email after DB commit (the callback does not run in case of DB rollback) |
| 379 | $this->dbProvider->getPrimaryDatabase()->onTransactionCommitOrIdle( |
| 380 | function () use ( $user, $creatingUser, $password ) { |
| 381 | $this->sendNewAccountEmail( $user, $creatingUser, $password ); |
| 382 | }, |
| 383 | __METHOD__ |
| 384 | ); |
| 385 | } |
| 386 | |
| 387 | /** |
| 388 | * Send an email about the new account creation and the temporary password. |
| 389 | * |
| 390 | * @stable to override |
| 391 | * @param User $user The new user account |
| 392 | * @param User $creatingUser The user who created the account (can be anonymous) |
| 393 | * @param string $password The temporary password |
| 394 | */ |
| 395 | protected function sendNewAccountEmail( User $user, User $creatingUser, $password ): void { |
| 396 | $ip = $creatingUser->getRequest()->getIP(); |
| 397 | // @codeCoverageIgnoreStart |
| 398 | if ( !$ip ) { |
| 399 | return; |
| 400 | } |
| 401 | // @codeCoverageIgnoreEnd |
| 402 | |
| 403 | $this->getHookRunner()->onUser__mailPasswordInternal( $creatingUser, $ip, $user ); |
| 404 | |
| 405 | $mainPageUrl = Title::newMainPage()->getCanonicalURL(); |
| 406 | $userLanguage = $this->userOptionsLookup->getOption( $user, 'language' ); |
| 407 | $subjectMessage = wfMessage( 'createaccount-title' ); |
| 408 | $bodyMessage = wfMessage( 'createaccount-text', $ip, $user->getName(), $password, |
| 409 | '<' . $mainPageUrl . '>', round( $this->newPasswordExpiry / 86400 ) ); |
| 410 | |
| 411 | $this->getHookRunner()->onUserModifyCreateAccountEmail( |
| 412 | $user, |
| 413 | $creatingUser, |
| 414 | $subjectMessage, |
| 415 | $bodyMessage |
| 416 | ); |
| 417 | |
| 418 | $status = $user->sendMail( |
| 419 | $subjectMessage->inLanguage( $userLanguage )->text(), |
| 420 | $bodyMessage->inLanguage( $userLanguage )->text() |
| 421 | ); |
| 422 | |
| 423 | // @codeCoverageIgnoreStart |
| 424 | if ( !$status->isGood() ) { |
| 425 | $this->logger->warning( 'Could not send account creation email: ' . |
| 426 | $status->getWikiText( false, false, 'en' ) ); |
| 427 | } |
| 428 | // @codeCoverageIgnoreEnd |
| 429 | } |
| 430 | |
| 431 | /** |
| 432 | * Wait for the new temporary password to be recorded, and if successful, send an email about it. |
| 433 | * |
| 434 | * If overridden, the override must call sendPasswordResetEmail(). |
| 435 | * |
| 436 | * @stable to override |
| 437 | * @param TemporaryPasswordAuthenticationRequest $req |
| 438 | */ |
| 439 | protected function maybeSendPasswordResetEmail( TemporaryPasswordAuthenticationRequest $req ): void { |
| 440 | // Send email after DB commit (the callback does not run in case of DB rollback) |
| 441 | $this->dbProvider->getPrimaryDatabase()->onTransactionCommitOrIdle( |
| 442 | function () use ( $req ) { |
| 443 | $this->sendPasswordResetEmail( $req ); |
| 444 | }, |
| 445 | __METHOD__ |
| 446 | ); |
| 447 | } |
| 448 | |
| 449 | /** |
| 450 | * Send an email about the new temporary password. |
| 451 | * |
| 452 | * @stable to override |
| 453 | * @param TemporaryPasswordAuthenticationRequest $req |
| 454 | */ |
| 455 | protected function sendPasswordResetEmail( TemporaryPasswordAuthenticationRequest $req ): void { |
| 456 | $user = User::newFromName( $req->username ); |
| 457 | if ( !$user ) { |
| 458 | return; |
| 459 | } |
| 460 | $userLanguage = $this->userOptionsLookup->getOption( $user, 'language' ); |
| 461 | $callerIsAnon = IPUtils::isValid( $req->caller ); |
| 462 | $callerName = $callerIsAnon ? $req->caller : User::newFromName( $req->caller )->getName(); |
| 463 | $passwordMessage = wfMessage( 'passwordreset-emailelement', $user->getName(), |
| 464 | $req->password )->inLanguage( $userLanguage ); |
| 465 | $emailMessage = wfMessage( $callerIsAnon ? 'passwordreset-emailtext-ip' |
| 466 | : 'passwordreset-emailtext-user' )->inLanguage( $userLanguage ); |
| 467 | $body = $emailMessage->params( $callerName, $passwordMessage, 1, |
| 468 | '<' . Title::newMainPage()->getCanonicalURL() . '>', |
| 469 | round( $this->newPasswordExpiry / 86400 ) )->text(); |
| 470 | |
| 471 | // Hint that the user can choose to require email address to request a temporary password |
| 472 | if ( |
| 473 | !$this->userOptionsLookup->getBoolOption( $user, 'requireemail' ) |
| 474 | ) { |
| 475 | $url = SpecialPage::getTitleFor( 'Preferences', false, 'mw-prefsection-personal-email' ) |
| 476 | ->getCanonicalURL(); |
| 477 | $body .= "\n\n" . wfMessage( 'passwordreset-emailtext-require-email' ) |
| 478 | ->inLanguage( $userLanguage ) |
| 479 | ->params( "<$url>" ) |
| 480 | ->text(); |
| 481 | } |
| 482 | |
| 483 | $emailTitle = wfMessage( 'passwordreset-emailtitle' )->inLanguage( $userLanguage ); |
| 484 | $user->sendMail( $emailTitle->text(), $body ); |
| 485 | } |
| 486 | |
| 487 | /** |
| 488 | * Return a tuple of temporary password and the time when it was generated. |
| 489 | * |
| 490 | * The password may be an InvalidPassword to represent that it was unset, or null if the user |
| 491 | * can't authenticate for other reasons. |
| 492 | * |
| 493 | * The time is a a timestamp in the database's format or null (use wfTimestampOrNull() to parse |
| 494 | * it). If it's null, the password doesn't expire. Otherwise, the password should be considered |
| 495 | * expired after $wgNewPasswordExpiry seconds since that time. |
| 496 | * |
| 497 | * @stable to override |
| 498 | * @param string $username Canonical username |
| 499 | * @param int $flags Bitfield of IDBAccessObject::READ_* constants |
| 500 | * @return array |
| 501 | * @phan-return array{0:?Password, 1:?string|int} |
| 502 | */ |
| 503 | abstract protected function getTemporaryPassword( string $username, $flags = IDBAccessObject::READ_NORMAL ): array; |
| 504 | |
| 505 | /** |
| 506 | * Set a temporary password and the time when it was generated. |
| 507 | * |
| 508 | * @param string $username Canonical username |
| 509 | * @param Password $tempPassHash Password, or an InvalidPassword to unset |
| 510 | * @param string|int|null $tempPassTime Timestamp in a format accepted by wfTimestampOrNull(); |
| 511 | * null means it doesn't expire |
| 512 | */ |
| 513 | abstract protected function setTemporaryPassword( string $username, Password $tempPassHash, $tempPassTime ): void; |
| 514 | } |