r86482 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r86481‎ | r86482 | r86483 >
Date:15:27, 20 April 2011
Author:happy-melon
Status:resolved (Comments)
Tags:
Comment:
(bug 13015, bug 18347, bug 18996, bug 20473, bug 23669, bug 28244) separate the password-reset request dialogue from SpecialUserlogin.
* Refactor with all the latest bells and whistles
* Allow wikis to enable resettting by entering an email address (bug 13015). This is currently an unindexed query, but it is disabled by default so no immediate problem.
* Allow resetting to be disabled entirely (bug 20473).
* Don't send registered users' IP addresses in the emails (bug 18347)
* Check that a user is not globally blocked before letting them send messages (bug 23669)
* Display a more useful error message when an account exists globally but not locally (bug 18996).
Modified paths:
  • /trunk/extensions/CentralAuth/CentralAuth.i18n.php (modified) (history)
  • /trunk/extensions/CentralAuth/CentralAuth.php (modified) (history)
  • /trunk/extensions/CentralAuth/CentralAuthHooks.php (modified) (history)
  • /trunk/extensions/GlobalBlocking/GlobalBlocking.class.php (modified) (history)
  • /trunk/extensions/GlobalBlocking/GlobalBlocking.php (modified) (history)
  • /trunk/extensions/Translate/tag/PageTranslationHooks.php (modified) (history)
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/DefaultSettings.php (modified) (history)
  • /trunk/phase3/includes/SpecialPage.php (modified) (history)
  • /trunk/phase3/includes/SpecialPageFactory.php (modified) (history)
  • /trunk/phase3/includes/specials/SpecialPasswordReset.php (added) (history)
  • /trunk/phase3/includes/specials/SpecialUserlogin.php (modified) (history)
  • /trunk/phase3/includes/templates/Userlogin.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesEn.php (modified) (history)
  • /trunk/phase3/maintenance/language/messages.inc (modified) (history)

Diff [purge]

Index: trunk/phase3/maintenance/language/messages.inc
@@ -508,6 +508,20 @@
509509 'resetpass-wrong-oldpass',
510510 'resetpass-temp-password',
511511 ),
 512+ 'passwordreset' => array(
 513+ 'passwordreset',
 514+ 'passwordreset-text',
 515+ 'passwordreset-legend',
 516+ 'passwordreset-disabled',
 517+ 'passwordreset-pretext',
 518+ 'passwordreset-username',
 519+ 'passwordreset-email',
 520+ 'passwordreset-emailtitle',
 521+ 'passwordreset-emailtext-ip',
 522+ 'passwordreset-emailtext-user',
 523+ 'passwordreset-emailelement',
 524+ 'passwordreset-emailsent',
 525+ ),
