/** * Do a password reset. Authorization is the caller's responsibility. * * Process the form. At this point we know that the user passes all the criteria in * userCanExecute(), and if the data array contains 'Username', etc, then Username * resets are allowed. * @param User $performingUser The user that does the password reset * @param string $username The user whose password is reset * @param string $email Alternative way to specify the user * @param bool $displayPassword Whether to display the password * @return StatusValue Will contain the passwords as a username => password array if the * $displayPassword flag was set * @throws LogicException When the user is not allowed to perform the action * @throws MWException On unexpected DB errors */ public function execute(User $performingUser, $username = null, $email = null, $displayPassword = false) { if (!$this->isAllowed($performingUser, $displayPassword)->isGood()) { $action = $this->isAllowed($performingUser)->isGood() ? 'display' : 'reset'; throw new LogicException('User ' . $performingUser->getName() . ' is not allowed to ' . $action . ' passwords'); } $resetRoutes = $this->config->get('PasswordResetRoutes') + ['username' => false, 'email' => false]; if ($resetRoutes['username'] && $username) { $method = 'username'; $users = [User::newFromName($username)]; } elseif ($resetRoutes['email'] && $email) { if (!Sanitizer::validateEmail($email)) { return StatusValue::newFatal('passwordreset-invalidemail'); } $method = 'email'; $users = $this->getUsersByEmail($email); } else { // The user didn't supply any data return StatusValue::newFatal('passwordreset-nodata'); } // Check for hooks (captcha etc), and allow them to modify the users list $error = []; $data = ['Username' => $username, 'Email' => $email, 'Capture' => $displayPassword ? '1' : null]; if (!Hooks::run('SpecialPasswordResetOnSubmit', [&$users, $data, &$error])) { return StatusValue::newFatal(Message::newFromSpecifier($error)); } if (!$users) { if ($method === 'email') { // Don't reveal whether or not an email address is in use return StatusValue::newGood([]); } else { return StatusValue::newFatal('noname'); } } $firstUser = $users[0]; if (!$firstUser instanceof User || !$firstUser->getId()) { // Don't parse username as wikitext (bug 65501) 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 return StatusValue::newFatal(wfMessage('noemail', wfEscapeWikiText($firstUser->getName()))); } // We need to have a valid IP address for the hook, but per bug 18347, we should // send the user's name if they're logged in. $ip = $performingUser->getRequest()->getIP(); if (!$ip) { return StatusValue::newFatal('badipaddress'); } Hooks::run('User::mailPasswordInternal', [&$performingUser, &$ip, &$firstUser]); $result = StatusValue::newGood(); $reqs = []; foreach ($users as $user) { $req = TemporaryPasswordAuthenticationRequest::newRandom(); $req->username = $user->getName(); $req->mailpassword = true; $req->hasBackchannel = $displayPassword; $req->caller = $performingUser->getName(); $status = $this->authManager->allowsAuthenticationDataChange($req, true); if ($status->isGood() && $status->getValue() !== 'ignored') { $reqs[] = $req; } elseif ($result->isGood()) { // only record the first error, to avoid exposing the number of users having the // same email address if ($status->getValue() === 'ignored') { $status = StatusValue::newFatal('passwordreset-ignored'); } $result->merge($status); } } if (!$result->isGood()) { return $result; } $passwords = []; foreach ($reqs as $req) { $this->authManager->changeAuthenticationData($req); // TODO record mail sending errors if ($displayPassword) { $passwords[$req->username] = $req->password; } } return StatusValue::newGood($passwords); }
public function getAuthenticationRequests($action, array $options) { switch ($action) { case AuthManager::ACTION_LOGIN: return [new PasswordAuthenticationRequest()]; case AuthManager::ACTION_CHANGE: return [TemporaryPasswordAuthenticationRequest::newRandom()]; case AuthManager::ACTION_CREATE: if (isset($options['username']) && $this->emailEnabled) { // Creating an account for someone else return [TemporaryPasswordAuthenticationRequest::newRandom()]; } else { // It's not terribly likely that an anonymous user will // be creating an account for someone else. return []; } case AuthManager::ACTION_REMOVE: return [new TemporaryPasswordAuthenticationRequest()]; default: return []; } }
public function testAccountCreationEmail() { $creator = \User::newFromName('Foo'); $user = self::getMutableTestUser()->getUser(); $user->setEmail(null); $req = TemporaryPasswordAuthenticationRequest::newRandom(); $req->username = $user->getName(); $req->mailpassword = true; $provider = $this->getProvider(['emailEnabled' => false]); $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertEquals(\StatusValue::newFatal('emaildisabled'), $status); $req->hasBackchannel = true; $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertFalse($status->hasMessage('emaildisabled')); $req->hasBackchannel = false; $provider = $this->getProvider(['emailEnabled' => true]); $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertEquals(\StatusValue::newFatal('noemailcreate'), $status); $req->hasBackchannel = true; $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertFalse($status->hasMessage('noemailcreate')); $req->hasBackchannel = false; $user->setEmail('*****@*****.**'); $status = $provider->testForAccountCreation($user, $creator, [$req]); $this->assertEquals(\StatusValue::newGood(), $status); $mailed = false; $resetMailer = $this->hookMailer(function ($headers, $to, $from, $subject, $body) use(&$mailed, $req) { $mailed = true; $this->assertSame('*****@*****.**', $to[0]->address); $this->assertContains($req->password, $body); return false; }); $expect = AuthenticationResponse::newPass($user->getName()); $expect->createRequest = clone $req; $expect->createRequest->username = $user->getName(); $res = $provider->beginPrimaryAccountCreation($user, $creator, [$req]); $this->assertEquals($expect, $res); $this->assertTrue($this->manager->getAuthenticationSessionData('no-email')); $this->assertFalse($mailed); $this->assertSame('byemail', $provider->finishAccountCreation($user, $creator, $res)); $this->assertTrue($mailed); ScopedCallback::consume($resetMailer); $this->assertTrue($mailed); }