Clean up e-mail authentication code.
authorBrion Vibber <brion@users.mediawiki.org>
Mon, 25 Apr 2005 18:38:43 +0000 (18:38 +0000)
committerBrion Vibber <brion@users.mediawiki.org>
Mon, 25 Apr 2005 18:38:43 +0000 (18:38 +0000)
* 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

15 files changed:
includes/Database.php
includes/GlobalFunctions.php
includes/SpecialConfirmemail.php [new file with mode: 0644]
includes/SpecialEmailuser.php
includes/SpecialPage.php
includes/SpecialPreferences.php
includes/SpecialUserlogin.php
includes/User.php
includes/UserMailer.php
includes/templates/Confirmemail.php [new file with mode: 0644]
includes/templates/Userlogin.php
languages/Language.php
maintenance/archives/patch-email-authentication.sql
maintenance/archives/patch-user_email_token.sql [new file with mode: 0644]
maintenance/updaters.inc

index 4ed8c59..bfa0556 100644 (file)
@@ -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
         */
index 9e334e5..61a6abc 100644 (file)
@@ -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 (file)
index 0000000..f88545b
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Entry point to confirm a user's e-mail address.
+ * When a new address is entered, a random unique code is generated and
+ * mailed to the user. A clickable link to this page is provided.
+ *
+ * @package MediaWiki
+ * @subpackage SpecialPage
+ */
+
+function wfSpecialConfirmemail( $code ) {
+       $form = new ConfirmationForm();
+       $form->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();
+               }
+       }
+}
+
+?>
index 577a8e9..9c53a71 100644 (file)
@@ -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);
                        }
                }
        }
index 7379bcd..3d7cba2 100644 (file)
@@ -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' ),
index b608222..cf52974 100644 (file)
@@ -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 ) ).'<br />';
+                       if( $wgUser->getEmailAuthenticationTimestamp() ) {
+                               $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'<br />';
                                $disabled = '';
                        } else {
-                               $emailauthenticated = wfMsg('emailnotauthenticated').'<br />';
+                               $skin = $wgUser->getSkin();
+                               $emailauthenticated = wfMsg('emailnotauthenticated').'<br />' .
+                                       $skin->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Confirmemail' ),
+                                               wfMsg( 'emailconfirmlink' ) );
                                $disabled = ' '.wfMsg('disableduntilauthent');
                        }
                } else {
index 0702ed4..b33b953 100644 (file)
@@ -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 = '<br />' . wfMsg( 'passwordsentforemailauthentication', $u->getName() );
-                               } else {
-                                       $mailmsg = '<br />' . 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' ) );
index ae3d43d..8ad9ae2 100644 (file)
@@ -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;
+       }
 }
 
 ?>
index f5f27f6..80eb445 100644 (file)
@@ -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 (file)
index 0000000..ec1dc1f
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * @package MediaWiki
+ * @subpackage Templates
+ */
+if( !defined( 'MEDIAWIKI' ) ) die();
+
+/** */
+require_once( 'includes/SkinTemplate.php' );
+
+/**
+ * HTML template for Special:Confirmemail form
+ * @package MediaWiki
+ * @subpackage Templates
+ */
+class ConfirmemailTemplate extends QuickTemplate {
+       function execute() {
+               if( $this->data['error'] ) {
+?>
+       <div class='error'><?php $this->msgWiki( $this->data['error']) ?></div>
+<?php } else { ?>
+       <div>
+       <?php $this->msgWiki( 'confirmemail_text' ) ?>
+       </div>
+<?php } ?>
+<form name="confirmemail" id="confirmemail" method="post" action="<?php $this->text('action') ?>">
+       <input type="hidden" name="action" value="submit" />
+       <input type="hidden" name="wpEditToken" value="<?php $this->text('edittoken') ?>" />
+       <table border='0'>
+               <tr>
+                       <td></td>
+                       <td><input type="submit" name="wpConfirm" value="<?php $this->msg('confirmemail_send') ?>" /></td>
+               </tr>
+       </table>
+</form>
+<?php
+       }
+}
+
+?>
\ No newline at end of file
index 5bceb01..6188d88 100644 (file)
@@ -98,11 +98,7 @@ class UserloginTemplate extends QuickTemplate {
                                <p>
                                        <?php $this->msgHtml( 'emailforlost' ) ?><br />
                                        <input tabindex='10' type='submit' name="wpMailmypassword"
-                                               value="<?php if ( $this->data['useemailauthent'] ) {
-                                                               $this->msg('mailmypasswordauthent') ?>" />
-                                                       <?php } else {
-                                                               $this->msg('mailmypassword') ?>" />
-                                                       <?php } ?>
+                                               value="<?php $this->msg('mailmypassword') ?>" />
                                </p>
                        </td>
                </tr>
index cf2674f..5c570c5 100644 (file)
@@ -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 <strong>not yet authenticated</strong> and the advanced email features are disabled until authentication <strong>(d.u.a.)</strong>.<br />
-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 <strong>not yet authenticated</strong> and the advanced email features are disabled until authentication <strong>(d.u.a.)</strong>.',
+'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' => '<strong>(d.u.a.)</strong>',
 'disablednoemail'      => '<strong>(disabled; no email address)</strong>',
@@ -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 */
index 7a33ac4..b35b10f 100644 (file)
@@ -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 (file)
index 0000000..d4d633b
--- /dev/null
@@ -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);
index 90af83e..27f9f2d 100644 (file)
@@ -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";
        }
 }