512526 'toolbar' => array(
513527 'bold_sample',
514528 'bold_tip',
Index: trunk/phase3/includes/SpecialPageFactory.php
@@ -73,6 +73,7 @@
7474 'Unblock' => 'SpecialUnblock',
7575 'BlockList' => 'SpecialBlockList',
7676 'ChangePassword' => 'SpecialChangePassword',
 77+ 'PasswordReset' => 'SpecialPasswordReset',
7778 'DeletedContributions' => 'DeletedContributionsPage',
7879 'Preferences' => 'SpecialPreferences',
7980 'Contributions' => 'SpecialContributions',
Index: trunk/phase3/includes/AutoLoader.php
@@ -91,6 +91,7 @@
9292 'FileRevertForm' => 'includes/FileRevertForm.php',
9393 'ForkController' => 'includes/ForkController.php',
9494 'FormOptions' => 'includes/FormOptions.php',
 95+ 'FormSpecialPage' => 'includes/SpecialPage.php',
9596 'GenderCache' => 'includes/GenderCache.php',
9697 'GlobalDependency' => 'includes/CacheDependency.php',
9798 'HashtableReplacer' => 'includes/StringUtils.php',
@@ -714,6 +715,7 @@
715716 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php',
716717 'SpecialNewFiles' => 'includes/specials/SpecialNewimages.php',
717718 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php',
 719+ 'SpecialPasswordReset' => 'includes/specials/SpecialPasswordReset.php',
718720 'SpecialPreferences' => 'includes/specials/SpecialPreferences.php',
719721 'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php',
720722 'SpecialProtectedpages' => 'includes/specials/SpecialProtectedpages.php',
Index: trunk/phase3/includes/DefaultSettings.php
@@ -3005,6 +3005,17 @@
30063006 $wgLivePasswordStrengthChecks = false;
30073007
30083008 /**
 3009+ * Whether to allow password resets ("enter some identifying data, and we'll send an email
 3010+ * with a temporary password you can use to get back into the account") identified by
 3011+ * various bits of data. Setting all of these to false (or the whole variable to false)
 3012+ * has the effect of disabling password resets entirely
 3013+ */
 3014+$wgPasswordResetRoutes = array(
 3015+ 'username' => true,
 3016+ 'email' => false, // Warning: enabling this will be *very* slow on large wikis
 3017+);
 3018+
 3019+/**
30093020 * Maximum number of Unicode characters in signature
30103021 */
30113022 $wgMaxSigChars = 255;
Index: trunk/phase3/includes/specials/SpecialPasswordReset.php
@@ -0,0 +1,225 @@
 2+<?php
 3+/**
 4+ * Implements Special:Blankpage
 5+ *
 6+ * This program is free software; you can redistribute it and/or modify
 7+ * it under the terms of the GNU General Public License as published by
 8+ * the Free Software Foundation; either version 2 of the License, or
 9+ * (at your option) any later version.
 10+ *
 11+ * This program is distributed in the hope that it will be useful,
 12+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 14+ * GNU General Public License for more details.
 15+ *
 16+ * You should have received a copy of the GNU General Public License along
 17+ * with this program; if not, write to the Free Software Foundation, Inc.,
 18+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 19+ * http://www.gnu.org/copyleft/gpl.html
 20+ *
 21+ * @file
 22+ * @ingroup SpecialPage
 23+ */
 24+
 25+/**
 26+ * Special page for requesting a password reset email
 27+ *
 28+ * @ingroup SpecialPage
 29+ */
 30+class SpecialPasswordReset extends FormSpecialPage {
 31+
 32+ public function __construct() {
 33+ parent::__construct( 'PasswordReset' );
 34+ }
 35+
 36+ public function userCanExecute( User $user ) {
 37+ global $wgPasswordResetRoutes, $wgAuth;
 38+
 39+ // Maybe password resets are disabled, or there are no allowable routes
 40+ if ( !is_array( $wgPasswordResetRoutes )
 41+ || !in_array( true, array_values( $wgPasswordResetRoutes ) ) )
 42+ {
 43+ throw new ErrorPageError( 'internalerror', 'passwordreset-disabled' );
 44+ }
 45+
 46+ // Maybe the external auth plugin won't allow local password changes
 47+ if ( !$wgAuth->allowPasswordChange() ) {
 48+ throw new ErrorPageError( 'internalerror', 'resetpass_forbidden' );
 49+ }
 50+
 51+ // Maybe the user is blocked (check this here rather than relying on the parent
 52+ // method as we have a more specific error message to use here
 53+ if ( $user->isBlocked() ) {
 54+ throw new ErrorPageError( 'internalerror', 'blocked-mailpassword' );
 55+ }
 56+
 57+ return parent::userCanExecute( $user );
 58+ }
 59+
 60+ protected function getFormFields() {
 61+ global $wgPasswordResetRoutes;
 62+ $a = array();
 63+ if ( isset( $wgPasswordResetRoutes['username'] ) && $wgPasswordResetRoutes['username'] ) {
 64+ $a['Username'] = array(
 65+ 'type' => 'text',
 66+ 'label-message' => 'passwordreset-username',
 67+ );
 68+ }
 69+
 70+ if ( isset( $wgPasswordResetRoutes['email'] ) && $wgPasswordResetRoutes['email'] ) {
 71+ $a['Email'] = array(
 72+ 'type' => 'email',
 73+ 'label-message' => 'passwordreset-email',
 74+ );
 75+ }
 76+
 77+ return $a;
 78+ }
 79+
 80+ protected function preText() {
 81+ global $wgPasswordResetRoutes;
 82+ $i = 0;
 83+ if ( isset( $wgPasswordResetRoutes['username'] ) && $wgPasswordResetRoutes['username'] ) {
 84+ $i++;
 85+ }
 86+ if ( isset( $wgPasswordResetRoutes['email'] ) && $wgPasswordResetRoutes['email'] ) {
 87+ $i++;
 88+ }
 89+ return wfMessage( 'passwordreset-pretext', $i )->parseAsBlock();
 90+ }
 91+
 92+ /**
 93+ * Process the form. At this point we know that the user passes all the criteria in
 94+ * userCanExecute(), and if the data array contains 'Username', etc, then Username
 95+ * resets are allowed.
 96+ * @param $data array
 97+ * @return Bool|Array
 98+ */
 99+ public function onSubmit( array $data ) {
 100+
 101+ if ( isset( $data['Username'] ) && $data['Username'] !== '' ) {
 102+ $method = 'username';
 103+ $users = array( User::newFromName( $data['Username'] ) );
 104+ } elseif ( isset( $data['Email'] )
 105+ && $data['Email'] !== ''
 106+ && Sanitizer::validateEmail( $data['Email'] ) )
 107+ {
 108+ $method = 'email';
 109+
 110+ // FIXME: this is an unindexed query
 111+ $res = wfGetDB( DB_SLAVE )->select(
 112+ 'user',
 113+ '*',
 114+ array( 'user_email' => $data['Email'] ),
 115+ __METHOD__
 116+ );
 117+ if ( $res ) {
 118+ $users = array();
 119+ foreach( $res as $row ){
 120+ $users[] = User::newFromRow( $row );
 121+ }
 122+ } else {
 123+ // Some sort of database error, probably unreachable
 124+ throw new MWException( 'Unknown database error in ' . __METHOD__ );
 125+ }
 126+ } else {
 127+ // The user didn't supply any data
 128+ return false;
 129+ }
 130+
 131+ // Check for hooks (captcha etc), and allow them to modify the users list
 132+ $error = array();
 133+ if ( !wfRunHooks( 'SpecialPasswordResetOnSubmit', array( &$users, $data, &$error ) ) ) {
 134+ return array( $error );
 135+ }
 136+
 137+ if( count( $users ) == 0 ){
 138+ if( $method == 'email' ){
 139+ // Don't reveal whether or not an email address is in use
 140+ return true;
 141+ } else {
 142+ return array( 'noname' );
 143+ }
 144+ }
 145+
 146+ $firstUser = $users[0];
 147+
 148+ if ( !$firstUser instanceof User || !$firstUser->getID() ) {
 149+ return array( array( 'nosuchuser', $data['Username'] ) );
 150+ }
 151+
 152+ // Check against the rate limiter
 153+ if ( $this->getUser()->pingLimiter( 'mailpassword' ) ) {
 154+ throw new ThrottledError;
 155+ }
 156+
 157+ // Check against password throttle
 158+ foreach ( $users as $user ) {
 159+ if ( $user->isPasswordReminderThrottled() ) {
 160+ global $wgPasswordReminderResendTime;
 161+ # Round the time in hours to 3 d.p., in case someone is specifying
 162+ # minutes or seconds.
 163+ return array( array( 'throttled-mailpassword', round( $wgPasswordReminderResendTime, 3 ) ) );
 164+ }
 165+ }
 166+
 167+ global $wgServer, $wgScript, $wgNewPasswordExpiry;
 168+
 169+ // All the users will have the same email address
 170+ if ( $firstUser->getEmail() == '' ) {
 171+ // This won't be reachable from the email route, so safe to expose the username
 172+ return array( array( 'noemail', $firstUser->getName() ) );
 173+ }
 174+
 175+ // We need to have a valid IP address for the hook, but per bug 18347, we should
 176+ // send the user's name if they're logged in.
 177+ $ip = wfGetIP();
 178+ if ( !$ip ) {
 179+ return array( 'badipaddress' );
 180+ }
 181+ $caller = $this->getUser();
 182+ wfRunHooks( 'User::mailPasswordInternal', array( &$caller, &$ip, &$firstUser ) );
 183+ $username = $caller->getName();
 184+ $msg = IP::isValid( $username )
 185+ ? 'passwordreset-emailtext-ip'
 186+ : 'passwordreset-emailtext-user';
 187+
 188+ $passwords = array();
 189+ foreach ( $users as $user ) {
 190+ $password = $user->randomPassword();
 191+ $user->setNewpassword( $password );
 192+ $user->saveSettings();
 193+ $passwords[] = wfMessage( 'passwordreset-emailelement', $user->getName(), $password );
 194+ }
 195+ $passwordBlock = implode( "\n\n", $passwords );
 196+
 197+ // Send in the user's language; which should hopefully be the same
 198+ $userLanguage = $firstUser->getOption( 'language' );
 199+
 200+ $body = wfMessage( $msg )->inLanguage( $userLanguage );
 201+ $body->params(
 202+ $username,
 203+ $passwordBlock,
 204+ count( $passwords ),
 205+ $wgServer . $wgScript,
 206+ round( $wgNewPasswordExpiry / 86400 )
 207+ );
 208+
 209+ $title = wfMessage( 'passwordreset-emailtitle' );
 210+
 211+ $result = $firstUser->sendMail( $title->text(), $body->text() );
 212+
 213+ if ( $result->isGood() ) {
 214+ return true;
 215+ } else {
 216+ // FIXME: The email didn't send, but we have already set the password throttle
 217+ // timestamp, so they won't be able to try again until it expires... :(
 218+ return array( array( 'mailerror', $result->getMessage() ) );
 219+ }
 220+ }
 221+
 222+ public function onSuccess() {
 223+ $this->getOutput()->addWikiMsg( 'passwordreset-emailsent' );
 224+ $this->getOutput()->returnToMain();
 225+ }
 226+}
Property changes on: trunk/phase3/includes/specials/SpecialPasswordReset.php
___________________________________________________________________
Added: svn:eol-style
1227 + native
Index: trunk/phase3/includes/specials/SpecialUserlogin.php
@@ -44,7 +44,7 @@
4545 const WRONG_TOKEN = 13;
4646
4747 var $mUsername, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
48 - var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
 48+ var $mAction, $mCreateaccount, $mCreateaccountMail;
4949 var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage;
5050 var $mSkipCookieCheck, $mReturnToQuery, $mToken, $mStickHTTPS;
5151 var $mType, $mReason, $mRealName;
@@ -90,8 +90,6 @@
9191 $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
9292 $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
9393 && $wgEnableEmail;
94 - $this->mMailmypassword = $request->getCheck( 'wpMailmypassword' )
95 - && $wgEnableEmail;
9694 $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
9795 $this->mAction = $request->getVal( 'action' );
9896 $this->mRemember = $request->getCheck( 'wpRemember' );
@@ -146,8 +144,6 @@
147145 return $this->addNewAccount();
148146 } elseif ( $this->mCreateaccountMail ) {
149147 return $this->addNewAccountMailPassword();
150 - } elseif ( $this->mMailmypassword ) {
151 - return $this->mailPassword();
152148 } elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
153149 return $this->processLogin();
154150 }
@@ -740,95 +736,6 @@
741737 }
742738
743739 /**
744 - * @private
745 - */
746 - function mailPassword() {
747 - global $wgUser, $wgOut, $wgAuth;
748 -
749 - if ( wfReadOnly() ) {
750 - $wgOut->readOnlyPage();
751 - return false;
752 - }
753 -
754 - if( !$wgAuth->allowPasswordChange() ) {
755 - $this->mainLoginForm( wfMsg( 'resetpass_forbidden' ) );
756 - return;
757 - }
758 -
759 - # Check against blocked IPs so blocked users can't flood admins
760 - # with password resets
761 - if( $wgUser->isBlocked() ) {
762 - $this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) );
763 - return;
764 - }
765 -
766 - # Check for hooks
767 - $error = null;
768 - if ( !wfRunHooks( 'UserLoginMailPassword', array( $this->mUsername, &$error ) ) ) {
769 - $this->mainLoginForm( $error );
770 - return;
771 - }
772 -
773 - # If the user doesn't have a login token yet, set one.
774 - if ( !self::getLoginToken() ) {
775 - self::setLoginToken();
776 - $this->mainLoginForm( wfMsg( 'sessionfailure' ) );
777 - return;
778 - }
779 -
780 - # If the user didn't pass a login token, tell them we need one
781 - if ( !$this->mToken ) {
782 - $this->mainLoginForm( wfMsg( 'sessionfailure' ) );
783 - return;
784 - }
785 -
786 - # Check against the rate limiter
787 - if( $wgUser->pingLimiter( 'mailpassword' ) ) {
788 - $wgOut->rateLimited();
789 - return;
790 - }
791 -
792 - if ( $this->mUsername == '' ) {
793 - $this->mainLoginForm( wfMsg( 'noname' ) );
794 - return;
795 - }
796 - $u = User::newFromName( $this->mUsername );
797 - if( !$u instanceof User ) {
798 - $this->mainLoginForm( wfMsg( 'noname' ) );
799 - return;
800 - }
801 - if ( 0 == $u->getID() ) {
802 - $this->mainLoginForm( wfMsgExt( 'nosuchuser', 'parseinline', $u->getName() ) );
803 - return;
804 - }
805 -
806 - # Validate the login token
807 - if ( $this->mToken !== self::getLoginToken() ) {
808 - $this->mainLoginForm( wfMsg( 'sessionfailure' ) );
809 - return;
810 - }
811 -
812 - # Check against password throttle
813 - if ( $u->isPasswordReminderThrottled() ) {
814 - global $wgPasswordReminderResendTime;
815 - # Round the time in hours to 3 d.p., in case someone is specifying
816 - # minutes or seconds.
817 - $this->mainLoginForm( wfMsgExt( 'throttled-mailpassword', array( 'parsemag' ),
818 - round( $wgPasswordReminderResendTime, 3 ) ) );
819 - return;
820 - }
821 -
822 - $result = $this->mailPasswordInternal( $u, true, 'passwordremindertitle', 'passwordremindertext' );
823 - if( $result->isGood() ) {
824 - $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ), 'success' );
825 - self::clearLoginToken();
826 - } else {
827 - $this->mainLoginForm( $result->getWikiText( 'mailerror' ) );
828 - }
829 - }
830 -
831 -
832 - /**
833740 * @param $u User object
834741 * @param $throttle Boolean
835742 * @param $emailTitle String: message name of email title
@@ -977,7 +884,7 @@
978885 global $wgEnableEmail, $wgEnableUserEmail;
979886 global $wgRequest, $wgLoginLanguageSelector;
980887 global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration;
981 - global $wgSecureLogin;
 888+ global $wgSecureLogin, $wgPasswordResetRoutes;
982889
983890 $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
984891
@@ -1043,6 +950,10 @@
1044951 $template->set( 'link', '' );
1045952 }
1046953
 954+ $resetLink = $this->mType == 'signup'
 955+ ? null
 956+ : is_array( $wgPasswordResetRoutes ) && in_array( true, array_values( $wgPasswordResetRoutes ) );
 957+
1047958 $template->set( 'header', '' );
1048959 $template->set( 'name', $this->mUsername );
1049960 $template->set( 'password', $this->mPassword );
@@ -1061,6 +972,7 @@
1062973 $template->set( 'emailrequired', $wgEmailConfirmToEdit );
1063974 $template->set( 'emailothers', $wgEnableUserEmail );
1064975 $template->set( 'canreset', $wgAuth->allowPasswordChange() );
 976+ $template->set( 'resetlink', $resetLink );
1065977 $template->set( 'canremember', ( $wgCookieExpiration > 0 ) );
1066978 $template->set( 'usereason', $wgUser->isLoggedIn() );
1067979 $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) || $this->mRemember );
Index: trunk/phase3/includes/SpecialPage.php
@@ -660,6 +660,138 @@
661661 }
662662
663663 /**
 664+ * Special page which uses an HTMLForm to handle processing. This is mostly a
 665+ * clone of FormAction. More special pages should be built this way; maybe this could be
 666+ * a new structure for SpecialPages
 667+ */
 668+abstract class FormSpecialPage extends SpecialPage {
 669+
 670+ /**
 671+ * Get an HTMLForm descriptor array
 672+ * @return Array
 673+ */
 674+ protected abstract function getFormFields();
 675+
 676+ /**
 677+ * Add pre- or post-text to the form
 678+ * @return String HTML which will be sent to $form->addPreText()
 679+ */
 680+ protected function preText() { return ''; }
 681+ protected function postText() { return ''; }
 682+
 683+ /**
 684+ * Play with the HTMLForm if you need to more substantially
 685+ * @param $form HTMLForm
 686+ */
 687+ protected function alterForm( HTMLForm $form ) {}
 688+
 689+ /**
 690+ * Get the HTMLForm to control behaviour
 691+ * @return HTMLForm|null
 692+ */
 693+ protected function getForm() {
 694+ $this->fields = $this->getFormFields();
 695+
 696+ // Give hooks a chance to alter the form, adding extra fields or text etc
 697+ wfRunHooks( "Special{$this->getName()}ModifyFormFields", array( &$this->fields ) );
 698+
 699+ $form = new HTMLForm( $this->fields, $this->getContext() );
 700+ $form->setSubmitCallback( array( $this, 'onSubmit' ) );
 701+ $form->setWrapperLegend( wfMessage( strtolower( $this->getName() ) . '-legend' ) );
 702+ $form->addHeaderText( wfMessage( strtolower( $this->getName() ) . '-text' )->parseAsBlock() );
 703+
 704+ // Retain query parameters (uselang etc)
 705+ $params = array_diff_key( $this->getRequest()->getQueryValues(), array( 'title' => null ) );
 706+ $form->addHiddenField( 'redirectparams', wfArrayToCGI( $params ) );
 707+
 708+ $form->addPreText( $this->preText() );
 709+ $form->addPostText( $this->postText() );
 710+ $this->alterForm( $form );
 711+
 712+ // Give hooks a chance to alter the form, adding extra fields or text etc
 713+ wfRunHooks( "Special{$this->getName()}BeforeFormDisplay", array( &$form ) );
 714+
 715+ return $form;
 716+ }
 717+
 718+ /**
 719+ * Process the form on POST submission.
 720+ * @param $data Array
 721+ * @return Bool|Array true for success, false for didn't-try, array of errors on failure
 722+ */
 723+ public abstract function onSubmit( array $data );
 724+
 725+ /**
 726+ * Do something exciting on successful processing of the form, most likely to show a
 727+ * confirmation message
 728+ */
 729+ public abstract function onSuccess();
 730+
 731+ /**
 732+ * Basic SpecialPage workflow: get a form, send it to the user; get some data back,
 733+ */
 734+ public function execute( $par ) {
 735+ $this->setParameter( $par );
 736+ $this->setHeaders();
 737+
 738+ // This will throw exceptions if there's a problem
 739+ $this->userCanExecute( $this->getUser() );
 740+
 741+ $form = $this->getForm();
 742+ if ( $form->show() ) {
 743+ $this->onSuccess();
 744+ }
 745+ }
 746+
 747+ /**
 748+ * Maybe do something interesting with the subpage parameter
 749+ * @param $par String
 750+ */
 751+ protected function setParameter( $par ){}
 752+
 753+ /**
 754+ * Checks if the given user (identified by an object) can perform this action. Can be
 755+ * overridden by sub-classes with more complicated permissions schemes. Failures here
 756+ * must throw subclasses of ErrorPageError
 757+ *
 758+ * @param $user User: the user to check, or null to use the context user
 759+ * @throws ErrorPageError
 760+ */
 761+ public function userCanExecute( User $user ) {
 762+ if ( $this->requiresWrite() && wfReadOnly() ) {
 763+ throw new ReadOnlyError();
 764+ }
 765+
 766+ if ( $this->getRestriction() !== null && !$user->isAllowed( $this->getRestriction() ) ) {
 767+ throw new PermissionsError( $this->getRestriction() );
 768+ }
 769+
 770+ if ( $this->requiresUnblock() && $user->isBlocked() ) {
 771+ $block = $user->mBlock;
 772+ throw new UserBlockedError( $block );
 773+ }
 774+
 775+ return true;
 776+ }
 777+
 778+ /**
 779+ * Whether this action requires the wiki not to be locked
 780+ * @return Bool
 781+ */
 782+ public function requiresWrite() {
 783+ return true;
 784+ }
 785+
 786+ /**
 787+ * Whether this action cannot be executed by a blocked user
 788+ * @return Bool
 789+ */
 790+ public function requiresUnblock() {
 791+ return true;
 792+ }
 793+}
 794+
 795+/**
664796 * Shortcut to construct a special page which is unlisted by default
665797 * @ingroup SpecialPage
666798 */
Index: trunk/phase3/includes/templates/Userlogin.php
@@ -130,11 +130,19 @@
131131 'tabindex' => '9'
132132 ) );
133133 if ( $this->data['useemail'] && $this->data['canreset'] ) {
134 - echo '&#160;';
135 - echo Html::input( 'wpMailmypassword', wfMsg( 'mailmypassword' ), 'submit', array(
136 - 'id' => 'wpMailmypassword',
137 - 'tabindex' => '10'
138 - ) );
 134+ if( $this->data['resetlink'] === true ){
 135+ echo '&#160;';
 136+ echo Linker::link(
 137+ SpecialPage::getTitleFor( 'PasswordReset' ),
 138+ wfMessage( 'userlogin-resetlink' )
 139+ );
 140+ } elseif( $this->data['resetlink'] === null ) {
 141+ echo '&#160;';
 142+ echo Html::input( 'wpMailmypassword', wfMsg( 'mailmypassword' ), 'submit', array(
 143+ 'id' => 'wpMailmypassword',
 144+ 'tabindex' => '10'
 145+ ) );
 146+ }
139147 } ?>
140148
141149 </td>
Index: trunk/phase3/languages/messages/MessagesEn.php
@@ -420,6 +420,7 @@
421421 'Myuploads' => array( 'MyUploads' ),
422422 'Newimages' => array( 'NewFiles', 'NewImages' ),
423423 'Newpages' => array( 'NewPages' ),
 424+ 'PasswordReset' => array( 'PasswordReset' ),
