canChangePassword( $user ) === true && parent::userCanExecute( $user ); } public function checkExecutePermissions( User $user ) { $error = $this->canChangePassword( $user ); if ( is_string( $error ) ) { throw new ErrorPageError( 'internalerror', $error ); } elseif ( !$error ) { throw new ErrorPageError( 'internalerror', 'resetpass_forbidden' ); } parent::checkExecutePermissions( $user ); } protected function getFormFields() { global $wgAuth; $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' ); $a = []; if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) { $a['Username'] = [ 'type' => 'text', 'label-message' => 'passwordreset-username', ]; if ( $this->getUser()->isLoggedIn() ) { $a['Username']['default'] = $this->getUser()->getName(); } } if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) { $a['Email'] = [ 'type' => 'email', 'label-message' => 'passwordreset-email', ]; } if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) { $domains = $wgAuth->domainList(); $a['Domain'] = [ 'type' => 'select', 'options' => $domains, 'label-message' => 'passwordreset-domain', ]; } if ( $this->getUser()->isAllowed( 'passwordreset' ) ) { $a['Capture'] = [ 'type' => 'check', 'label-message' => 'passwordreset-capture', 'help-message' => 'passwordreset-capture-help', ]; } return $a; } protected function getDisplayFormat() { return 'ooui'; } public function alterForm( HTMLForm $form ) { $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' ); $form->addHiddenFields( $this->getRequest()->getValues( 'returnto', 'returntoquery' ) ); $i = 0; if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) { $i++; } if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) { $i++; } if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) { $i++; } $message = ( $i > 1 ) ? 'passwordreset-text-many' : 'passwordreset-text-one'; $form->setHeaderText( $this->msg( $message, $i )->parseAsBlock() ); $form->setSubmitTextMsg( 'mailmypassword' ); } /** * 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 array $data * @throws MWException * @throws ThrottledError|PermissionsError * @return bool|array */ public function onSubmit( array $data ) { global $wgAuth, $wgMinimalPasswordLength; if ( isset( $data['Domain'] ) ) { if ( $wgAuth->validDomain( $data['Domain'] ) ) { $wgAuth->setDomain( $data['Domain'] ); } else { $wgAuth->setDomain( 'invaliddomain' ); } } if ( isset( $data['Capture'] ) && !$this->getUser()->isAllowed( 'passwordreset' ) ) { // The user knows they don't have the passwordreset permission, // but they tried to spoof the form. That's naughty throw new PermissionsError( 'passwordreset' ); } /** * @var $firstUser User * @var $users User[] */ if ( isset( $data['Username'] ) && $data['Username'] !== '' ) { $method = 'username'; $users = [ User::newFromName( $data['Username'] ) ]; } elseif ( isset( $data['Email'] ) && $data['Email'] !== '' && Sanitizer::validateEmail( $data['Email'] ) ) { $method = 'email'; $res = wfGetDB( DB_SLAVE )->select( 'user', User::selectFields(), [ 'user_email' => $data['Email'] ], __METHOD__ ); if ( $res ) { $users = []; foreach ( $res as $row ) { $users[] = User::newFromRow( $row ); } } else { // Some sort of database error, probably unreachable throw new MWException( 'Unknown database error in ' . __METHOD__ ); } } else { // The user didn't supply any data return false; } // Check for hooks (captcha etc), and allow them to modify the users list $error = []; if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', [ &$users, $data, &$error ] ) ) { return [ $error ]; } $this->method = $method; if ( count( $users ) == 0 ) { if ( $method == 'email' ) { // Don't reveal whether or not an email address is in use return true; } else { return [ 'noname' ]; } } $firstUser = $users[0]; if ( !$firstUser instanceof User || !$firstUser->getId() ) { // Don't parse username as wikitext (bug 65501) return [ [ 'nosuchuser', wfEscapeWikiText( $data['Username'] ) ] ]; } // Check against the rate limiter if ( $this->getUser()->pingLimiter( 'mailpassword' ) ) { throw new ThrottledError; } // Check against password throttle foreach ( $users as $user ) { if ( $user->isPasswordReminderThrottled() ) { # Round the time in hours to 3 d.p., in case someone is specifying # minutes or seconds. return [ [ 'throttled-mailpassword', round( $this->getConfig()->get( 'PasswordReminderResendTime' ), 3 ) ] ]; } } // 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 [ [ '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 = $this->getRequest()->getIP(); if ( !$ip ) { return [ 'badipaddress' ]; } $caller = $this->getUser(); Hooks::run( 'User::mailPasswordInternal', [ &$caller, &$ip, &$firstUser ] ); $username = $caller->getName(); $msg = IP::isValid( $username ) ? 'passwordreset-emailtext-ip' : 'passwordreset-emailtext-user'; // Send in the user's language; which should hopefully be the same $userLanguage = $firstUser->getOption( 'language' ); $passwords = []; foreach ( $users as $user ) { $password = PasswordFactory::generateRandomPasswordString( $wgMinimalPasswordLength ); $user->setNewpassword( $password ); $user->saveSettings(); $passwords[] = $this->msg( 'passwordreset-emailelement', $user->getName(), $password ) ->inLanguage( $userLanguage )->text(); // We'll escape the whole thing later } $passwordBlock = implode( "\n\n", $passwords ); $this->email = $this->msg( $msg )->inLanguage( $userLanguage ); $this->email->params( $username, $passwordBlock, count( $passwords ), '<' . Title::newMainPage()->getCanonicalURL() . '>', round( $this->getConfig()->get( 'NewPasswordExpiry' ) / 86400 ) ); $title = $this->msg( 'passwordreset-emailtitle' )->inLanguage( $userLanguage ); $this->result = $firstUser->sendMail( $title->text(), $this->email->text() ); if ( isset( $data['Capture'] ) && $data['Capture'] ) { // Save the user, will be used if an error occurs when sending the email $this->firstUser = $firstUser; } else { // Blank the email if the user is not supposed to see it $this->email = null; } if ( $this->result->isGood() ) { return true; } elseif ( isset( $data['Capture'] ) && $data['Capture'] ) { // The email didn't send, but maybe they knew that and that's why they captured it return true; } else { // @todo FIXME: The email wasn't sent, but we have already set // the password throttle timestamp, so they won't be able to try // again until it expires... :( return [ [ 'mailerror', $this->result->getMessage() ] ]; } } public function onSuccess() { if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->email != null ) { // @todo Logging if ( $this->result->isGood() ) { $this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture' ); } else { $this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture', $this->result->getMessage(), $this->firstUser->getName() ); } $this->getOutput()->addHTML( Html::rawElement( 'pre', [], $this->email->escaped() ) ); } if ( $this->method === 'email' ) { $this->getOutput()->addWikiMsg( 'passwordreset-emailsentemail' ); } else { $this->getOutput()->addWikiMsg( 'passwordreset-emailsentusername' ); } $this->getOutput()->returnToMain(); } protected function canChangePassword( User $user ) { global $wgAuth; $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' ); // Maybe password resets are disabled, or there are no allowable routes if ( !is_array( $resetRoutes ) || !in_array( true, array_values( $resetRoutes ) ) ) { return 'passwordreset-disabled'; } // Maybe the external auth plugin won't allow local password changes if ( !$wgAuth->allowPasswordChange() ) { return 'resetpass_forbidden'; } // Maybe email features have been disabled if ( !$this->getConfig()->get( 'EnableEmail' ) ) { return 'passwordreset-emaildisabled'; } // Maybe the user is blocked (check this here rather than relying on the parent // method as we have a more specific error message to use here if ( $user->isBlocked() ) { return 'blocked-mailpassword'; } return true; } /** * Hide the password reset page if resets are disabled. * @return bool */ function isListed() { if ( $this->canChangePassword( $this->getUser() ) === true ) { return parent::isListed(); } return false; } protected function getGroupName() { return 'users'; } }