From 399f95577b0025da566d487af03c54fe5dec6d2c Mon Sep 17 00:00:00 2001 From: Happy-melon Date: Wed, 20 Apr 2011 15:27:09 +0000 Subject: [PATCH] (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). --- includes/AutoLoader.php | 2 + includes/DefaultSettings.php | 11 + includes/SpecialPage.php | 132 ++++++++++++ includes/SpecialPageFactory.php | 1 + includes/specials/SpecialPasswordReset.php | 225 +++++++++++++++++++++ includes/specials/SpecialUserlogin.php | 102 +--------- includes/templates/Userlogin.php | 18 +- languages/messages/MessagesEn.php | 39 +++- maintenance/language/messages.inc | 14 ++ 9 files changed, 443 insertions(+), 101 deletions(-) create mode 100644 includes/specials/SpecialPasswordReset.php diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 8c88bdc2ea..edae895666 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -91,6 +91,7 @@ $wgAutoloadLocalClasses = array( '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', @@ -714,6 +715,7 @@ $wgAutoloadLocalClasses = array( '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', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 852b59a72f..6bc943711e 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -3004,6 +3004,17 @@ $wgMinimalPasswordLength = 1; */ $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 */ diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index f375fc02d6..866cbecd5c 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -659,6 +659,138 @@ class SpecialPage { } } +/** + * 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 diff --git a/includes/SpecialPageFactory.php b/includes/SpecialPageFactory.php index ebe3001817..2a74186e7f 100644 --- a/includes/SpecialPageFactory.php +++ b/includes/SpecialPageFactory.php @@ -73,6 +73,7 @@ class SpecialPageFactory { 'Unblock' => 'SpecialUnblock', 'BlockList' => 'SpecialBlockList', 'ChangePassword' => 'SpecialChangePassword', + 'PasswordReset' => 'SpecialPasswordReset', 'DeletedContributions' => 'DeletedContributionsPage', 'Preferences' => 'SpecialPreferences', 'Contributions' => 'SpecialContributions', diff --git a/includes/specials/SpecialPasswordReset.php b/includes/specials/SpecialPasswordReset.php new file mode 100644 index 0000000000..fe9d9248a4 --- /dev/null +++ b/includes/specials/SpecialPasswordReset.php @@ -0,0 +1,225 @@ +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(); + } +} diff --git a/includes/specials/SpecialUserlogin.php b/includes/specials/SpecialUserlogin.php index 119e11d8dc..25fc6f9a81 100644 --- a/includes/specials/SpecialUserlogin.php +++ b/includes/specials/SpecialUserlogin.php @@ -44,7 +44,7 @@ class LoginForm extends SpecialPage { 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; @@ -90,8 +90,6 @@ class LoginForm extends SpecialPage { $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' ); @@ -146,8 +144,6 @@ class LoginForm extends SpecialPage { return $this->addNewAccount(); } elseif ( $this->mCreateaccountMail ) { return $this->addNewAccountMailPassword(); - } elseif ( $this->mMailmypassword ) { - return $this->mailPassword(); } elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) { return $this->processLogin(); } @@ -739,95 +735,6 @@ class LoginForm extends SpecialPage { $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 @@ -977,7 +884,7 @@ class LoginForm extends SpecialPage { global $wgEnableEmail, $wgEnableUserEmail; global $wgRequest, $wgLoginLanguageSelector; global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration; - global $wgSecureLogin; + global $wgSecureLogin, $wgPasswordResetRoutes; $titleObj = SpecialPage::getTitleFor( 'Userlogin' ); @@ -1043,6 +950,10 @@ class LoginForm extends SpecialPage { $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 ); @@ -1061,6 +972,7 @@ class LoginForm extends SpecialPage { $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 ); diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index e87168dae5..bcc02240f6 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -130,11 +130,19 @@ class UserloginTemplate extends QuickTemplate { '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' + ) ); + } } ?> diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 3a097e7f2c..5d8c52182a 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -420,6 +420,7 @@ $specialPageAliases = array( 'Myuploads' => array( 'MyUploads' ), 'Newimages' => array( 'NewFiles', 'NewImages' ), 'Newpages' => array( 'NewPages' ), + 'PasswordReset' => array( 'PasswordReset' ), 'PermanentLink' => array( 'PermanentLink', 'PermaLink' ), 'Popularpages' => array( 'PopularPages' ), 'Preferences' => array( 'Preferences' ), @@ -1062,6 +1063,7 @@ Do not forget to change your [[Special:Preferences|{{SITENAME}} 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.', @@ -1156,7 +1158,7 @@ Please wait before trying again.', '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:', @@ -1176,6 +1178,41 @@ Now logging you in...', 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', diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index 10ad919592..bf10adb2fd 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -508,6 +508,20 @@ $wgMessageStructure = array( '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', -- 2.20.1