* @ingroup SpecialPage
*/
+use MediaWiki\Auth\AuthManager;
+
/**
- * Special page for requesting a password reset email
+ * Special page for requesting a password reset email.
+ *
+ * Requires the TemporaryPasswordPrimaryAuthenticationProvider and the
+ * EmailNotificationSecondaryAuthenticationProvider (or something providing equivalent
+ * functionality) to be enabled.
*
* @ingroup SpecialPage
*/
class SpecialPasswordReset extends FormSpecialPage {
- /**
- * @var Message
- */
- private $email;
+ /** @var PasswordReset */
+ private $passwordReset;
/**
- * @var User
+ * @var string[] Temporary storage for the passwords which have been sent out, keyed by username.
*/
- private $firstUser;
+ private $passwords = [];
/**
* @var Status
public function __construct() {
parent::__construct( 'PasswordReset', 'editmyprivateinfo' );
+ $this->passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
}
public function doesWrites() {
}
public function userCanExecute( User $user ) {
- return $this->canChangePassword( $user ) === true && parent::userCanExecute( $user );
+ return $this->passwordReset->isAllowed( $user )->isGood();
}
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' );
+ $status = Status::wrap( $this->passwordReset->isAllowed( $user ) );
+ if ( !$status->isGood() ) {
+ throw new ErrorPageError( 'internalerror', $status->getMessage() );
}
parent::checkExecutePermissions( $user );
}
protected function getFormFields() {
- global $wgAuth;
$resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
- $a = array();
+ $a = [];
if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) {
- $a['Username'] = array(
+ $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'] = array(
+ $a['Email'] = [
'type' => 'email',
'label-message' => 'passwordreset-email',
- );
- }
-
- if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
- $domains = $wgAuth->domainList();
- $a['Domain'] = array(
- 'type' => 'select',
- 'options' => $domains,
- 'label-message' => 'passwordreset-domain',
- );
+ ];
}
if ( $this->getUser()->isAllowed( 'passwordreset' ) ) {
- $a['Capture'] = array(
+ $a['Capture'] = [
'type' => 'check',
'label-message' => 'passwordreset-capture',
'help-message' => 'passwordreset-capture-help',
- );
+ ];
}
return $a;
if ( isset( $resetRoutes['email'] ) && $resetRoutes['email'] ) {
$i++;
}
- if ( isset( $resetRoutes['domain'] ) && $resetRoutes['domain'] ) {
- $i++;
- }
$message = ( $i > 1 ) ? 'passwordreset-text-many' : 'passwordreset-text-one';
* @param array $data
* @throws MWException
* @throws ThrottledError|PermissionsError
- * @return bool|array
+ * @return Status
*/
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 = array( User::newFromName( $data['Username'] ) );
- } elseif ( isset( $data['Email'] )
- && $data['Email'] !== ''
- && Sanitizer::validateEmail( $data['Email'] )
- ) {
- $method = 'email';
- $res = wfGetDB( DB_SLAVE )->select(
- 'user',
- User::selectFields(),
- array( 'user_email' => $data['Email'] ),
- __METHOD__
- );
-
- if ( $res ) {
- $users = array();
-
- 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 = array();
- if ( !Hooks::run( 'SpecialPasswordResetOnSubmit', array( &$users, $data, &$error ) ) ) {
- return array( $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 array( 'noname' );
- }
- }
+ $username = isset( $data['Username'] ) ? $data['Username'] : null;
+ $email = isset( $data['Email'] ) ? $data['Email'] : null;
+ $capture = !empty( $data['Capture'] );
- $firstUser = $users[0];
-
- if ( !$firstUser instanceof User || !$firstUser->getID() ) {
- // Don't parse username as wikitext (bug 65501)
- return array( array( 'nosuchuser', wfEscapeWikiText( $data['Username'] ) ) );
+ $this->method = $username ? 'username' : 'email';
+ $this->result = Status::wrap(
+ $this->passwordReset->execute( $this->getUser(), $username, $email, $capture ) );
+ if ( $capture && $this->result->isOK() ) {
+ $this->passwords = $this->result->getValue();
}
- // Check against the rate limiter
- if ( $this->getUser()->pingLimiter( 'mailpassword' ) ) {
+ if ( $this->result->hasMessage( 'actionthrottledtext' ) ) {
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 array( array(
- '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 array( array( '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 array( 'badipaddress' );
- }
- $caller = $this->getUser();
- Hooks::run( 'User::mailPasswordInternal', array( &$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 = array();
- 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 array( array( 'mailerror', $this->result->getMessage() ) );
- }
+ return $this->result;
}
public function onSuccess() {
- if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->email != null ) {
+ if ( $this->getUser()->isAllowed( 'passwordreset' ) && $this->passwords ) {
// @todo Logging
if ( $this->result->isGood() ) {
- $this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture' );
+ $this->getOutput()->addWikiMsg( 'passwordreset-emailsent-capture2',
+ count( $this->passwords ) );
} else {
- $this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture',
- $this->result->getMessage(), $this->firstUser->getName() );
+ $this->getOutput()->addWikiMsg( 'passwordreset-emailerror-capture2',
+ $this->result->getMessage(), key( $this->passwords ), count( $this->passwords ) );
}
- $this->getOutput()->addHTML( Html::rawElement( 'pre', array(), $this->email->escaped() ) );
+ $this->getOutput()->addHTML( Html::openElement( 'ul' ) );
+ foreach ( $this->passwords as $username => $pwd ) {
+ $this->getOutput()->addHTML( Html::rawElement( 'li', [],
+ htmlspecialchars( $username, ENT_QUOTES )
+ . $this->msg( 'colon-separator' )->text()
+ . htmlspecialchars( $pwd, ENT_QUOTES )
+ ) );
+ }
+ $this->getOutput()->addHTML( Html::closeElement( 'ul' ) );
}
if ( $this->method === 'email' ) {
$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 ) {
+ public function isListed() {
+ if ( $this->passwordReset->isAllowed( $this->getUser() )->isGood() ) {
return parent::isListed();
}