From: Brion Vibber Date: Mon, 25 Apr 2005 18:38:43 +0000 (+0000) Subject: Clean up e-mail authentication code. X-Git-Tag: 1.5.0alpha1~131 X-Git-Url: http://git.cyclocoop.org///%22%40url%40//%22?a=commitdiff_plain;h=aed4a04076a27de87953dd766fdd34b8dd0213d0;p=lhc%2Fweb%2Fwiklou.git Clean up e-mail authentication code. * Add Special:Confirmemail unlisted page for requesting confirmation emails and as the destination * There is now a confirmation token separate from the login password, which is cleaner and hopefully a lot less confusing. * Confirmation token expires after 7 days * Added support functions for nullable timestamp columns: wfTimestampOrNull and Database::timestampOrNull * userMailer now returns WikiError objects * Added convenience functions to User for email management, consolidated some checks There are changes to the user table, so run update.php --- diff --git a/includes/Database.php b/includes/Database.php index 4ed8c59f2f..bfa0556bf0 100644 --- a/includes/Database.php +++ b/includes/Database.php @@ -1396,6 +1396,17 @@ class Database { return wfTimestamp(TS_MW,$ts); } + /** + * Local database timestamp format or null + */ + function timestampOrNull( $ts = null ) { + if( is_null( $ts ) ) { + return null; + } else { + return $this->timestamp( $ts ); + } + } + /** * @todo document */ diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 9e334e518e..61a6abc455 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -1056,6 +1056,21 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { } } +/** + * Return a formatted timestamp, or null if input is null. + * For dealing with nullable timestamp columns in the database. + * @param int $outputtype + * @param string $ts + * @return string + */ +function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) { + if( is_null( $ts ) ) { + return null; + } else { + return wfTimestamp( $outputtype, $ts ); + } +} + /** * Check where as the operating system is Windows * diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php new file mode 100644 index 0000000000..f88545bcc8 --- /dev/null +++ b/includes/SpecialConfirmemail.php @@ -0,0 +1,99 @@ +show( $code ); +} + +class ConfirmationForm { + function show( $code ) { + if( empty( $code ) ) { + $this->showEmpty( $this->checkAndSend() ); + } else { + $this->showCode( $code ); + } + } + + function showCode( $code ) { + $user = User::newFromConfirmationCode( $code ); + if( is_null( $user ) ) { + $this->showInvalidCode(); + } else { + $this->confirmAndShow( $user ); + } + } + + + function confirmAndShow( $user ) { + if( $user->confirmEmail() ) { + $this->showSuccess(); + } else { + $this->showError(); + } + } + + function checkAndSend() { + global $wgUser, $wgRequest; + if( $wgRequest->wasPosted() && + $wgUser->isLoggedIn() && + $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { + $result = $wgUser->sendConfirmationMail(); + if( WikiError::isError( $result ) ) { + return 'confirmemail_sendfailed'; + } else { + return 'confirmemail_sent'; + } + } else { + # boo + return ''; + } + } + + function showEmpty( $err ) { + require_once( 'templates/Confirmemail.php' ); + global $wgOut, $wgUser; + + $tpl = new ConfirmemailTemplate(); + $tpl->set( 'error', $err ); + $tpl->set( 'edittoken', $wgUser->editToken() ); + + $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail' ); + $tpl->set( 'action', $title->getLocalUrl() ); + + + $wgOut->addTemplate( $tpl ); + } + + function showInvalidCode() { + global $wgOut; + $wgOut->addWikiText( wfMsg( 'confirmemail_invalid' ) ); + } + + function showError() { + global $wgOut; + $wgOut->addWikiText( wfMsg( 'confirmemail_error' ) ); + } + + function showSuccess() { + global $wgOut, $wgRequest, $wgUser; + + if( $wgUser->isLoggedIn() ) { + $wgOut->addWikiText( wfMsg( 'confirmemail_loggedin' ) ); + } else { + $wgOut->addWikiText( wfMsg( 'confirmemail_success' ) ); + require_once( 'SpecialUserlogin.php' ); + $form = new LoginForm( $wgRequest ); + $form->execute(); + } + } +} + +?> diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php index 577a8e9366..9c53a7148a 100644 --- a/includes/SpecialEmailuser.php +++ b/includes/SpecialEmailuser.php @@ -18,8 +18,8 @@ function wfSpecialEmailuser( $par ) { return; } - if ( $wgUser->isAnon() || - ( !$wgUser->isValidEmailAddr( $wgUser->getEmail() ) ) ) { + if( !$wgUser->canSendEmail() ) { + wfDebug( "User can't send.\n" ); $wgOut->errorpage( "mailnologin", "mailnologintext" ); return; } @@ -31,30 +31,26 @@ function wfSpecialEmailuser( $par ) { $target = $par; } if ( "" == $target ) { + wfDebug( "Target is empty.\n" ); $wgOut->errorpage( "notargettitle", "notargettext" ); return; } + $nt = Title::newFromURL( $target ); if ( is_null( $nt ) ) { + wfDebug( "Target is invalid title.\n" ); $wgOut->errorpage( "notargettitle", "notargettext" ); return; } + $nu = User::newFromName( $nt->getText() ); - - if ( 0 == $nu->getID() ) { + if( is_null( $nu ) || !$nu->canReceiveEmail() ) { + wfDebug( "Target is invalid user or can't receive.\n" ); $wgOut->errorpage( "noemailtitle", "noemailtext" ); return; } $address = $nu->getEmail(); - - if ( ( !$nu->isValidEmailAddr( $address ) ) || - ( 1 == $nu->getOption( "disablemail" ) ) || - ( 0 == $nu->getEmailauthenticationtimestamp() ) ) { - $wgOut->errorpage( "noemailtitle", "noemailtext" ); - return; - } - $f = new EmailUserForm( $nu->getName() . " <{$address}>", $target ); if ( "success" == $action ) { @@ -147,13 +143,13 @@ class EmailUserForm { $mailResult = userMailer( $this->mAddress, $from, $subject, $this->text ); - if (!$mailResult) { + if( WikiError::isError( $mailResult ) ) { + $wgOut->addHTML( wfMsg( "usermailererror" ) . $mailResult); + } else { $titleObj = Title::makeTitle( NS_SPECIAL, "Emailuser" ); $encTarget = wfUrlencode( $this->target ); $wgOut->redirect( $titleObj->getFullURL( "target={$encTarget}&action=success" ) ); wfRunHooks('EmailUserComplete', array($this->mAddress, $from, $subject, $this->text)); - } else { - $wgOut->addHTML( wfMsg( "usermailererror" ) . $mailResult); } } } diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index 7379bcdf06..3d7cba2e8b 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -66,6 +66,11 @@ if( !$wgDisableInternalSearch ) { $wgSpecialPages['Search'] = new UnlistedSpecialPage( 'Search' ); } +global $wgEmailAuthentication; +if( $wgEmailAuthentication ) { + $wgSpecialPages['Confirmemail'] = new UnlistedSpecialPage( 'Confirmemail' ); +} + $wgSpecialPages = array_merge($wgSpecialPages, array ( 'Wantedpages' => new SpecialPage( 'Wantedpages' ), 'Shortpages' => new SpecialPage( 'Shortpages' ), diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php index b608222ab8..cf52974a2d 100644 --- a/includes/SpecialPreferences.php +++ b/includes/SpecialPreferences.php @@ -237,32 +237,31 @@ class PreferencesForm { $wgUser->setCookies(); $wgUser->saveSettings(); + $error = wfMsg( 'savedprefs' ); if( $wgEnableEmail ) { - $newadr = strtolower( $this->mUserEmail ); - $oldadr = strtolower($wgUser->getEmail()); - if (($newadr <> '') && ($newadr <> $oldadr)) { # the user has supplied a new email address on the login page - # prepare for authentication and mail a temporary password to newadr - require_once( 'SpecialUserlogin.php' ); - if ( !$wgUser->isValidEmailAddr( $newadr ) ) { - $this->mainPrefsForm( wfMsg( 'invalidemailaddress' ) ); - return; - } - $wgUser->mEmail = $newadr; # new behaviour: set this new emailaddr from login-page into user database record - $wgUser->mEmailAuthenticationtimestamp = 0; # but flag as "dirty" = unauthenticated - $wgUser->saveSettings(); - if ($wgEmailAuthentication) { - # mail a temporary password to the dirty address - # on "save options", this user will be logged-out automatically - $error = LoginForm::mailPasswordInternal( $wgUser, true, $dummy ); - if ($error === '') { - return LoginForm::mainLoginForm( wfMsg( 'passwordsentforemailauthentication', $wgUser->getName() ) ); - } else { - return LoginForm::mainLoginForm( wfMsg( 'mailerror', $error ) ); + $newadr = $this->mUserEmail; + $oldadr = $wgUser->getEmail(); + if( ($newadr != '') && ($newadr != $oldadr) ) { + # the user has supplied a new email address on the login page + if( $wgUser->isValidEmailAddr( $newadr ) ) { + $wgUser->mEmail = $newadr; # new behaviour: set this new emailaddr from login-page into user database record + $wgUser->mEmailAuthenticated = null; # but flag as "dirty" = unauthenticated + $wgUser->saveSettings(); + if ($wgEmailAuthentication) { + # Mail a temporary password to the dirty address. + # User can come back through the confirmation URL to re-enable email. + $result = $wgUser->sendConfirmationMail(); + if( WikiError::isError( $result ) ) { + $error = wfMsg( 'mailerror', $result->getMessage() ); + } else { + $error = wfMsg( 'passwordsentforemailauthentication', $wgUser->getName() ); + } } - # if user returns, that new email address gets authenticated in checkpassword() + } else { + $error = wfMsg( 'invalidemailaddress' ); } } else { - $wgUser->setEmail( strtolower($this->mUserEmail) ); + $wgUser->setEmail( $this->mUserEmail ); $wgUser->setCookies(); $wgUser->saveSettings(); } @@ -270,7 +269,7 @@ class PreferencesForm { $wgOut->setParserOptions( ParserOptions::newFromUser( $wgUser ) ); $po = ParserOptions::newFromUser( $wgUser ); - $this->mainPrefsForm( wfMsg( 'savedprefs' ) ); + $this->mainPrefsForm( $error ); } /** @@ -281,7 +280,7 @@ class PreferencesForm { $this->mOldpass = $this->mNewpass = $this->mRetypePass = ''; $this->mUserEmail = $wgUser->getEmail(); - $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp(); + $this->mUserEmailAuthenticated = $wgUser->getEmailAuthenticationTimestamp(); $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : ''; $this->mUserLanguage = $wgUser->getOption( 'language' ); if( empty( $this->mUserLanguage ) ) { @@ -454,11 +453,14 @@ class PreferencesForm { else { $emfc = ''; } if ($wgEmailAuthentication && ($this->mUserEmail != '') ) { - if ($wgUser->getEmailAuthenticationtimestamp() != 0) { - $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationtimestamp(), true ) ).'
'; + if( $wgUser->getEmailAuthenticationTimestamp() ) { + $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'
'; $disabled = ''; } else { - $emailauthenticated = wfMsg('emailnotauthenticated').'
'; + $skin = $wgUser->getSkin(); + $emailauthenticated = wfMsg('emailnotauthenticated').'
' . + $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Confirmemail' ), + wfMsg( 'emailconfirmlink' ) ); $disabled = ' '.wfMsg('disableduntilauthent'); } } else { diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php index 0702ed42c6..b33b95395c 100644 --- a/includes/SpecialUserlogin.php +++ b/includes/SpecialUserlogin.php @@ -91,7 +91,6 @@ class LoginForm { */ function addNewAccountMailPassword() { global $wgOut; - global $wgEmailAuthentication; if ('' == $this->mEmail) { $this->mainLoginForm( wfMsg( 'noemail', htmlspecialchars( $this->mName ) ) ); @@ -104,37 +103,19 @@ class LoginForm { return; } - $newadr = strtolower($this->mEmail); - - # prepare for authentication and mail a temporary password to newadr - if ( !$u->isValidEmailAddr( $newadr ) ) { - return $this->mainLoginForm( wfMsg( 'invalidemailaddress', $error ) ); - } - $u->mEmail = $newadr; # new behaviour: set this new emailaddr from login-page into user database record - $u->mEmailAuthenticationtimestamp = 0; # but flag as "dirty" = unauthenticated - - if ($wgEmailAuthentication) { - $error = $this->mailPasswordInternal( $u, true, $dummy ); # mail a temporary password to the dirty address - } - + $u->saveSettings(); + $result = $this->mailPasswordInternal($u); + $wgOut->setPageTitle( wfMsg( 'accmailtitle' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - if ($wgEmailAuthentication) { - if ($error === '') { - return $this->mainLoginForm( wfMsg( 'passwordsentforemailauthentication', $u->getName() ) ); + if( WikiError::isError( $result ) ) { + $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) ); } else { - return $this->mainLoginForm( wfMsg( 'mailerror', $error ) ); - } - # if user returns, that new email address gets authenticated in checkpassword() + $wgOut->addWikiText( wfMsg( 'accmailtext', $u->getName(), $u->getEmail() ) ); + $wgOut->returnToMain( false ); } -# if ( $error === '' ) { -# $wgOut->addWikiText( wfMsg( 'accmailtext', $u->getName(), $u->getEmail() ) ); -# $wgOut->returnToMain( false ); -# } else { -# $this->mainLoginForm( wfMsg( 'mailerror', $error ) ); -# } $u = 0; } @@ -144,7 +125,6 @@ class LoginForm { */ function addNewAccount() { global $wgUser, $wgOut; - global $wgEmailAuthentication; $u = $this->addNewAccountInternal(); @@ -152,31 +132,13 @@ class LoginForm { return; } - $newadr = strtolower($this->mEmail); - if ($newadr != '') { # prepare for authentication and mail a temporary password to newadr - if ( !$u->isValidEmailAddr( $newadr ) ) { - return $this->mainLoginForm( wfMsg( 'invalidemailaddress', $error ) ); - } - $u->mEmail = $newadr; # new behaviour: set this new emailaddr from login-page into user database record - $u->mEmailAuthenticationtimestamp = 0; # but flag as "dirty" = unauthenticated - - if ($wgEmailAuthentication) { - # mail a temporary password to the dirty address - - $error = $this->mailPasswordInternal( $u, true, $dummy ); - if ($error === '') { - return $this->mainLoginForm( wfMsg( 'passwordsentforemailauthentication', $u->getName() ) ); - } else { - return $this->mainLoginForm( wfMsg( 'mailerror', $error ) ); - } - # if user returns, that new email address gets authenticated in checkpassword() - } - } - $wgUser = $u; $wgUser->setCookies(); $wgUser->saveSettings(); + if( $wgUser->isValidEmailAddr( $wgUser->getEmail() ) ) { + $wgUser->sendConfirmationMail(); + } if( $this->hasSessionCookie() ) { return $this->successfulLogin( wfMsg( 'welcomecreation', $wgUser->getName() ) ); @@ -275,8 +237,7 @@ class LoginForm { * @access private */ function processLogin() { - global $wgUser, $wgLang; - global $wgEmailAuthentication; + global $wgUser; if ( '' == $this->mName ) { $this->mainLoginForm( wfMsg( 'noname' ) ); @@ -309,14 +270,6 @@ class LoginForm { $u->loadFromDatabase(); } - # store temporarily the status before the password check is performed - $mailmsg = ''; - $oldadr = strtolower($u->getEmail()); - $newadr = strtolower($this->mEmail); - $alreadyauthenticated = (( $u->mEmailAuthenticationtimestamp != 0 ) || ($oldadr == '')) ; - - # checkPassword sets EmailAuthenticationtimestamp, if the newPassword is used - if (!$u->checkPassword( $this->mPassword )) { $this->mainLoginForm( wfMsg( 'wrongpassword' ) ); return; @@ -331,54 +284,13 @@ class LoginForm { } $u->setOption( 'rememberpassword', $r ); - /* check if user with correct password has entered a new email address */ - if (($newadr <> '') && ($newadr <> $oldadr)) { # the user supplied a new email address on the login page - - # prepare for authentication and mail a temporary password to newadr - if ( !$u->isValidEmailAddr( $newadr ) ) { - return $this->mainLoginForm( wfMsg( 'invalidemailaddress', $error ) ); - } - $u->mEmail = $newadr; # new behaviour: store this new emailaddr from login-page now into user database record ... - $u->mEmailAuthenticationtimestamp = 0; # ... but flag the address as "dirty" (unauthenticated) - $alreadyauthenticated = false; - - if ($wgEmailAuthentication) { - - # mail a temporary one-time password to the dirty address and return here to complete the user login - # if the user returns now or later using this temp. password, then the new email address $newadr - # - which is already stored in his user record - gets authenticated in checkpassword() - - $error = $this->mailPasswordInternal( $u, false, $newpassword_temp); - $u->mNewpassword = $newpassword_temp; - - # The temporary password is mailed. The user is logged-in as he entered his correct password - # This appears to be more intuitive than alternative 2. - - if ($error === '') { - $mailmsg = '
' . wfMsg( 'passwordsentforemailauthentication', $u->getName() ); - } else { - $mailmsg = '
' . wfMsg( 'mailerror', $error ) ; - } - } - } - $wgUser = $u; $wgUser->setCookies(); - # save all settings (incl. new email address and/or temporary password, if applicable) $wgUser->saveSettings(); - if ( !$wgEmailAuthentication || $alreadyauthenticated ) { - $authenticated = ''; - $mailmsg = ''; - } elseif ($u->mEmailAuthenticationtimestamp != 0) { - $authenticated = ' ' . wfMsg( 'emailauthenticated', $wgLang->timeanddate( $u->mEmailAuthenticationtimestamp, true ) ); - } else { - $authenticated = ' ' . wfMsg( 'emailnotauthenticated' ); - } - if( $this->hasSessionCookie() ) { - return $this->successfulLogin( wfMsg( 'loginsuccess', $wgUser->getName() ) . $authenticated . $mailmsg ); + return $this->successfulLogin( wfMsg( 'loginsuccess', $wgUser->getName() ) ); } else { return $this->cookieRedirectCheck( 'login' ); } @@ -407,20 +319,20 @@ class LoginForm { $u->loadFromDatabase(); - $error = $this->mailPasswordInternal( $u, true, $dummy ); - if ($error === '') { - $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ) ); + $result = $this->mailPasswordInternal( $u ); + if( WikiError::isError( $result ) ) { + $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) ); } else { - $this->mainLoginForm( wfMsg( 'mailerror', $error ) ); + $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ) ); } - return; } /** + * @return mixed true on success, WikiError on failure * @access private */ - function mailPasswordInternal( $u, $savesettings = true, &$newpassword_out ) { + function mailPasswordInternal( $u ) { global $wgPasswordSender, $wgDBname, $wgIP; global $wgCookiePath, $wgCookieDomain; @@ -431,25 +343,17 @@ class LoginForm { $np = $u->randomPassword(); $u->setNewpassword( $np ); - # we want to store this new password together with other values in the calling function - $newpassword_out = $u->mNewpassword; - - # WHY IS THIS HERE ? SHOULDN'T IT BE User::setcookie ??? setcookie( "{$wgDBname}Token", '', time() - 3600, $wgCookiePath, $wgCookieDomain ); - if ($savesettings) { $u->saveSettings(); - } $ip = $wgIP; if ( '' == $ip ) { $ip = '(Unknown)'; } $m = wfMsg( 'passwordremindermailbody', $ip, $u->getName(), wfUrlencode($u->getName()), $np ); - - require_once('UserMailer.php'); - $error = userMailer( $u->getEmail(), $wgPasswordSender, wfMsg( 'passwordremindermailsubject' ), $m ); + $result = $u->sendMail( wfMsg( 'passwordremindermailsubject' ), $m ); - return htmlspecialchars( $error ); + return $result; } @@ -491,7 +395,6 @@ class LoginForm { function mainLoginForm( $err ) { global $wgUser, $wgOut, $wgLang; global $wgDBname, $wgAllowRealName, $wgEnableEmail; - global $wgEmailAuthentication; if ( '' == $this->mName ) { if ( $wgUser->isLoggedIn() ) { @@ -522,7 +425,6 @@ class LoginForm { $template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() ); $template->set( 'userealname', $wgAllowRealName ); $template->set( 'useemail', $wgEnableEmail ); - $template->set( 'useemailauthent', $wgEmailAuthentication ); $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember ); $wgOut->setPageTitle( wfMsg( 'userlogin' ) ); diff --git a/includes/User.php b/includes/User.php index ae3d43df37..8ad9ae2ee0 100644 --- a/includes/User.php +++ b/includes/User.php @@ -23,7 +23,7 @@ class User { * @access private */ var $mId, $mName, $mPassword, $mEmail, $mNewtalk; - var $mEmailAuthenticationtimestamp; + var $mEmailAuthenticated; var $mRights, $mOptions; var $mDataLoaded, $mNewpassword; var $mSkin; @@ -43,8 +43,9 @@ class User { /** * Static factory method - * @static * @param string $name Username, validated by Title:newFromText() + * @return User + * @static */ function newFromName( $name ) { $u = new User(); @@ -60,6 +61,30 @@ class User { return $u; } } + + /** + * Factory method to fetch whichever use has a given email confirmation code. + * This code is generated when an account is created or its e-mail address + * has changed. + * + * If the code is invalid or has expired, returns NULL. + * + * @param string $code + * @return User + * @static + */ + function newFromConfirmationCode( $code ) { + $dbr =& wfGetDB( DB_SLAVE ); + $name = $dbr->selectField( 'user', 'user_name', array( + 'user_email_token' => md5( $code ), + 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ), + ) ); + if( is_string( $name ) ) { + return User::newFromName( $name ); + } else { + return null; + } + } /** * Get username given an id. @@ -128,7 +153,8 @@ class User { function isValidEmailAddr ( $addr ) { # There used to be a regular expression here, it got removed because it # rejected valid addresses. - return true; + return ( trim( $addr ) != '' ) && + (false !== strpos( $addr, '@' ) ); } /** @@ -166,7 +192,7 @@ class User { $this->mNewtalk = -1; $this->mName = $wgIP; $this->mRealName = $this->mEmail = ''; - $this->mEmailAuthenticationtimestamp = 0; + $this->mEmailAuthenticated = null; $this->mPassword = $this->mNewpassword = ''; $this->mRights = array(); $this->mGroups = array(); @@ -475,14 +501,14 @@ class User { $dbr =& wfGetDB( DB_SLAVE ); $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email', - 'user_emailauthenticationtimestamp', + 'user_email_authenticated', 'user_real_name','user_options','user_touched', 'user_token' ), array( 'user_id' => $this->mId ), $fname ); if ( $s !== false ) { $this->mName = $s->user_name; $this->mEmail = $s->user_email; - $this->mEmailAuthenticationtimestamp = wfTimestamp(TS_MW,$s->user_emailauthenticationtimestamp); + $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated ); $this->mRealName = $s->user_real_name; $this->mPassword = $s->user_password; $this->mNewpassword = $s->user_newpassword; @@ -667,9 +693,9 @@ class User { return $this->mEmail; } - function getEmailAuthenticationtimestamp() { + function getEmailAuthenticationTimestamp() { $this->loadFromDatabase(); - return $this->mEmailAuthenticationtimestamp; + return $this->mEmailAuthenticated; } function setEmail( $str ) { @@ -1022,7 +1048,7 @@ class User { 'user_newpassword' => $this->mNewpassword, 'user_real_name' => $this->mRealName, 'user_email' => $this->mEmail, - 'user_emailauthenticationtimestamp' => $dbw->timestamp($this->mEmailAuthenticationtimestamp), + 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 'user_options' => $this->encodeOptions(), 'user_touched' => $dbw->timestamp($this->mTouched), 'user_token' => $this->mToken @@ -1082,7 +1108,7 @@ class User { 'user_password' => $this->mPassword, 'user_newpassword' => $this->mNewpassword, 'user_email' => $this->mEmail, - 'user_emailauthenticationtimestamp' => $dbw->timestamp($this->mEmailAuthenticationtimestamp), + 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 'user_real_name' => $this->mRealName, 'user_options' => $this->encodeOptions(), 'user_token' => $this->mToken @@ -1253,8 +1279,13 @@ class User { if ( 0 == strcmp( $ep, $this->mPassword ) ) { return true; } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) { - $this->mEmailAuthenticationtimestamp = wfTimestampNow(); - $this->mNewpassword = ''; # use the temporary one-time password only once: clear it now ! + # If e-mail confirmation hasn't been done already, + # we may as well confirm it here -- the user can only + # get this password via e-mail. + $this->mEmailAuthenticated = wfTimestampNow(); + + # use the temporary one-time password only once: clear it now ! + $this->mNewpassword = ''; $this->saveSettings(); return true; } elseif ( function_exists( 'iconv' ) ) { @@ -1281,7 +1312,7 @@ class User { */ function editToken( $salt = '' ) { if( !isset( $_SESSION['wsEditToken'] ) ) { - $token = dechex( mt_rand() ) . dechex( mt_rand() ); + $token = $this->generateToken(); $_SESSION['wsEditToken'] = $token; } else { $token = $_SESSION['wsEditToken']; @@ -1292,6 +1323,16 @@ class User { return md5( $token . $salt ); } + /** + * Generate a hex-y looking random token for various uses. + * Could be made more cryptographically sure if someone cares. + * @return string + */ + function generateToken( $salt = '' ) { + $token = dechex( mt_rand() ) . dechex( mt_rand() ); + return md5( $token . $salt ); + } + /** * Check given value against the token value stored in the session. * A match should confirm that the form was submitted from the @@ -1306,6 +1347,138 @@ class User { function matchEditToken( $val, $salt = '' ) { return ( $val == $this->editToken( $salt ) ); } + + /** + * Generate a new e-mail confirmation token and send a confirmation + * mail to the user's given address. + * + * @return mixed True on success, a WikiError object on failure. + */ + function sendConfirmationMail() { + global $wgIP, $wgContLang; + $url = $this->confirmationTokenUrl( $expiration ); + return $this->sendMail( wfMsg( 'confirmemail_subject' ), + wfMsg( 'confirmemail_body', + $wgIP, + $this->getName(), + $url, + $wgContLang->timeanddate( $expiration, false ) ) ); + } + + /** + * Send an e-mail to this user's account. Does not check for + * confirmed status or validity. + * + * @param string $subject + * @param string $body + * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise. + * @return mixed True on success, a WikiError object on failure. + */ + function sendMail( $subject, $body, $from = null ) { + if( is_null( $from ) ) { + global $wgPasswordSender; + $from = $wgPasswordSender; + } + + require_once( 'UserMailer.php' ); + $error = userMailer( $this->getEmail(), $from, $subject, $body ); + + if( $error == '' ) { + return true; + } else { + return new WikiError( $error ); + } + } + + /** + * Generate, store, and return a new e-mail confirmation code. + * A hash (unsalted since it's used as a key) is stored. + * @param &$expiration mixed output: accepts the expiration time + * @return string + * @access private + */ + function confirmationToken( &$expiration ) { + $fname = 'User::confirmationToken'; + + $now = time(); + $expires = $now + 7 * 24 * 60 * 60; + $expiration = wfTimestamp( TS_MW, $expires ); + + $token = $this->generateToken( $this->mId . $this->mEmail . $expires ); + $hash = md5( $token ); + + $dbw =& wfGetDB( DB_MASTER ); + $dbw->update( 'user', + array( 'user_email_token' => $hash, + 'user_email_token_expires' => $dbw->timestamp( $expires ) ), + array( 'user_id' => $this->mId ), + $fname ); + + return $token; + } + + /** + * Generate and store a new e-mail confirmation token, and return + * the URL the user can use to confirm. + * @param &$expiration mixed output: accepts the expiration time + * @return string + * @access private + */ + function confirmationTokenUrl( &$expiration ) { + $token = $this->confirmationToken( $expiration ); + $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token ); + return $title->getFullUrl(); + } + + /** + * Mark the e-mail address confirmed and save. + */ + function confirmEmail() { + $this->loadFromDatabase(); + $this->mEmailAuthenticated = wfTimestampNow(); + $this->saveSettings(); + return true; + } + + /** + * Is this user allowed to send e-mails within limits of current + * site configuration? + * @return bool + */ + function canSendEmail() { + return $this->isEmailConfirmed(); + } + + /** + * Is this user allowed to receive e-mails within limits of current + * site configuration? + * @return bool + */ + function canReceiveEmail() { + return $this->canSendEmail() && !$this->getOption( 'disablemail' ); + } + + /** + * Is this user's e-mail address valid-looking and confirmed within + * limits of the current site configuration? + * + * If $wgEmailAuthentication is on, this may require the user to have + * confirmed their address by returning a code or using a password + * sent to the address from the wiki. + * + * @return bool + */ + function isEmailConfirmed() { + global $wgEmailAuthentication; + $this->loadFromDatabase(); + if( $this->isAnon() ) + return false; + if( !$this->isValidEmailAddr( $this->mEmail ) ) + return false; + if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) + return false; + return true; + } } ?> diff --git a/includes/UserMailer.php b/includes/UserMailer.php index f5f27f6284..80eb445c72 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -25,6 +25,8 @@ * @package MediaWiki */ +require_once( 'WikiError.php' ); + /** * Provide mail capabilities * @param string $string ???? @@ -230,8 +232,7 @@ class EmailNotification { if ( ( $enotifwatchlistpage && $watchingUser->getOption('enotifwatchlistpages') ) || ( $enotifusertalkpage && $watchingUser->getOption('enotifusertalkpages') ) && (!$currentMinorEdit || ($wgEmailNotificationForMinorEdits && $watchingUser->getOption('enotifminoredits') ) ) - && ($watchingUser->getEmail() != '') - && (!$wgEmailAuthentication || ($watchingUser->getEmailAuthenticationtimestamp() != 0 ) ) ) { + && ($watchingUser->isEmailConfirmed() ) ) { # ... adjust remaining text and page edit time placeholders # which needs to be personalized for each user $sent = $this->composeAndSendPersonalisedMail( $watchingUser, $mail, $article ); diff --git a/includes/templates/Confirmemail.php b/includes/templates/Confirmemail.php new file mode 100644 index 0000000000..ec1dc1fb1d --- /dev/null +++ b/includes/templates/Confirmemail.php @@ -0,0 +1,40 @@ +data['error'] ) { +?> +
msgWiki( $this->data['error']) ?>
+ +
+ msgWiki( 'confirmemail_text' ) ?> +
+ +
+ + + + + + + +
+
+ \ No newline at end of file diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index 5bceb01a57..6188d88bc3 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -98,11 +98,7 @@ class UserloginTemplate extends QuickTemplate {

msgHtml( 'emailforlost' ) ?>
- msg('mailmypassword') ?>" /> - + value="msg('mailmypassword') ?>" />

diff --git a/languages/Language.php b/languages/Language.php index cf2674fac1..5c570c57ea 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -625,8 +625,8 @@ Please re-login with that for authentication purposes.", 'mailerror' => "Error sending mail: $1", 'acct_creation_throttle_hit' => 'Sorry, you have already created $1 accounts. You can\'t make any more.', 'emailauthenticated' => 'Your email address was authenticated on $1.', -'emailnotauthenticated' => 'Your email address is not yet authenticated and the advanced email features are disabled until authentication (d.u.a.).
-To authenticate, please login in with the temporary password which has been mailed to you, or request a new one on the login page.', +'emailnotauthenticated' => 'Your email address is not yet authenticated and the advanced email features are disabled until authentication (d.u.a.).', +'emailconfirmlink' => 'Confirm your e-mail address', 'invalidemailaddress' => 'The email address cannot be accepted as it appears to have an invalid format. Please enter a well-formatted address or empty that field.', 'disableduntilauthent' => '(d.u.a.)', 'disablednoemail' => '(disabled; no email address)', @@ -1864,6 +1864,34 @@ ta[\'ca-nstab-category\'] = new Array(\'c\',\'View the category page\'); 'watchlistall1' => 'all', 'watchlistall2' => 'all', 'contributionsall' => 'all', + +# E-mail address confirmation +'confirmemail' => 'Confirm E-mail address', +'confirmemail_text' => "This wiki requires that you validate your e-mail address +before using e-mail features. Activate the button below to send a confirmation +mail to your address. The mail will include a link containing a code; load the +link in your browser to confirm that your e-mail address is valid.", +'confirmemail_send' => 'Mail a confirmation code', +'confirmemail_sent' => 'Confirmation e-mail sent.', +'confirmemail_sendfailed' => 'Could not send confirmation mail. Check address for invalid characters.', +'confirmemail_invalid' => 'Invalid confirmation code. The code may have expired.', +'confirmemail_success' => 'Your e-mail address has been confirmed. You may now log in and enjoy the wiki.', +'confirmemail_loggedin' => 'Your e-mail address has now been confirmed.', +'confirmemail_error' => 'Something went wrong saving your confirmation.', + +'confirmemail_subject' => '{{SITENAME}} e-mail address confirmation', +'confirmemail_body' => "Someone, probably you from IP address $1, has registered an +account \"$2\" with this e-mail address on {{SITENAME}}. + +To confirm that this account really does belong to you and activate +e-mail features on {{SITENAME}}, open this link in your browser: + +$3 + +If this is *not* you, don't follow the link. This confirmation code +will expire at $4. +", + ); /* a fake language converter */ diff --git a/maintenance/archives/patch-email-authentication.sql b/maintenance/archives/patch-email-authentication.sql index 7a33ac417a..b35b10f15e 100644 --- a/maintenance/archives/patch-email-authentication.sql +++ b/maintenance/archives/patch-email-authentication.sql @@ -1,3 +1,3 @@ --- Patch for email authentication T.Gries/M.Arndt 27.11.2004 --- A new column is added to the table 'user'. -ALTER TABLE /*$wgDBprefix*/user ADD (user_emailauthenticationtimestamp varchar(14) binary NOT NULL default '0'); +-- Added early in 1.5 alpha development, removed 2005-04-25 + +ALTER TABLE /*$wgDBprefix*/user DROP COLUMN user_emailauthenticationtimestamp; diff --git a/maintenance/archives/patch-user_email_token.sql b/maintenance/archives/patch-user_email_token.sql new file mode 100644 index 0000000000..d4d633b776 --- /dev/null +++ b/maintenance/archives/patch-user_email_token.sql @@ -0,0 +1,12 @@ +-- +-- E-mail confirmation token and expiration timestamp, +-- for verification of e-mail addresses. +-- +-- 2005-04-25 +-- + +ALTER TABLE /*$wgDBprefix*/user + ADD COLUMN user_email_authenticated CHAR(14) BINARY, + ADD COLUMN user_email_token CHAR(32) BINARY, + ADD COLUMN user_email_token_expires CHAR(14) BINARY, + ADD INDEX (user_email_token); diff --git a/maintenance/updaters.inc b/maintenance/updaters.inc index 90af83ee95..27f9f2d2bf 100644 --- a/maintenance/updaters.inc +++ b/maintenance/updaters.inc @@ -31,6 +31,7 @@ $wgNewFields = array( array( 'recentchanges', 'rc_patrolled', 'patch-rc-patrol.sql' ), array( 'user', 'user_real_name', 'patch-user-realname.sql' ), array( 'user', 'user_token', 'patch-user_token.sql' ), + array( 'user', 'user_email_token', 'patch-user_email_token.sql' ), array( 'user_rights', 'ur_user', 'patch-rename-user_groups-and_rights.sql' ), array( 'group', 'group_rights', 'patch-userlevels-rights.sql' ), array( 'logging', 'log_params', 'patch-log_params.sql' ), @@ -216,12 +217,11 @@ function do_copy_newtalk_to_watchlist() { function do_user_update() { global $wgDatabase; if( $wgDatabase->fieldExists( 'user', 'user_emailauthenticationtimestamp' ) ) { - echo "EAUTHENT: The user table is already set up for email authentication.\n"; - } else { - echo "EAUTHENT: Adding user_emailauthenticationtimestamp field for email authentication management."; - /* ALTER TABLE user ADD (user_emailauthenticationtimestamp varchar(14) binary NOT NULL default '0'); */ + echo "User table contains old email authentication field. Dropping... "; dbsource( "maintenance/archives/patch-email-authentication.sql", $wgDatabase ); echo "ok\n"; + } else { + echo "...user table does not contain old email authentication field.\n"; } }