/** * @dataProvider provideAllowsAuthenticationDataChange * @param StatusValue $primaryReturn * @param StatusValue $secondaryReturn * @param Status $expect */ public function testAllowsAuthenticationDataChange($primaryReturn, $secondaryReturn, $expect) { $req = $this->getMockForAbstractClass(AuthenticationRequest::class); $mock1 = $this->getMockForAbstractClass(PrimaryAuthenticationProvider::class); $mock1->expects($this->any())->method('getUniqueId')->will($this->returnValue('1')); $mock1->expects($this->any())->method('providerAllowsAuthenticationDataChange')->with($this->equalTo($req))->will($this->returnValue($primaryReturn)); $mock2 = $this->getMockForAbstractClass(SecondaryAuthenticationProvider::class); $mock2->expects($this->any())->method('getUniqueId')->will($this->returnValue('2')); $mock2->expects($this->any())->method('providerAllowsAuthenticationDataChange')->with($this->equalTo($req))->will($this->returnValue($secondaryReturn)); $this->primaryauthMocks = [$mock1]; $this->secondaryauthMocks = [$mock2]; $this->initializeManager(true); $this->assertEquals($expect, $this->manager->allowsAuthenticationDataChange($req)); }
/** * 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); }