424425 'PermanentLink' => array( 'PermanentLink', 'PermaLink' ),
425426 'Popularpages' => array( 'PopularPages' ),
426427 'Preferences' => array( 'Preferences' ),
@@ -1062,6 +1063,7 @@
10631064 'createaccount' => 'Create account',
10641065 'gotaccount' => 'Already have an account? $1.',
10651066 'gotaccountlink' => 'Log in',
 1067+'userlogin-resetlink' => 'Forgotten your login details?',
10661068 'createaccountmail' => 'By e-mail',
10671069 'createaccountreason' => 'Reason:',
10681070 'badretype' => 'The passwords you entered do not match.',
@@ -1156,7 +1158,7 @@
11571159 'php-mail-error' => '$1', # do not translate or duplicate this message to other languages
11581160 'php-mail-error-unknown' => "Unknown error in PHP's mail() function",
11591161
1160 -# Password reset dialog
 1162+# Change Password dialog
11611163 'resetpass' => 'Change password',
11621164 'resetpass_announce' => 'You logged in with a temporary e-mailed code.
11631165 To finish logging in, you must set a new password here:',
@@ -1176,6 +1178,41 @@
11771179 You may have already successfully changed your password or requested a new temporary password.',
11781180 'resetpass-temp-password' => 'Temporary password:',
11791181
 1182+# Special:PasswordReset
 1183+'passwordreset' => 'Reset password',
 1184+'passwordreset-text' => 'Complete this form to receive an email reminder of your account details.',
 1185+'passwordreset-legend' => 'Reset password',
 1186+'passwordreset-disabled' => 'Password resets have been disabled on this wiki.',
 1187+'passwordreset-pretext' => '{{PLURAL:$1||Enter one of the pieces of data below}}',
 1188+'passwordreset-username' => 'Username:',
 1189+'passwordreset-email' => 'Email:',
 1190+'passwordreset-emailtitle' => 'Account details on {{SITENAME}}',
 1191+'passwordreset-emailtext-ip' => '
 1192+Someone (probably you, from IP address $1) requested a reminder of your
 1193+account details for {{SITENAME}} ($4). The following user {{PLURAL:$3|account is|accounts are}}
 1194+associated with this email address:
 1195+
 1196+$2
 1197+
 1198+{{PLURAL:$3|This temporary password|These temporary passwords}} will expire in {{PLURAL:$5|one day|$5 days}}.
 1199+You should log in and choose a new password now. If someone else made this
 1200+request, or if you have remembered your original password, and you no longer
 1201+wish to change it, you may ignore this message and continue using your old
 1202+password.',
 1203+'passwordreset-emailtext-user' => '
 1204+User $1 on {{SITENAME}} requested a reminder of your account details for {{SITENAME}}
 1205+($4). The following user {{PLURAL:$3|account is|accounts are}} associated with this email address:
 1206+
 1207+$2
 1208+
 1209+{{PLURAL:$3|This temporary password|These temporary passwords}} will expire in {{PLURAL:$5|one day|$5 days}}.
 1210+You should log in and choose a new password now. If someone else made this
 1211+request, or if you have remembered your original password, and you no longer
 1212+wish to change it, you may ignore this message and continue using your old
 1213+password.',
 1214+'passwordreset-emailelement' => "\tUsername: $1\n\tTemporary password: $2",
 1215+'passwordreset-emailsent' => 'A reminder email has been sent.',
 1216+
