'FileRevertForm' => 'includes/FileRevertForm.php',
'ForkController' => 'includes/ForkController.php',
'FormOptions' => 'includes/FormOptions.php',
+ 'FormSpecialPage' => 'includes/SpecialPage.php',
'GenderCache' => 'includes/GenderCache.php',
'GlobalDependency' => 'includes/CacheDependency.php',
'HashtableReplacer' => 'includes/StringUtils.php',
'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php',
'SpecialNewFiles' => 'includes/specials/SpecialNewimages.php',
'SpecialNewpages' => 'includes/specials/SpecialNewpages.php',
+ 'SpecialPasswordReset' => 'includes/specials/SpecialPasswordReset.php',
'SpecialPreferences' => 'includes/specials/SpecialPreferences.php',
'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php',
'SpecialProtectedpages' => 'includes/specials/SpecialProtectedpages.php',
*/
$wgLivePasswordStrengthChecks = false;
+/**
+ * Whether to allow password resets ("enter some identifying data, and we'll send an email
+ * with a temporary password you can use to get back into the account") identified by
+ * various bits of data. Setting all of these to false (or the whole variable to false)
+ * has the effect of disabling password resets entirely
+ */
+$wgPasswordResetRoutes = array(
+ 'username' => true,
+ 'email' => false, // Warning: enabling this will be *very* slow on large wikis
+);
+
/**
* Maximum number of Unicode characters in signature
*/
}
}
+/**
+ * Special page which uses an HTMLForm to handle processing. This is mostly a
+ * clone of FormAction. More special pages should be built this way; maybe this could be
+ * a new structure for SpecialPages
+ */
+abstract class FormSpecialPage extends SpecialPage {
+
+ /**
+ * Get an HTMLForm descriptor array
+ * @return Array
+ */
+ protected abstract function getFormFields();
+
+ /**
+ * Add pre- or post-text to the form
+ * @return String HTML which will be sent to $form->addPreText()
+ */
+ protected function preText() { return ''; }
+ protected function postText() { return ''; }
+
+ /**
+ * Play with the HTMLForm if you need to more substantially
+ * @param $form HTMLForm
+ */
+ protected function alterForm( HTMLForm $form ) {}
+
+ /**
+ * Get the HTMLForm to control behaviour
+ * @return HTMLForm|null
+ */
+ protected function getForm() {
+ $this->fields = $this->getFormFields();
+
+ // Give hooks a chance to alter the form, adding extra fields or text etc
+ wfRunHooks( "Special{$this->getName()}ModifyFormFields", array( &$this->fields ) );
+
+ $form = new HTMLForm( $this->fields, $this->getContext() );
+ $form->setSubmitCallback( array( $this, 'onSubmit' ) );
+ $form->setWrapperLegend( wfMessage( strtolower( $this->getName() ) . '-legend' ) );
+ $form->addHeaderText( wfMessage( strtolower( $this->getName() ) . '-text' )->parseAsBlock() );
+
+ // Retain query parameters (uselang etc)
+ $params = array_diff_key( $this->getRequest()->getQueryValues(), array( 'title' => null ) );
+ $form->addHiddenField( 'redirectparams', wfArrayToCGI( $params ) );
+
+ $form->addPreText( $this->preText() );
+ $form->addPostText( $this->postText() );
+ $this->alterForm( $form );
+
+ // Give hooks a chance to alter the form, adding extra fields or text etc
+ wfRunHooks( "Special{$this->getName()}BeforeFormDisplay", array( &$form ) );
+
+ return $form;
+ }
+
+ /**
+ * Process the form on POST submission.
+ * @param $data Array
+ * @return Bool|Array true for success, false for didn't-try, array of errors on failure
+ */
+ public abstract function onSubmit( array $data );
+
+ /**
+ * Do something exciting on successful processing of the form, most likely to show a
+ * confirmation message
+ */
+ public abstract function onSuccess();
+
+ /**
+ * Basic SpecialPage workflow: get a form, send it to the user; get some data back,
+ */
+ public function execute( $par ) {
+ $this->setParameter( $par );
+ $this->setHeaders();
+
+ // This will throw exceptions if there's a problem
+ $this->userCanExecute( $this->getUser() );
+
+ $form = $this->getForm();
+ if ( $form->show() ) {
+ $this->onSuccess();
+ }
+ }
+
+ /**
+ * Maybe do something interesting with the subpage parameter
+ * @param $par String
+ */
+ protected function setParameter( $par ){}
+
+ /**
+ * Checks if the given user (identified by an object) can perform this action. Can be
+ * overridden by sub-classes with more complicated permissions schemes. Failures here
+ * must throw subclasses of ErrorPageError
+ *
+ * @param $user User: the user to check, or null to use the context user
+ * @throws ErrorPageError
+ */
+ public function userCanExecute( User $user ) {
+ if ( $this->requiresWrite() && wfReadOnly() ) {
+ throw new ReadOnlyError();
+ }
+
+ if ( $this->getRestriction() !== null && !$user->isAllowed( $this->getRestriction() ) ) {
+ throw new PermissionsError( $this->getRestriction() );
+ }
+
+ if ( $this->requiresUnblock() && $user->isBlocked() ) {
+ $block = $user->mBlock;
+ throw new UserBlockedError( $block );
+ }
+
+ return true;
+ }
+
+ /**
+ * Whether this action requires the wiki not to be locked
+ * @return Bool
+ */
+ public function requiresWrite() {
+ return true;
+ }
+
+ /**
+ * Whether this action cannot be executed by a blocked user
+ * @return Bool
+ */
+ public function requiresUnblock() {
+ return true;
+ }
+}
+
/**
* Shortcut to construct a special page which is unlisted by default
* @ingroup SpecialPage
'Unblock' => 'SpecialUnblock',
'BlockList' => 'SpecialBlockList',
'ChangePassword' => 'SpecialChangePassword',
+ 'PasswordReset' => 'SpecialPasswordReset',
'DeletedContributions' => 'DeletedContributionsPage',
'Preferences' => 'SpecialPreferences',
'Contributions' => 'SpecialContributions',
--- /dev/null
+<?php
+/**
+ * Implements Special:Blankpage
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page for requesting a password reset email
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialPasswordReset extends FormSpecialPage {
+
+ public function __construct() {
+ parent::__construct( 'PasswordReset' );
+ }
+
+ public function userCanExecute( User $user ) {
+ global $wgPasswordResetRoutes, $wgAuth;
+
+ // Maybe password resets are disabled, or there are no allowable routes
+ if ( !is_array( $wgPasswordResetRoutes )
+ || !in_array( true, array_values( $wgPasswordResetRoutes ) ) )
+ {
+ throw new ErrorPageError( 'internalerror', 'passwordreset-disabled' );
+ }
+
+ // Maybe the external auth plugin won't allow local password changes
+ if ( !$wgAuth->allowPasswordChange() ) {
+ throw new ErrorPageError( 'internalerror', 'resetpass_forbidden' );
+ }
+
+ // 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() ) {
+ throw new ErrorPageError( 'internalerror', 'blocked-mailpassword' );
+ }
+
+ return parent::userCanExecute( $user );
+ }
+
+ protected function getFormFields() {
+ global $wgPasswordResetRoutes;
+ $a = array();
+ if ( isset( $wgPasswordResetRoutes['username'] ) && $wgPasswordResetRoutes['username'] ) {
+ $a['Username'] = array(
+ 'type' => 'text',
+ 'label-message' => 'passwordreset-username',
+ );
+ }
+
+ if ( isset( $wgPasswordResetRoutes['email'] ) && $wgPasswordResetRoutes['email'] ) {
+ $a['Email'] = array(
+ 'type' => 'email',
+ 'label-message' => 'passwordreset-email',
+ );
+ }
+
+ return $a;
+ }
+
+ protected function preText() {
+ global $wgPasswordResetRoutes;
+ $i = 0;
+ if ( isset( $wgPasswordResetRoutes['username'] ) && $wgPasswordResetRoutes['username'] ) {
+ $i++;
+ }
+ if ( isset( $wgPasswordResetRoutes['email'] ) && $wgPasswordResetRoutes['email'] ) {
+ $i++;
+ }
+ return wfMessage( 'passwordreset-pretext', $i )->parseAsBlock();
+ }
+
+ /**
+ * 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 $data array
+ * @return Bool|Array
+ */
+ public function onSubmit( array $data ) {
+
+ 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';
+
+ // FIXME: this is an unindexed query
+ $res = wfGetDB( DB_SLAVE )->select(
+ 'user',
+ '*',
+ 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 ( !wfRunHooks( 'SpecialPasswordResetOnSubmit', array( &$users, $data, &$error ) ) ) {
+ return array( $error );
+ }
+
+ 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' );
+ }
+ }
+
+ $firstUser = $users[0];
+
+ if ( !$firstUser instanceof User || !$firstUser->getID() ) {
+ return array( array( 'nosuchuser', $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() ) {
+ global $wgPasswordReminderResendTime;
+ # Round the time in hours to 3 d.p., in case someone is specifying
+ # minutes or seconds.
+ return array( array( 'throttled-mailpassword', round( $wgPasswordReminderResendTime, 3 ) ) );
+ }
+ }
+
+ global $wgServer, $wgScript, $wgNewPasswordExpiry;
+
+ // 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', $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 = wfGetIP();
+ if ( !$ip ) {
+ return array( 'badipaddress' );
+ }
+ $caller = $this->getUser();
+ wfRunHooks( 'User::mailPasswordInternal', array( &$caller, &$ip, &$firstUser ) );
+ $username = $caller->getName();
+ $msg = IP::isValid( $username )
+ ? 'passwordreset-emailtext-ip'
+ : 'passwordreset-emailtext-user';
+
+ $passwords = array();
+ foreach ( $users as $user ) {
+ $password = $user->randomPassword();
+ $user->setNewpassword( $password );
+ $user->saveSettings();
+ $passwords[] = wfMessage( 'passwordreset-emailelement', $user->getName(), $password );
+ }
+ $passwordBlock = implode( "\n\n", $passwords );
+
+ // Send in the user's language; which should hopefully be the same
+ $userLanguage = $firstUser->getOption( 'language' );
+
+ $body = wfMessage( $msg )->inLanguage( $userLanguage );
+ $body->params(
+ $username,
+ $passwordBlock,
+ count( $passwords ),
+ $wgServer . $wgScript,
+ round( $wgNewPasswordExpiry / 86400 )
+ );
+
+ $title = wfMessage( 'passwordreset-emailtitle' );
+
+ $result = $firstUser->sendMail( $title->text(), $body->text() );
+
+ if ( $result->isGood() ) {
+ return true;
+ } else {
+ // FIXME: The email didn't send, 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', $result->getMessage() ) );
+ }
+ }
+
+ public function onSuccess() {
+ $this->getOutput()->addWikiMsg( 'passwordreset-emailsent' );
+ $this->getOutput()->returnToMain();
+ }
+}
const WRONG_TOKEN = 13;
var $mUsername, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
- var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
+ var $mAction, $mCreateaccount, $mCreateaccountMail;
var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage;
var $mSkipCookieCheck, $mReturnToQuery, $mToken, $mStickHTTPS;
var $mType, $mReason, $mRealName;
$this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
$this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
&& $wgEnableEmail;
- $this->mMailmypassword = $request->getCheck( 'wpMailmypassword' )
- && $wgEnableEmail;
$this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
$this->mAction = $request->getVal( 'action' );
$this->mRemember = $request->getCheck( 'wpRemember' );
return $this->addNewAccount();
} elseif ( $this->mCreateaccountMail ) {
return $this->addNewAccountMailPassword();
- } elseif ( $this->mMailmypassword ) {
- return $this->mailPassword();
} elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
return $this->processLogin();
}
$reset->execute( null );
}
- /**
- * @private
- */
- function mailPassword() {
- global $wgUser, $wgOut, $wgAuth;
-
- if ( wfReadOnly() ) {
- $wgOut->readOnlyPage();
- return false;
- }
-
- if( !$wgAuth->allowPasswordChange() ) {
- $this->mainLoginForm( wfMsg( 'resetpass_forbidden' ) );
- return;
- }
-
- # Check against blocked IPs so blocked users can't flood admins
- # with password resets
- if( $wgUser->isBlocked() ) {
- $this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) );
- return;
- }
-
- # Check for hooks
- $error = null;
- if ( !wfRunHooks( 'UserLoginMailPassword', array( $this->mUsername, &$error ) ) ) {
- $this->mainLoginForm( $error );
- return;
- }
-
- # If the user doesn't have a login token yet, set one.
- if ( !self::getLoginToken() ) {
- self::setLoginToken();
- $this->mainLoginForm( wfMsg( 'sessionfailure' ) );
- return;
- }
-
- # If the user didn't pass a login token, tell them we need one
- if ( !$this->mToken ) {
- $this->mainLoginForm( wfMsg( 'sessionfailure' ) );
- return;
- }
-
- # Check against the rate limiter
- if( $wgUser->pingLimiter( 'mailpassword' ) ) {
- $wgOut->rateLimited();
- return;
- }
-
- if ( $this->mUsername == '' ) {
- $this->mainLoginForm( wfMsg( 'noname' ) );
- return;
- }
- $u = User::newFromName( $this->mUsername );
- if( !$u instanceof User ) {
- $this->mainLoginForm( wfMsg( 'noname' ) );
- return;
- }
- if ( 0 == $u->getID() ) {
- $this->mainLoginForm( wfMsgExt( 'nosuchuser', 'parseinline', $u->getName() ) );
- return;
- }
-
- # Validate the login token
- if ( $this->mToken !== self::getLoginToken() ) {
- $this->mainLoginForm( wfMsg( 'sessionfailure' ) );
- return;
- }
-
- # Check against password throttle
- if ( $u->isPasswordReminderThrottled() ) {
- global $wgPasswordReminderResendTime;
- # Round the time in hours to 3 d.p., in case someone is specifying
- # minutes or seconds.
- $this->mainLoginForm( wfMsgExt( 'throttled-mailpassword', array( 'parsemag' ),
- round( $wgPasswordReminderResendTime, 3 ) ) );
- return;
- }
-
- $result = $this->mailPasswordInternal( $u, true, 'passwordremindertitle', 'passwordremindertext' );
- if( $result->isGood() ) {
- $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ), 'success' );
- self::clearLoginToken();
- } else {
- $this->mainLoginForm( $result->getWikiText( 'mailerror' ) );
- }
- }
-
-
/**
* @param $u User object
* @param $throttle Boolean
global $wgEnableEmail, $wgEnableUserEmail;
global $wgRequest, $wgLoginLanguageSelector;
global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration;
- global $wgSecureLogin;
+ global $wgSecureLogin, $wgPasswordResetRoutes;
$titleObj = SpecialPage::getTitleFor( 'Userlogin' );
$template->set( 'link', '' );
}
+ $resetLink = $this->mType == 'signup'
+ ? null
+ : is_array( $wgPasswordResetRoutes ) && in_array( true, array_values( $wgPasswordResetRoutes ) );
+
$template->set( 'header', '' );
$template->set( 'name', $this->mUsername );
$template->set( 'password', $this->mPassword );
$template->set( 'emailrequired', $wgEmailConfirmToEdit );
$template->set( 'emailothers', $wgEnableUserEmail );
$template->set( 'canreset', $wgAuth->allowPasswordChange() );
+ $template->set( 'resetlink', $resetLink );
$template->set( 'canremember', ( $wgCookieExpiration > 0 ) );
$template->set( 'usereason', $wgUser->isLoggedIn() );
$template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) || $this->mRemember );
'tabindex' => '9'
) );
if ( $this->data['useemail'] && $this->data['canreset'] ) {
- echo ' ';
- echo Html::input( 'wpMailmypassword', wfMsg( 'mailmypassword' ), 'submit', array(
- 'id' => 'wpMailmypassword',
- 'tabindex' => '10'
- ) );
+ if( $this->data['resetlink'] === true ){
+ echo ' ';
+ echo Linker::link(
+ SpecialPage::getTitleFor( 'PasswordReset' ),
+ wfMessage( 'userlogin-resetlink' )
+ );
+ } elseif( $this->data['resetlink'] === null ) {
+ echo ' ';
+ echo Html::input( 'wpMailmypassword', wfMsg( 'mailmypassword' ), 'submit', array(
+ 'id' => 'wpMailmypassword',
+ 'tabindex' => '10'
+ ) );
+ }
} ?>
</td>
'Myuploads' => array( 'MyUploads' ),
'Newimages' => array( 'NewFiles', 'NewImages' ),
'Newpages' => array( 'NewPages' ),
+ 'PasswordReset' => array( 'PasswordReset' ),
'PermanentLink' => array( 'PermanentLink', 'PermaLink' ),
'Popularpages' => array( 'PopularPages' ),
'Preferences' => array( 'Preferences' ),
'createaccount' => 'Create account',
'gotaccount' => 'Already have an account? $1.',
'gotaccountlink' => 'Log in',
+'userlogin-resetlink' => 'Forgotten your login details?',
'createaccountmail' => 'By e-mail',
'createaccountreason' => 'Reason:',
'badretype' => 'The passwords you entered do not match.',
'php-mail-error' => '$1', # do not translate or duplicate this message to other languages
'php-mail-error-unknown' => "Unknown error in PHP's mail() function",
-# Password reset dialog
+# Change Password dialog
'resetpass' => 'Change password',
'resetpass_announce' => 'You logged in with a temporary e-mailed code.
To finish logging in, you must set a new password here:',
You may have already successfully changed your password or requested a new temporary password.',
'resetpass-temp-password' => 'Temporary password:',
+# Special:PasswordReset
+'passwordreset' => 'Reset password',
+'passwordreset-text' => 'Complete this form to receive an email reminder of your account details.',
+'passwordreset-legend' => 'Reset password',
+'passwordreset-disabled' => 'Password resets have been disabled on this wiki.',
+'passwordreset-pretext' => '{{PLURAL:$1||Enter one of the pieces of data below}}',
+'passwordreset-username' => 'Username:',
+'passwordreset-email' => 'Email:',
+'passwordreset-emailtitle' => 'Account details on {{SITENAME}}',
+'passwordreset-emailtext-ip' => '
+Someone (probably you, from IP address $1) requested a reminder of your
+account details for {{SITENAME}} ($4). The following user {{PLURAL:$3|account is|accounts are}}
+associated with this email address:
+
+$2
+
+{{PLURAL:$3|This temporary password|These temporary passwords}} will expire in {{PLURAL:$5|one day|$5 days}}.
+You should log in and choose a new password now. If someone else made this
+request, or if you have remembered your original password, and you no longer
+wish to change it, you may ignore this message and continue using your old
+password.',
+'passwordreset-emailtext-user' => '
+User $1 on {{SITENAME}} requested a reminder of your account details for {{SITENAME}}
+($4). The following user {{PLURAL:$3|account is|accounts are}} associated with this email address:
+
+$2
+
+{{PLURAL:$3|This temporary password|These temporary passwords}} will expire in {{PLURAL:$5|one day|$5 days}}.
+You should log in and choose a new password now. If someone else made this
+request, or if you have remembered your original password, and you no longer
+wish to change it, you may ignore this message and continue using your old
+password.',
+'passwordreset-emailelement' => "\tUsername: $1\n\tTemporary password: $2",
+'passwordreset-emailsent' => 'A reminder email has been sent.',
+
# Edit page toolbar
'bold_sample' => 'Bold text',
'bold_tip' => 'Bold text',
'resetpass-wrong-oldpass',
'resetpass-temp-password',
),
+ 'passwordreset' => array(
+ 'passwordreset',
+ 'passwordreset-text',
+ 'passwordreset-legend',
+ 'passwordreset-disabled',
+ 'passwordreset-pretext',
+ 'passwordreset-username',
+ 'passwordreset-email',
+ 'passwordreset-emailtitle',
+ 'passwordreset-emailtext-ip',
+ 'passwordreset-emailtext-user',
+ 'passwordreset-emailelement',
+ 'passwordreset-emailsent',
+ ),
'toolbar' => array(
'bold_sample',
'bold_tip',