Exemplo n.º 1
0
 /**
  * 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);
 }