* (bug 8121) wfRandom() was not between 0 and 1
* Add static method Parser::createAssocArgs($args), so parser functions can
use the same code to parse arguments as the templates do.
+* Change behavior of logins using the temporary e-mailed password (as stored
+ in user_newpassword hash field). Instead of just logging in silently and
+ leaving the previous user_password field in place indefinitely, the user
+ is now prompted to set a new password.
+
+ The password-changing form is at Special:Resetpass; currently it's only
+ usable for changing from the temporary password during login, but it
+ could perhaps be generalized, replacing the subform in preferences.
+
+ Once the new password is set successfully, the temporary password is wiped
+ so it cannot be used to login a second time, and the login process
+ is completed.
+* Suppress 'mail new password' button on login form if $wgAuth forbids
+ changing user passwords; it wouldn't work very well...
+* Consolidate password length checks and $wgAuth manipulation into
+ User::setPassword() to avoid duplicate code in different places
+ that set passwords.
+* User::setPassword() now throws PasswordError exceptions if the password
+ is illegal or cannot be set via $wgAuth. These can be caught and a human-
+ readable error message displayed by UI code.
+
== Languages updated ==
'UsercreateTemplate' => 'includes/templates/Userlogin.php',
'UserloginTemplate' => 'includes/templates/Userlogin.php',
'Language' => 'languages/Language.php',
+ 'PasswordResetForm' => 'includes/SpecialResetpass.php',
// API classes
'ApiBase' => 'includes/api/ApiBase.php',
'Recentchangeslinked' => array( 'UnlistedSpecialPage', 'Recentchangeslinked' ),
'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ),
'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ),
+ 'Resetpass' => array( 'UnlistedSpecialPage', 'Resetpass' ),
'Booksources' => array( 'SpecialPage', 'Booksources' ),
'Categories' => array( 'SpecialPage', 'Categories' ),
'Export' => array( 'SpecialPage', 'Export' ),
return;
}
- if ( strlen( $this->mNewpass ) < $wgMinimalPasswordLength ) {
- $this->mainPrefsForm( 'error', wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) );
- return;
- }
-
if (!$wgUser->checkPassword( $this->mOldpass )) {
$this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) );
return;
}
- if (!$wgAuth->setPassword( $wgUser, $this->mNewpass )) {
- $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
+
+ try {
+ $wgUser->setPassword( $this->mNewpass );
+ $this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
+ } catch( PasswordError $e ) {
+ $this->mainPrefsForm( 'error', $e->getMessage() );
return;
}
- $wgUser->setPassword( $this->mNewpass );
- $this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
-
}
$wgUser->setRealName( $this->mRealName );
--- /dev/null
+<?php
+
+function wfSpecialResetpass( $par ) {
+ $form = new PasswordResetForm();
+ $form->execute( $par );
+}
+
+class PasswordResetForm extends SpecialPage {
+ function __construct( $name=null, $reset=null ) {
+ if( $name !== null ) {
+ $this->mName = $name;
+ $this->mTemporaryPassword = $reset;
+ } else {
+ global $wgRequest;
+ $this->mName = $wgRequest->getVal( 'wpName' );
+ $this->mTemporaryPassword = $wgRequest->getVal( 'wpPassword' );
+ }
+ }
+
+ /**
+ * Main execution point
+ */
+ function execute( $par='' ) {
+ global $wgUser, $wgAuth, $wgOut, $wgRequest;
+
+ if( !$wgAuth->allowPasswordChange() ) {
+ $this->error( wfMsg( 'resetpass_forbidden' ) );
+ return;
+ }
+
+ if( $this->mName === null && !$wgRequest->wasPosted() ) {
+ $this->error( wfMsg( 'resetpass_missing' ) );
+ return;
+ }
+
+ if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
+ $newpass = $wgRequest->getVal( 'wpNewPassword' );
+ $retype = $wgRequest->getVal( 'wpRetype' );
+ try {
+ $this->attemptReset( $newpass, $retype );
+ $wgOut->addWikiText( wfMsg( 'resetpass_success' ) );
+
+ $data = array(
+ 'action' => 'submitlogin',
+ 'wpName' => $this->mName,
+ 'wpPassword' => $newpass,
+ 'returnto' => $wgRequest->getVal( 'returnto' ),
+ );
+ if( $wgRequest->getCheck( 'wpRemember' ) ) {
+ $data['wpRemember'] = 1;
+ }
+ $login = new LoginForm( new FauxRequest( $data, true ) );
+ $login->execute();
+
+ return;
+ } catch( PasswordError $e ) {
+ $this->error( $e->getMessage() );
+ }
+ }
+ $this->showForm();
+ }
+
+ function error( $msg ) {
+ global $wgOut;
+ $wgOut->addHtml( '<div class="errorbox">' .
+ htmlspecialchars( $msg ) .
+ '</div>' );
+ }
+
+ function showForm() {
+ global $wgOut, $wgUser, $wgLang, $wgRequest;
+
+ $self = SpecialPage::getTitleFor( 'Resetpass' );
+ $form =
+ '<div id="userloginForm">' .
+ wfOpenElement( 'form',
+ array(
+ 'method' => 'post',
+ 'action' => $self->getLocalUrl() ) ) .
+ '<h2>' . wfMsgHtml( 'resetpass_header' ) . '</h2>' .
+ '<div id="userloginprompt">' .
+ wfMsgExt( 'resetpass_text', array( 'parse' ) ) .
+ '</div>' .
+ '<table>' .
+ wfHidden( 'token', $wgUser->editToken() ) .
+ wfHidden( 'wpName', $this->mName ) .
+ wfHidden( 'wpPassword', $this->mTemporaryPassword ) .
+ wfHidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) .
+ $this->pretty( array(
+ array( 'wpName', 'username', 'text', $this->mName ),
+ array( 'wpNewPassword', 'newpassword', 'password', '' ),
+ array( 'wpRetype', 'yourpasswordagain', 'password', '' ),
+ ) ) .
+ '<tr>' .
+ '<td></td>' .
+ '<td>' .
+ Xml::checkLabel( wfMsg( 'remembermypassword' ),
+ 'wpRemember', 'wpRemember',
+ $wgRequest->getCheck( 'wpRemember' ) ) .
+ '</td>' .
+ '</tr>' .
+ '<tr>' .
+ '<td></td>' .
+ '<td>' .
+ wfSubmitButton( wfMsgHtml( 'resetpass_submit' ) ) .
+ '</td>' .
+ '</tr>' .
+ '</table>' .
+ wfCloseElement( 'form' ) .
+ '</div>';
+ $wgOut->addHtml( $form );
+ }
+
+ function pretty( $fields ) {
+ $out = '';
+ foreach( $fields as $list ) {
+ list( $name, $label, $type, $value ) = $list;
+ if( $type == 'text' ) {
+ $field = '<tt>' . htmlspecialchars( $value ) . '</tt>';
+ } else {
+ $field = Xml::input( $name, 20, $value,
+ array( 'id' => $name, 'type' => $type ) );
+ }
+ $out .= '<tr>';
+ $out .= '<td align="right">';
+ $out .= Xml::label( wfMsg( $label ), $name );
+ $out .= '</td>';
+ $out .= '<td>';
+ $out .= $field;
+ $out .= '</td>';
+ $out .= '</tr>';
+ }
+ return $out;
+ }
+
+ /**
+ * @throws PasswordError
+ */
+ function attemptReset( $newpass, $retype ) {
+ $user = User::newFromName( $this->mName );
+ if( $user->isAnon() ) {
+ throw new PasswordError( 'no such user' );
+ }
+
+ if( !$user->checkTemporaryPassword( $this->mTemporaryPassword ) ) {
+ throw new PasswordError( wfMsg( 'resetpass_bad_temporary' ) );
+ }
+
+ if( $newpass !== $retype ) {
+ throw new PasswordError( wfMsg( 'badretype' ) );
+ }
+
+ $user->setPassword( $newpass );
+ $user->saveSettings();
+ }
+}
+
+?>
const NOT_EXISTS = 4;
const WRONG_PASS = 5;
const EMPTY_PASS = 6;
+ const RESET_PASS = 7;
var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
}
if (!$u->checkPassword( $this->mPassword )) {
- return '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
+ if( $u->checkTemporaryPassword( $this->mPassword ) ) {
+ // The e-mailed temporary password should not be used
+ // for actual logins; that's a very sloppy habit,
+ // and insecure if an attacker has a few seconds to
+ // click "search" on someone's open mail reader.
+ //
+ // Allow it to be used only to reset the password
+ // a single time to a new value, which won't be in
+ // the user's e-mail archives.
+ //
+ // For backwards compatibility, we'll still recognize
+ // it at the login form to minimize surprises for
+ // people who have been logging in with a temporary
+ // password for some time.
+ //
+ // At this point we just return an appropriate code
+ // indicating that the UI should show a password
+ // reset form; bot interfaces etc will probably just
+ // fail cleanly here.
+ return self::RESET_PASS;
+ } else {
+ return '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
+ }
} else {
$wgAuth->updateUser( $u );
$wgUser = $u;
case self::EMPTY_PASS:
$this->mainLoginForm( wfMsg( 'wrongpasswordempty' ) );
break;
+ case self::RESET_PASS:
+ $this->resetLoginForm( wfMsg( 'resetpass_announce' ) );
+ break;
default:
wfDebugDieBacktrace( "Unhandled case value" );
}
}
+
+ function resetLoginForm( $error ) {
+ global $wgOut;
+ $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" );
+ $reset = new PasswordResetForm( $this->mName, $this->mPassword );
+ $reset->execute();
+ }
/**
* @private
*/
function mailPassword() {
- global $wgUser, $wgOut;
+ global $wgUser, $wgOut, $wgAuth;
+
+ if( !$wgAuth->allowPasswordChange() ) {
+ $this->mainLoginForm( wfMsg( 'resetpass_forbidden' ) );
+ return;
+ }
# Check against blocked IPs
# fixme -- should we not?
function mainLoginForm( $msg, $msgtype = 'error' ) {
global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector;
+ global $wgAuth;
if ( $this->mType == 'signup' ) {
if ( !$wgUser->isAllowed( 'createaccount' ) ) {
$template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() );
$template->set( 'userealname', $wgAllowRealName );
$template->set( 'useemail', $wgEnableEmail );
+ $template->set( 'canreset', $wgAuth->allowPasswordChange() );
$template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember );
# Prepare language selection links as needed
# places, so we can't safely include ' or " even though we really should.
define( 'EDIT_TOKEN_SUFFIX', '\\' );
+/**
+ * Thrown by User::setPassword() on error
+ */
+class PasswordError extends MWException {
+ // NOP
+}
+
/**
*
* @package MediaWiki
}
/**
- * Set the password and reset the random token
+ * Set the password and reset the random token
+ * Calls through to authentication plugin if necessary;
+ * will have no effect if the auth plugin refuses to
+ * pass the change through or if the legal password
+ * checks fail.
+ *
+ * @param string $str
+ * @throws PasswordError on failure
*/
function setPassword( $str ) {
+ global $wgAuth, $wgMinimalPasswordLength;
+
+ if( !$wgAuth->allowPasswordChange() ) {
+ throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
+ }
+
+ if( $wgMinimalPasswordLength &&
+ strlen( $str ) < $wgMinimalPasswordLength ) {
+ throw new PasswordError( wfMsg( 'passwordtooshort',
+ $wgMinimalPasswordLength ) );
+ }
+
+ if( !$wgAuth->setPassword( $this, $str ) ) {
+ throw new PasswordError( wfMsg( 'externaldberror' ) );
+ }
+
$this->load();
$this->setToken();
$this->mPassword = $this->encryptPassword( $str );
$this->mNewpassword = '';
$this->mNewpassTime = NULL;
+
+ return true;
}
/**
$ep = $this->encryptPassword( $password );
if ( 0 == strcmp( $ep, $this->mPassword ) ) {
return true;
- } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
- return true;
} elseif ( function_exists( 'iconv' ) ) {
# Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
# Check for this with iconv
}
return false;
}
+
+ /**
+ * Check if the given clear-text password matches the temporary password
+ * sent by e-mail for password reset operations.
+ * @return bool
+ */
+ function checkTemporaryPassword( $plaintext ) {
+ $hash = $this->encryptPassword( $plaintext );
+ return $hash === $this->mNewpassword;
+ }
/**
* Initialize (if necessary) and return a session token value
<tr>
<td></td>
<td align='left' style="white-space:nowrap">
- <input type='submit' name="wpLoginattempt" id="wpLoginattempt" tabindex="5" value="<?php $this->msg('login') ?>" /> <?php if( $this->data['useemail'] ) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword"
+ <input type='submit' name="wpLoginattempt" id="wpLoginattempt" tabindex="5" value="<?php $this->msg('login') ?>" /> <?php if( $this->data['useemail'] && $this->data['canreset']) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword"
tabindex="6"
value="<?php $this->msg('mailmypassword') ?>" />
<?php } ?>
'accountcreated' => 'Account created',
'accountcreatedtext' => 'The user account for $1 has been created.',
+# Password reset dialog
+'resetpass' => 'Reset account password',
+'resetpass_announce' => 'Login with temporary e-mailed code. To finish logging in, you must set a new password.',
+'resetpass_text' => "<!-- Add text here -->",
+'resetpass_header' => 'Reset password',
+'resetpass_submit' => 'Set password and log in',
+'resetpass_success' => 'Your password has been changed successfully! Now logging you in...',
+'resetpass_bad_temporary' => 'Invalid temporary password. You may have already successfully changed your password or requested a new temporary password.',
+'resetpass_forbidden' => 'Passwords cannot be changed on this wiki',
+'resetpass_missing' => 'No form data.',
+
+
# Edit page toolbar
'bold_sample'=>'Bold text',
'bold_tip'=>'Bold text',