11801217 # Edit page toolbar
11811218 'bold_sample' => 'Bold text',
11821219 'bold_tip' => 'Bold text',
Index: trunk/extensions/GlobalBlocking/GlobalBlocking.php
@@ -29,7 +29,7 @@
3030
3131 $wgHooks['getUserPermissionsErrorsExpensive'][] = 'GlobalBlocking::getUserPermissionsErrors';
3232 $wgHooks['UserIsBlockedGlobally'][] = 'GlobalBlocking::isBlockedGlobally';
33 -$wgHooks['UserLoginMailPassword'][] = 'GlobalBlocking::onMailPassword';
 33+$wgHooks['SpecialPasswordResetOnSubmit'][] = 'GlobalBlocking::onSpecialPasswordResetOnSubmit';
3434 $wgHooks['OtherBlockLogLink'][] = 'GlobalBlocking::getBlockLogLink';
3535
3636 $wgAutoloadClasses['SpecialGlobalBlock'] = "$dir/SpecialGlobalBlock.php";
Index: trunk/extensions/GlobalBlocking/GlobalBlocking.class.php
@@ -289,7 +289,7 @@
290290 return array();
291291 }
292292
293 - static function onMailPassword( $name, &$error ) {
 293+ static function onSpecialPasswordResetOnSubmit( &$users, $data, &$error ) {
294294 global $wgUser;
295295
296296 if ( GlobalBlocking::getUserBlockErrors( $wgUser, wfGetIp() ) ) {
Index: trunk/extensions/CentralAuth/CentralAuth.php
@@ -187,6 +187,7 @@
188188 $wgHooks['UserLoadDefaults'][] = 'CentralAuthHooks::onUserLoadDefaults';
189189 $wgHooks['getUserPermissionsErrorsExpensive'][] = 'CentralAuthHooks::onGetUserPermissionsErrorsExpensive';
190190 $wgHooks['MakeGlobalVariablesScript'][] = 'CentralAuthHooks::onMakeGlobalVariablesScript';
 191+$wgHooks['SpecialPasswordResetOnSubmit'] = 'CentralAuthHooks::onSpecialPasswordResetOnSubmit';
191192
192193 // For interaction with the Special:Renameuser extension
193194 $wgHooks['RenameUserWarning'][] = 'CentralAuthHooks::onRenameUserWarning';
Index: trunk/extensions/CentralAuth/CentralAuthHooks.php
@@ -98,6 +98,32 @@
9999 return true;
100100 }
101101
 102+ /**
 103+ * Show a nicer error when the user account does not exist on the local wiki, but
 104+ * does exist globally
 105+ * @param $users Array
 106+ * @param $data Array
 107+ * @param $abortError String
 108+ * @return bool
 109+ */
 110+ static function onSpecialPasswordResetOnSubmit( &$users, $data, &$abortError ) {
 111+ if ( count( $users ) == 0 || !$users[0] instanceof User ){
 112+ // We can't handle this
 113+ return true;
 114+ }
 115+
 116+ $firstUser = $users[0];
 117+ if( !$firstUser->getID() ) {
 118+ $centralUser = CentralAuthUser::getInstance( $firstUser );
 119+ if ( $centralUser->exists() ) {
 120+ $abortError = array( 'centralauth-account-exists-reset' );
 121+ return false;
 122+ }
 123+ }
 124+
 125+ return true;
 126+ }
 127+
102128 static function onUserLoginComplete( &$user, &$inject_html ) {
103129 global $wgCentralAuthCookies, $wgRequest;
104130 if ( !$wgCentralAuthCookies ) {
Index: trunk/extensions/CentralAuth/CentralAuth.i18n.php
@@ -241,6 +241,7 @@
242242 // Other messages
243243 'centralauth-invalid-wiki' => 'No such wiki database: $1',
244244 'centralauth-account-exists' => 'Cannot create account: the requested username is already taken in the unified login system.',
 245+ 'centralauth-account-exists-reset' => 'The username $1 is not registered on this wiki, but it does exist in the unified login system.',
245246 'centralauth-login-progress' => 'Logging you in to projects of {{int:Centralauth-groupname}}:', # This message supports {{GENDER}}
246247 'centralauth-logout-progress' => 'Logging you out from other projects of {{int:Centralauth-groupname}}:',
247248 'centralauth-login-no-others' => 'You have been automatically logged into other projects of {{int:Centralauth-groupname}}.',
Index: trunk/extensions/Translate/tag/PageTranslationHooks.php
@@ -210,6 +210,7 @@
211211
212212 $options = $parser->getOptions();
213213
 214+ $sk = $options->getSkin();
214215 if ( method_exists( $options, 'getUserLang' ) ) {
215216 $userLangCode = $options->getUserLang();
216217 } else {
@@ -246,9 +247,9 @@
247248 if ( $parser->getTitle()->getText() === $_title->getText() ) {
248249 $languages[] = Html::rawElement( 'b', null, "*$name* $percent" );
249250 } elseif ( $code === $userLangCode ) {
250 - $languages[] = Linker::linkKnown( $_title, Html::rawElement( 'b', null, "$name $percent" ) );
 251+ $languages[] = $sk->linkKnown( $_title, Html::rawElement( 'b', null, "$name $percent" ) );
251252 } else {
252 - $languages[] = Linker::linkKnown( $_title, "$name $percent" );
 253+ $languages[] = $sk->linkKnown( $_title, "$name $percent" );
253254 }
254255 }
255256

Sign-offs

UserFlagDate
Aaron Schulzinspected19:37, 21 July 2011
Aaron Schulztested21:56, 22 July 2011
Catropeinspected19:49, 5 September 2011

Follow-up revisions

RevisionCommit summaryAuthorDate
r86485Follow-up r86482: patch to add an index on user_email. Not a problem if this...happy-melon15:33, 20 April 2011
r86513Follow-up r86482:...siebrand17:54, 20 April 2011
r86519FIx inconsistent messages in r86482.siebrand18:08, 20 April 2011
r86545Fixes for r86482raymond20:19, 20 April 2011
r86627Fix hook definition from r86482ialex16:04, 21 April 2011
r92898Removed Special{$this->getName()}ModifyFormFields from r86482. Seems kind of ...aaron21:31, 22 July 2011
r92924* Moved email changing from sp:Preferences to new sp:ChangeEmail, which requi...aaron00:48, 23 July 2011
r92932Follow-up r86482, r86485: removed user_name portion of user_email index per CRaaron08:15, 23 July 2011

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r86280Rename Special:Resetpass to Special:ChangePassword. "pass" is vague and unin...happy-melon20:29, 17 April 2011

Comments

#Comment by Catrope (talk | contribs)   08:17, 21 April 2011

SpecialPasswordReset.php claims to implement Special:Blankpage

#Comment by Aaron Schulz (talk | contribs)   20:31, 21 April 2011

11 bugs done in one commit? :)

#Comment by Catrope (talk | contribs)   22:02, 21 April 2011

You're double-counting, it's "only" 6 :)

#Comment by Happy-melon (talk | contribs)   22:06, 21 April 2011

Per IRC cabal, this is a new feature that doesn't need to go into 1.17.

#Comment by Aaron Schulz (talk | contribs)   17:32, 21 July 2011

What was the PageTranslationHooks.php change for?

#Comment by Happy-melon (talk | contribs)   21:12, 21 July 2011

Something to do with the Linker/Skin split, but I'm slightly wary of it now I look at it again; it might be a regression. I'll check.

#Comment by Aaron Schulz (talk | contribs)   17:37, 21 July 2011
+		wfRunHooks( "Special{$this->getName()}ModifyFormFields", array( &$this->fields ) );

This should just be "SpecialPageModifyFormFields" and then have $this->getName() as a pass-by-value argument.

#Comment by Aaron Schulz (talk | contribs)   21:32, 22 July 2011

Hook removed in r92898.

#Comment by Aaron Schulz (talk | contribs)   17:39, 21 July 2011
+		$block = $user->mBlock;

Can we just add a real method for this kind of stuff? This is ugly.

#Comment by Aaron Schulz (talk | contribs)   19:27, 21 July 2011

I kind of don't like how is_array( $wgPasswordResetRoutes ) && in_array( true, array_values( $wgPasswordResetRoutes ) ) is duplicated in three or so places.

#Comment by Aaron Schulz (talk | contribs)   20:38, 21 July 2011
throw new PasswordError( 'no such user' );

?

#Comment by Aaron Schulz (talk | contribs)   20:54, 22 July 2011

Fixed this myself.

#Comment by Aaron Schulz (talk | contribs)   20:30, 22 July 2011

This does check $wgPasswordAttemptThrottle, so people could keep guessing passwords at high speed. And when the get it right, they both login as the user and change the password.

This lets someone take over the user and locking them out all at once for added convenience :)

#Comment by Aaron Schulz (talk | contribs)   20:34, 22 July 2011

I meant "does not check" of course.

#Comment by Aaron Schulz (talk | contribs)   21:07, 22 July 2011

See r92887.

#Comment by Aaron Schulz (talk | contribs)   08:32, 23 July 2011

Actually, *these* changes didn't add that. I was thinking this made ChangePassword too for some reason, but it's just ResetPassword. The fixes there aren't "follow ups" to this rev then. Un-associating them...my mistake :/

Status & tagging log