X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FUser.php;h=1e3809d1414e2c0ff9b43fdf421a493634a03356;hb=7c82730aa2ff685712718c54cb1920cb7738e53a;hp=faf7ab560854ee579a6ad8aa06d14864969f2ceb;hpb=63d232acf4ddb2b2b5da215ac95a50584cbd4b77;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/User.php b/includes/User.php index faf7ab5608..1e3809d141 100644 --- a/includes/User.php +++ b/includes/User.php @@ -1,6 +1,6 @@ loadDefaults(); + $this->mVersion = MW_USER_VERSION; } /** * Static factory method - * @static * @param string $name Username, validated by Title:newFromText() + * @return User + * @static */ function newFromName( $name ) { - $u = new User(); + # Force usernames to capital + global $wgContLang; + $name = $wgContLang->ucfirst( $name ); # Clean up name according to title rules - $t = Title::newFromText( $name ); if( is_null( $t ) ) { - return NULL; + return null; + } + + # Reject various classes of invalid names + $canonicalName = $t->getText(); + global $wgAuth; + $canonicalName = $wgAuth->getCanonicalName( $t->getText() ); + + if( !User::isValidUserName( $canonicalName ) ) { + return null; + } + + $u = new User(); + $u->setName( $canonicalName ); + $u->setId( $u->idFromName( $canonicalName ) ); + 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 { - $u->setName( $t->getText() ); - $u->setId( $u->idFromName( $t->getText() ) ); - return $u; + return null; } } + /** + * Serialze sleep function, for better cache efficiency and avoidance of + * silly "incomplete type" errors when skins are cached + */ + function __sleep() { + return array( 'mId', 'mName', 'mPassword', 'mEmail', 'mNewtalk', + 'mEmailAuthenticated', 'mRights', 'mOptions', 'mDataLoaded', + 'mNewpassword', 'mBlockedby', 'mBlockreason', 'mTouched', + 'mToken', 'mRealName', 'mHash', 'mGroups' ); + } + /** * Get username given an id. * @param integer $id Database user id @@ -69,7 +118,7 @@ class User { */ function whoIs( $id ) { $dbr =& wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ) ); + return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' ); } /** @@ -80,7 +129,7 @@ class User { */ function whoIsReal( $id ) { $dbr =& wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ) ); + return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' ); } /** @@ -108,21 +157,108 @@ class User { } /** - * does the string match an anonymous user IP address? - * @param string $name Nickname of a user + * does the string match an anonymous IPv4 address? + * + * Note: We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP + * address because the usemod software would "cloak" anonymous IP + * addresses like this, if we allowed accounts like this to be created + * new users could get the old edits of these anonymous users. + * + * @bug 3631 + * * @static + * @param string $name Nickname of a user + * @return bool */ function isIP( $name ) { - return preg_match("/^\d{1,3}\.\d{1,3}.\d{1,3}\.\d{1,3}$/",$name); + return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/",$name); + /*return preg_match("/^ + (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\. + (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\. + (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\. + (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5])) + $/x", $name);*/ } /** - * does the string match roughly an email address ? + * Is the input a valid username? + * + * Checks if the input is a valid username, we don't want an empty string, + * an IP address, anything that containins slashes (would mess up subpages), + * is longer than the maximum allowed username size or doesn't begin with + * a capital letter. + * + * @param string $name + * @return bool + * @static + */ + function isValidUserName( $name ) { + global $wgContLang, $wgMaxNameChars; + + if ( $name == '' + || User::isIP( $name ) + || strpos( $name, '/' ) !== false + || strlen( $name ) > $wgMaxNameChars + || $name != $wgContLang->ucfirst( $name ) ) + return false; + + // Ensure that the name can't be misresolved as a different title, + // such as with extra namespace keys at the start. + $parsed = Title::newFromText( $name ); + if( is_null( $parsed ) + || $parsed->getNamespace() + || strcmp( $name, $parsed->getPrefixedText() ) ) + return false; + else + return true; + } + + /** + * Is the input a valid password? + * + * @param string $password + * @return bool + * @static + */ + function isValidPassword( $password ) { + global $wgMinimalPasswordLength; + return strlen( $password ) >= $wgMinimalPasswordLength; + } + + /** + * Does the string match roughly an email address ? + * + * There used to be a regular expression here, it got removed because it + * rejected valid addresses. Actually just check if there is '@' somewhere + * in the given address. + * + * @todo Check for RFC 2822 compilance + * @bug 959 + * * @param string $addr email address * @static + * @return bool */ function isValidEmailAddr ( $addr ) { - return preg_match( '/^([a-z0-9_.-]+([a-z0-9_.-]+)*\@[a-z0-9_-]+([a-z0-9_.-]+)*([a-z.]{2,})+)$/', strtolower($addr)); + return ( trim( $addr ) != '' ) && + (false !== strpos( $addr, '@' ) ); + } + + /** + * Count the number of edits of a user + * + * @param int $uid The user ID to check + * @return int + */ + function edits( $uid ) { + $fname = 'User::edits'; + + $dbr =& wfGetDB( DB_SLAVE ); + return $dbr->selectField( + 'revision', 'count(*)', + array( 'rev_user' => $uid ), + $fname + ); } /** @@ -152,44 +288,39 @@ class User { $n++; $fname = 'User::loadDefaults' . $n; wfProfileIn( $fname ); - - global $wgContLang, $wgIP; + + global $wgContLang, $wgDBname; global $wgNamespacesToBeSearchedDefault; $this->mId = 0; $this->mNewtalk = -1; - $this->mName = $wgIP; + $this->mName = false; $this->mRealName = $this->mEmail = ''; - $this->mEmailAuthenticationtimestamp = 0; + $this->mEmailAuthenticated = null; $this->mPassword = $this->mNewpassword = ''; $this->mRights = array(); $this->mGroups = array(); - // Getting user defaults only if we have an available language - if( isset( $wgContLang ) ) { - $this->loadDefaultFromLanguage(); - } - + $this->mOptions = User::getDefaultOptions(); + foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) { $this->mOptions['searchNs'.$nsnum] = $val; } unset( $this->mSkin ); $this->mDataLoaded = false; $this->mBlockedby = -1; # Unset - $this->mTouched = '0'; # Allow any pages to be cached $this->setToken(); # Random $this->mHash = false; + + if ( isset( $_COOKIE[$wgDBname.'LoggedOut'] ) ) { + $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgDBname.'LoggedOut'] ); + } + else { + $this->mTouched = '0'; # Allow any pages to be cached + } + wfProfileOut( $fname ); } - /** - * Used to load user options from a language. - * This is not in loadDefault() cause we sometime create user before having - * a language object. - */ - function loadDefaultFromLanguage(){ - $this->mOptions = User::getDefaultOptions(); - } - /** * Combine the language default options with any site-specific options * and add the default language variants. @@ -204,17 +335,17 @@ class User { */ global $wgContLang, $wgDefaultUserOptions; $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptions(); - + /** * default language setting */ $variant = $wgContLang->getPreferredVariant(); $defOpt['variant'] = $variant; $defOpt['language'] = $variant; - + return $defOpt; } - + /** * Get a given default option value. * @@ -235,26 +366,55 @@ class User { /** * Get blocking information * @access private + * @param bool $bFromSlave Specify whether to check slave or master. To improve performance, + * non-critical checks are done against slaves. Check when actually saving should be done against + * master. + * + * Note that even if $bFromSlave is false, the check is done first against slave, then master. + * The logic is that if blocked on slave, we'll assume it's either blocked on master or + * just slightly outta sync and soon corrected - safer to block slightly more that less. + * And it's cheaper to check slave first, then master if needed, than master always. */ - function getBlockedStatus() { - global $wgIP, $wgBlockCache, $wgProxyList; + function getBlockedStatus( $bFromSlave = true ) { + global $wgBlockCache, $wgProxyList, $wgEnableSorbs, $wgProxyWhitelist; - if ( -1 != $this->mBlockedby ) { return; } + if ( -1 != $this->mBlockedby ) { + wfDebug( "User::getBlockedStatus: already loaded.\n" ); + return; + } - $this->mBlockedby = 0; + $fname = 'User::getBlockedStatus'; + wfProfileIn( $fname ); + wfDebug( "$fname: checking...\n" ); - # User blocking - if ( $this->mId ) { - $block = new Block(); - if ( $block->load( $wgIP , $this->mId ) ) { - $this->mBlockedby = $block->mBy; - $this->mBlockreason = $block->mReason; + $this->mBlockedby = 0; + $ip = wfGetIP(); + + # User/IP blocking + $block = new Block(); + $block->forUpdate( $bFromSlave ); + if ( $block->load( $ip , $this->mId ) ) { + wfDebug( "$fname: Found block.\n" ); + $this->mBlockedby = $block->mBy; + $this->mBlockreason = $block->mReason; + if ( $this->isLoggedIn() ) { + $this->spreadBlock(); } + } else { + wfDebug( "$fname: No block.\n" ); } - # IP/range blocking + # Range blocking if ( !$this->mBlockedby ) { - $block = $wgBlockCache->get( $wgIP ); + # Check first against slave, and optionally from master. + wfDebug( "$fname: Checking range blocks\n" ); + $block = $wgBlockCache->get( $ip, true ); + if ( !$block && !$bFromSlave ) + { + # Not blocked: check against master, to make sure. + $wgBlockCache->clearLocal( ); + $block = $wgBlockCache->get( $ip, false ); + } if ( $block !== false ) { $this->mBlockedby = $block->mBy; $this->mBlockreason = $block->mReason; @@ -262,24 +422,177 @@ class User { } # Proxy blocking - if ( !$this->mBlockedby ) { - if ( array_key_exists( $wgIP, $wgProxyList ) ) { + if ( !$this->isSysop() && !in_array( $ip, $wgProxyWhitelist ) ) { + + # Local list + if ( array_key_exists( $ip, $wgProxyList ) ) { + $this->mBlockedby = wfMsg( 'proxyblocker' ); $this->mBlockreason = wfMsg( 'proxyblockreason' ); - $this->mBlockedby = "Proxy blocker"; } + + # DNSBL + if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) { + if ( $this->inSorbsBlacklist( $ip ) ) { + $this->mBlockedby = wfMsg( 'sorbs' ); + $this->mBlockreason = wfMsg( 'sorbsreason' ); + } + } + } + + # Extensions + wfRunHooks( 'GetBlockedStatus', array( &$this ) ); + + wfProfileOut( $fname ); + } + + function inSorbsBlacklist( $ip ) { + global $wgEnableSorbs; + return $wgEnableSorbs && + $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' ); + } + + function inOpmBlacklist( $ip ) { + global $wgEnableOpm; + return $wgEnableOpm && + $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' ); + } + + function inDnsBlacklist( $ip, $base ) { + $fname = 'User::inDnsBlacklist'; + wfProfileIn( $fname ); + + $found = false; + $host = ''; + + if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) { + # Make hostname + for ( $i=4; $i>=1; $i-- ) { + $host .= $m[$i] . '.'; + } + $host .= $base; + + # Send query + $ipList = gethostbynamel( $host ); + + if ( $ipList ) { + wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" ); + $found = true; + } else { + wfDebug( "Requested $host, not found in $base.\n" ); + } + } + + wfProfileOut( $fname ); + return $found; + } + + /** + * Primitive rate limits: enforce maximum actions per time period + * to put a brake on flooding. + * + * Note: when using a shared cache like memcached, IP-address + * last-hit counters will be shared across wikis. + * + * @return bool true if a rate limiter was tripped + * @access public + */ + function pingLimiter( $action='edit' ) { + global $wgRateLimits; + if( !isset( $wgRateLimits[$action] ) ) { + return false; + } + if( $this->isAllowed( 'delete' ) ) { + // goddam cabal + return false; + } + + global $wgMemc, $wgDBname, $wgRateLimitLog; + $fname = 'User::pingLimiter'; + wfProfileIn( $fname ); + + $limits = $wgRateLimits[$action]; + $keys = array(); + $id = $this->getId(); + $ip = wfGetIP(); + + if( isset( $limits['anon'] ) && $id == 0 ) { + $keys["$wgDBname:limiter:$action:anon"] = $limits['anon']; + } + + if( isset( $limits['user'] ) && $id != 0 ) { + $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user']; + } + if( $this->isNewbie() ) { + if( isset( $limits['newbie'] ) && $id != 0 ) { + $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie']; + } + if( isset( $limits['ip'] ) ) { + $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip']; + } + if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) { + $subnet = $matches[1]; + $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet']; + } + } + + $triggered = false; + foreach( $keys as $key => $limit ) { + list( $max, $period ) = $limit; + $summary = "(limit $max in {$period}s)"; + $count = $wgMemc->get( $key ); + if( $count ) { + if( $count > $max ) { + wfDebug( "$fname: tripped! $key at $count $summary\n" ); + if( $wgRateLimitLog ) { + @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog ); + } + $triggered = true; + } else { + wfDebug( "$fname: ok. $key at $count $summary\n" ); + } + } else { + wfDebug( "$fname: adding record for $key $summary\n" ); + $wgMemc->add( $key, 1, intval( $period ) ); + } + $wgMemc->incr( $key ); } + + wfProfileOut( $fname ); + return $triggered; } /** * Check if user is blocked * @return bool True if blocked, false otherwise */ - function isBlocked() { - $this->getBlockedStatus(); - if ( 0 === $this->mBlockedby ) { return false; } - return true; + function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site + wfDebug( "User::isBlocked: enter\n" ); + $this->getBlockedStatus( $bFromSlave ); + return $this->mBlockedby !== 0; } - + + /** + * Check if user is blocked from editing a particular article + */ + function isBlockedFrom( $title, $bFromSlave = false ) { + global $wgBlockAllowsUTEdit; + $fname = 'User::isBlockedFrom'; + wfProfileIn( $fname ); + wfDebug( "$fname: enter\n" ); + + if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() && + $title->getNamespace() == NS_USER_TALK ) + { + $blocked = false; + wfDebug( "$fname: self-talk page, ignoring any blocks\n" ); + } else { + wfDebug( "$fname: asking isBlocked()\n" ); + $blocked = $this->isBlocked( $bFromSlave ); + } + wfProfileOut( $fname ); + return $blocked; + } + /** * Get name of blocker * @return string name of blocker @@ -288,7 +601,7 @@ class User { $this->getBlockedStatus(); return $this->mBlockedby; } - + /** * Get blocking reason * @return string Blocking reason @@ -316,7 +629,7 @@ class User { } /** - * Read datas from session + * Create a new user object using data from session * @static */ function loadFromSession() { @@ -329,7 +642,7 @@ class User { return new User(); } } else if ( isset( $_COOKIE["{$wgDBname}UserID"] ) ) { - $sId = IntVal( $_COOKIE["{$wgDBname}UserID"] ); + $sId = intval( $_COOKIE["{$wgDBname}UserID"] ); $_SESSION['wsUserID'] = $sId; } else { return new User(); @@ -345,6 +658,10 @@ class User { $passwordCorrect = FALSE; $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" ); + if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) { + # Expire old serialized objects; they may be corrupt. + $user = false; + } if($makenew = !$user) { wfDebug( "User::loadFromSession() unable to load from memcached\n" ); $user = new User(); @@ -369,7 +686,6 @@ class User { else wfDebug( "User::loadFromSession() unable to save to memcached\n" ); } - $user->spreadBlock(); return $user; } return new User(); # Can't log in from session @@ -379,39 +695,37 @@ class User { * Load a user from the database */ function loadFromDatabase() { - global $wgCommandLineMode, $wgAnonGroupId, $wgLoggedInGroupId; + global $wgCommandLineMode; $fname = "User::loadFromDatabase"; - if ( $this->mDataLoaded || $wgCommandLineMode ) { + + # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress + # loading in a command line script, don't assume all command line scripts need it like this + #if ( $this->mDataLoaded || $wgCommandLineMode ) { + if ( $this->mDataLoaded ) { return; } # Paranoia - $this->mId = IntVal( $this->mId ); + $this->mId = intval( $this->mId ); /** Anonymous user */ - if(!$this->mId) { + if( !$this->mId ) { /** Get rights */ - $anong = Group::newFromId($wgAnonGroupId); - if (!$anong) - wfDebugDieBacktrace("Please update your database schema " - ."and populate initial group data from " - ."maintenance/archives patches"); - $anong->loadFromDatabase(); - $this->mRights = explode(',', $anong->getRights()); + $this->mRights = $this->getGroupPermissions( array( '*' ) ); $this->mDataLoaded = true; return; } # the following stuff is for non-anonymous users only - + $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 = $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; @@ -419,28 +733,16 @@ class User { $this->mTouched = wfTimestamp(TS_MW,$s->user_touched); $this->mToken = $s->user_token; - // Get groups id - $res = $dbr->select( 'user_groups', array( 'ug_group' ), array( 'ug_user' => $this->mId ) ); - - while($group = $dbr->fetchRow($res)) { - $this->mGroups[] = $group[0]; - } - - // add the default group for logged in user - $this->mGroups[] = $wgLoggedInGroupId; - - $this->mRights = array(); - // now we merge groups rights to get this user rights - foreach($this->mGroups as $aGroupId) { - $g = Group::newFromId($aGroupId); - $g->loadFromDatabase(); - $this->mRights = array_merge($this->mRights, explode(',', $g->getRights())); + $res = $dbr->select( 'user_groups', + array( 'ug_group' ), + array( 'ug_user' => $this->mId ), + $fname ); + $this->mGroups = array(); + while( $row = $dbr->fetchObject( $res ) ) { + $this->mGroups[] = $row->ug_group; } - - // array merge duplicate rights which are part of several groups - $this->mRights = array_unique($this->mRights); - - $dbr->freeResult($res); + $effectiveGroups = array_merge( array( '*', 'user' ), $this->mGroups ); + $this->mRights = $this->getGroupPermissions( $effectiveGroups ); } $this->mDataLoaded = true; @@ -454,6 +756,9 @@ class User { function getName() { $this->loadFromDatabase(); + if ( $this->mName === false ) { + $this->mName = wfGetIP(); + } return $this->mName; } @@ -462,7 +767,7 @@ class User { $this->mName = $str; } - + /** * Return the title dbkey form of the name, for eg user pages. * @return string @@ -471,11 +776,12 @@ class User { function getTitleKey() { return str_replace( ' ', '_', $this->getName() ); } - + function getNewtalk() { + global $wgUseEnotif; $fname = 'User::getNewtalk'; $this->loadFromDatabase(); - + # Load the newtalk status if it is unloaded (mNewtalk=-1) if( $this->mNewtalk == -1 ) { $this->mNewtalk = 0; # reset talk page status @@ -484,27 +790,40 @@ class User { # entire User object stored in there. if( !$this->mId ) { global $wgDBname, $wgMemc; - $key = "$wgDBname:newtalk:ip:{$this->mName}"; + $key = "$wgDBname:newtalk:ip:" . $this->getName(); $newtalk = $wgMemc->get( $key ); if( is_integer( $newtalk ) ) { $this->mNewtalk = $newtalk ? 1 : 0; return (bool)$this->mNewtalk; } } - + $dbr =& wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'watchlist', - array( 'wl_user' ), - array( 'wl_title' => $this->getTitleKey(), - 'wl_namespace' => NS_USER_TALK, - 'wl_user' => $this->mId, - 'wl_notificationtimestamp != 0' ), - 'User::getNewtalk' ); - if( $dbr->numRows($res) > 0 ) { - $this->mNewtalk = 1; + if ( $wgUseEnotif ) { + $res = $dbr->select( 'watchlist', + array( 'wl_user' ), + array( 'wl_title' => $this->getTitleKey(), + 'wl_namespace' => NS_USER_TALK, + 'wl_user' => $this->mId, + 'wl_notificationtimestamp ' . $dbr->notNullTimestamp() ), + 'User::getNewtalk' ); + if( $dbr->numRows($res) > 0 ) { + $this->mNewtalk = 1; + } + $dbr->freeResult( $res ); + } elseif ( $this->mId ) { + $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId ), $fname ); + + if ( $dbr->numRows($res)>0 ) { + $this->mNewtalk= 1; + } + $dbr->freeResult( $res ); + } else { + $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->getName() ), $fname ); + $this->mNewtalk = $dbr->numRows( $res ) > 0 ? 1 : 0; + $dbr->freeResult( $res ); } - $dbr->freeResult( $res ); - + if( !$this->mId ) { $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 ); } @@ -520,8 +839,9 @@ class User { } function invalidateCache() { + global $wgClockSkewFudge; $this->loadFromDatabase(); - $this->mTouched = wfTimestampNow(); + $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge ); # Don't forget to save the options after this or # it won't take effect! } @@ -531,28 +851,14 @@ class User { return ($timestamp >= $this->mTouched); } - /** - * Salt a password. - * Will only be salted if $wgPasswordSalt is true - * @param string Password. - * @return string Salted password or clear password. - */ - function addSalt( $p ) { - global $wgPasswordSalt; - if($wgPasswordSalt) - return md5( "{$this->mId}-{$p}" ); - else - return $p; - } - /** * Encrypt a password. * It can eventuall salt a password @see User::addSalt() * @param string $p clear Password. - * @param string Encrypted password. + * @return string Encrypted password. */ function encryptPassword( $p ) { - return $this->addSalt( md5( $p ) ); + return wfEncryptPassword( $this->mId, $p ); } # Set the password and reset the random token @@ -565,19 +871,22 @@ class User { # Set the random token (used for persistent authentication) function setToken( $token = false ) { + global $wgSecretKey, $wgProxyKey, $wgDBname; if ( !$token ) { - $this->mToken = ''; - # Take random data from PRNG - # This is reasonably secure if the PRNG has been seeded correctly - for ($i = 0; $imToken .= sprintf( "%04X", mt_rand( 0, 65535 ) ); + if ( $wgSecretKey ) { + $key = $wgSecretKey; + } elseif ( $wgProxyKey ) { + $key = $wgProxyKey; + } else { + $key = microtime(); } + $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId ); } else { $this->mToken = $token; } } - + function setCookiePassword( $str ) { $this->loadFromDatabase(); $this->mCookiePassword = md5( $str ); @@ -593,9 +902,9 @@ class User { return $this->mEmail; } - function getEmailAuthenticationtimestamp() { + function getEmailAuthenticationTimestamp() { $this->loadFromDatabase(); - return $this->mEmailAuthenticationtimestamp; + return $this->mEmailAuthenticated; } function setEmail( $str ) { @@ -616,7 +925,7 @@ class User { function getOption( $oname ) { $this->loadFromDatabase(); if ( array_key_exists( $oname, $this->mOptions ) ) { - return $this->mOptions[$oname]; + return trim( $this->mOptions[$oname] ); } else { return ''; } @@ -636,59 +945,111 @@ class User { $this->loadFromDatabase(); return $this->mRights; } - - function addRight( $rname ) { - $this->loadFromDatabase(); - array_push( $this->mRights, $rname ); - $this->invalidateCache(); - } + /** + * Get the list of explicit group memberships this user has. + * The implicit * and user groups are not included. + * @return array of strings + */ function getGroups() { $this->loadFromDatabase(); return $this->mGroups; } - function setGroups($groups) { - $this->loadFromDatabase(); - $this->mGroups = $groups; + /** + * Get the list of implicit group memberships this user has. + * This includes all explicit groups, plus 'user' if logged in + * and '*' for all accounts. + * @return array of strings + */ + function getEffectiveGroups() { + $base = array( '*' ); + if( $this->isLoggedIn() ) { + $base[] = 'user'; + } + return array_merge( $base, $this->getGroups() ); + } + + /** + * Remove the user from the given group. + * This takes immediate effect. + * @string $group + */ + function addGroup( $group ) { + $dbw =& wfGetDB( DB_MASTER ); + $dbw->insert( 'user_groups', + array( + 'ug_user' => $this->getID(), + 'ug_group' => $group, + ), + 'User::addGroup', + array( 'IGNORE' ) ); + + $this->mGroups = array_merge( $this->mGroups, array( $group ) ); + $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() ); + $this->invalidateCache(); + $this->saveSettings(); + } + + /** + * Remove the user from the given group. + * This takes immediate effect. + * @string $group + */ + function removeGroup( $group ) { + $dbw =& wfGetDB( DB_MASTER ); + $dbw->delete( 'user_groups', + array( + 'ug_user' => $this->getID(), + 'ug_group' => $group, + ), + 'User::removeGroup' ); + + $this->mGroups = array_diff( $this->mGroups, array( $group ) ); + $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() ); + + $this->invalidateCache(); + $this->saveSettings(); + } + + + /** + * A more legible check for non-anonymousness. + * Returns true if the user is not an anonymous visitor. + * + * @return bool + */ + function isLoggedIn() { + return( $this->getID() != 0 ); + } + + /** + * A more legible check for anonymousness. + * Returns true if the user is an anonymous visitor. + * + * @return bool + */ + function isAnon() { + return !$this->isLoggedIn(); } /** * Check if a user is sysop - * Die with backtrace. Use User:isAllowed() instead. * @deprecated */ function isSysop() { - /** - $this->loadFromDatabase(); - if ( 0 == $this->mId ) { return false; } - - return in_array( 'sysop', $this->mRights ); - */ - wfDebugDieBacktrace("User::isSysop() is deprecated. Use User::isAllowed() instead"); + return $this->isAllowed( 'protect' ); } /** @deprecated */ function isDeveloper() { - /** - $this->loadFromDatabase(); - if ( 0 == $this->mId ) { return false; } - - return in_array( 'developer', $this->mRights ); - */ - wfDebugDieBacktrace("User::isDeveloper() is deprecated. Use User::isAllowed() instead"); + return $this->isAllowed( 'siteadmin' ); } /** @deprecated */ function isBureaucrat() { - /** - $this->loadFromDatabase(); - if ( 0 == $this->mId ) { return false; } - - return in_array( 'bureaucrat', $this->mRights ); - */ - wfDebugDieBacktrace("User::isBureaucrat() is deprecated. Use User::isAllowed() instead"); + return $this->isAllowed( 'makesysop' ); } /** @@ -697,10 +1058,6 @@ class User { */ function isBot() { $this->loadFromDatabase(); - - # Why was this here? I need a UID=0 conversion script [TS] - # if ( 0 == $this->mId ) { return false; } - return in_array( 'bot', $this->mRights ); } @@ -719,16 +1076,17 @@ class User { * @todo FIXME : need to check the old failback system [AV] */ function &getSkin() { - global $IP; + global $IP, $wgRequest; if ( ! isset( $this->mSkin ) ) { $fname = 'User::getSkin'; wfProfileIn( $fname ); - + # get all skin names available $skinNames = Skin::getSkinNames(); - + # get the user skin $userSkin = $this->getOption( 'skin' ); + $userSkin = $wgRequest->getText('useskin', $userSkin); if ( $userSkin == '' ) { $userSkin = 'standard'; } if ( !isset( $skinNames[$userSkin] ) ) { @@ -756,7 +1114,7 @@ class User { # Grab the skin class and initialise it. Each skin checks for PHPTal # and will not load if it's not enabled. require_once( $IP.'/skins/'.$sn.'.php' ); - + # Check if we got if not failback to default skin $className = 'Skin'.$sn; if( !class_exists( $className ) ) { @@ -776,7 +1134,7 @@ class User { /**#@+ * @param string $title Article title to look at */ - + /** * Check watched status of an article * @return bool True if article is watched @@ -809,19 +1167,48 @@ class User { * If e-notif e-mails are on, they will receive notification mails on * the next change of the page if it's watched etc. */ - function clearNotification( $title ) { - $dbw =& wfGetDB( DB_MASTER ); - $success = $dbw->update( 'watchlist', - array( /* SET */ - 'wl_notificationtimestamp' => 0 - ), array( /* WHERE */ - 'wl_title' => $title->getDBkey(), - 'wl_namespace' => $title->getNamespace(), - 'wl_user' => $this->getId() - ), 'User::clearLastVisited' - ); + function clearNotification( &$title ) { + global $wgUser, $wgUseEnotif; + + if ( !$wgUseEnotif ) { + return; + } + + $userid = $this->getID(); + if ($userid==0) { + return; + } + + // Only update the timestamp if the page is being watched. + // The query to find out if it is watched is cached both in memcached and per-invocation, + // and when it does have to be executed, it can be on a slave + // If this is the user's newtalk page, we always update the timestamp + if ($title->getNamespace() == NS_USER_TALK && + $title->getText() == $wgUser->getName()) + { + $watched = true; + } elseif ( $this->getID() == $wgUser->getID() ) { + $watched = $title->userIsWatching(); + } else { + $watched = true; + } + + // If the page is watched by the user (or may be watched), update the timestamp on any + // any matching rows + if ( $watched ) { + $dbw =& wfGetDB( DB_MASTER ); + $success = $dbw->update( 'watchlist', + array( /* SET */ + 'wl_notificationtimestamp' => NULL + ), array( /* WHERE */ + 'wl_title' => $title->getDBkey(), + 'wl_namespace' => $title->getNamespace(), + 'wl_user' => $this->getID() + ), 'User::clearLastVisited' + ); + } } - + /**#@-*/ /** @@ -833,8 +1220,12 @@ class User { * @access public */ function clearAllNotifications( $currentUser ) { + global $wgUseEnotif; + if ( !$wgUseEnotif ) { + return; + } if( $currentUser != 0 ) { - + $dbw =& wfGetDB( DB_MASTER ); $success = $dbw->update( 'watchlist', array( /* SET */ @@ -883,8 +1274,8 @@ class User { $_SESSION['wsUserID'] = $this->mId; setcookie( $wgDBname.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain ); - $_SESSION['wsUserName'] = $this->mName; - setcookie( $wgDBname.'UserName', $this->mName, $exp, $wgCookiePath, $wgCookieDomain ); + $_SESSION['wsUserName'] = $this->getName(); + setcookie( $wgDBname.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain ); $_SESSION['wsToken'] = $this->mToken; if ( 1 == $this->getOption( 'rememberpassword' ) ) { @@ -899,7 +1290,7 @@ class User { * It will clean the session cookie */ function logout() { - global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP; + global $wgCookiePath, $wgCookieDomain, $wgDBname; $this->loadDefaults(); $this->setLoaded( true ); @@ -907,32 +1298,23 @@ class User { setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain ); setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain ); + + # Remember when user logged out, to prevent seeing cached pages + setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain ); } /** * Save object settings into database */ function saveSettings() { - global $wgMemc, $wgDBname; + global $wgMemc, $wgDBname, $wgUseEnotif; $fname = 'User::saveSettings'; - $dbw =& wfGetDB( DB_MASTER ); - if ( ! $this->getNewtalk() ) { - # Delete the watchlist entry for user_talk page X watched by user X - $dbw->delete( 'watchlist', - array( 'wl_user' => $this->mId, - 'wl_title' => $this->getTitleKey(), - 'wl_namespace' => NS_USER_TALK ), - $fname ); - if( !$this->mId ) { - # Anon users have a separate memcache space for newtalk - # since they don't store their own info. Trim... - $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" ); - } - } - + if ( wfReadOnly() ) { return; } + $this->saveNewtalk(); if ( 0 == $this->mId ) { return; } - + + $dbw =& wfGetDB( DB_MASTER ); $dbw->update( 'user', array( /* SET */ 'user_name' => $this->mName, @@ -940,7 +1322,7 @@ class User { 'user_newpassword' => $this->mNewpassword, 'user_real_name' => $this->mRealName, 'user_email' => $this->mEmail, - 'user_emailauthenticationtimestamp' => $this->mEmailAuthenticationtimestamp, + 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 'user_options' => $this->encodeOptions(), 'user_touched' => $dbw->timestamp($this->mTouched), 'user_token' => $this->mToken @@ -948,26 +1330,83 @@ class User { 'user_id' => $this->mId ), $fname ); - $dbw->set( 'user_rights', 'ur_rights', implode( ',', $this->mRights ), - 'ur_user='. $this->mId, $fname ); $wgMemc->delete( "$wgDBname:user:id:$this->mId" ); - - // delete old groups - $dbw->delete( 'user_groups', array( 'ug_user' => $this->mId), $fname); - - // save new ones - foreach ($this->mGroups as $group) { - $dbw->replace( 'user_groups', - array(array('ug_user','ug_group')), - array( - 'ug_user' => $this->mId, - 'ug_group' => $group - ), $fname - ); + } + + /** + * Save value of new talk flag. + */ + function saveNewtalk() { + global $wgDBname, $wgMemc, $wgUseEnotif; + + $fname = 'User::saveNewtalk'; + + $changed = false; + + if ( wfReadOnly() ) { return ; } + $dbr =& wfGetDB( DB_SLAVE ); + $dbw =& wfGetDB( DB_MASTER ); + $changed = false; + if ( $wgUseEnotif ) { + if ( ! $this->getNewtalk() ) { + # Delete the watchlist entry for user_talk page X watched by user X + $dbw->delete( 'watchlist', + array( 'wl_user' => $this->mId, + 'wl_title' => $this->getTitleKey(), + 'wl_namespace' => NS_USER_TALK ), + $fname ); + if ( $dbw->affectedRows() ) { + $changed = true; + } + if( !$this->mId ) { + # Anon users have a separate memcache space for newtalk + # since they don't store their own info. Trim... + $wgMemc->delete( "$wgDBname:newtalk:ip:" . $this->getName() ); + } + } + } else { + if ($this->getID() != 0) { + $field = 'user_id'; + $value = $this->getID(); + $key = false; + } else { + $field = 'user_ip'; + $value = $this->getName(); + $key = "$wgDBname:newtalk:ip:$value"; + } + + $dbr =& wfGetDB( DB_SLAVE ); + $dbw =& wfGetDB( DB_MASTER ); + + $res = $dbr->selectField('user_newtalk', $field, + array($field => $value), $fname); + + $changed = true; + if ($res !== false && $this->mNewtalk == 0) { + $dbw->delete('user_newtalk', array($field => $value), $fname); + if ( $key ) { + $wgMemc->set( $key, 0 ); + } + } else if ($res === false && $this->mNewtalk == 1) { + $dbw->insert('user_newtalk', array($field => $value), $fname); + if ( $key ) { + $wgMemc->set( $key, 1 ); + } + } else { + $changed = false; + } + } + + # Update user_touched, so that newtalk notifications in the client cache are invalidated + if ( $changed && $this->getID() ) { + $dbw->update('user', + /*SET*/ array( 'user_touched' => $this->mTouched ), + /*WHERE*/ array( 'user_id' => $this->getID() ), + $fname); + $wgMemc->set( "$wgDBname:user:id:{$this->mId}", $this, 86400 ); } } - /** * Checks if a user with the given name exists, returns the ID */ @@ -975,7 +1414,7 @@ class User { $fname = 'User::idForName'; $gotid = 0; - $s = trim( $this->mName ); + $s = trim( $this->getName() ); if ( 0 == strcmp( '', $s ) ) return 0; $dbr =& wfGetDB( DB_SLAVE ); @@ -1000,32 +1439,16 @@ class User { 'user_password' => $this->mPassword, 'user_newpassword' => $this->mNewpassword, 'user_email' => $this->mEmail, - 'user_emailauthenticationtimestamp' => $this->mEmailAuthenticationtimestamp, + 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 'user_real_name' => $this->mRealName, 'user_options' => $this->encodeOptions(), 'user_token' => $this->mToken ), $fname ); $this->mId = $dbw->insertId(); - $dbw->insert( 'user_rights', - array( - 'ur_user' => $this->mId, - 'ur_rights' => implode( ',', $this->mRights ) - ), $fname - ); - - foreach ($this->mGroups as $group) { - $dbw->insert( 'user_groups', - array( - 'ug_user' => $this->mId, - 'ug_group' => $group - ), $fname - ); - } } function spreadBlock() { - global $wgIP; # If the (non-anonymous) user is blocked, this function will block any IP address # that they successfully log on from. $fname = 'User::spreadBlock'; @@ -1041,16 +1464,23 @@ class User { } # Check if this IP address is already blocked - $ipblock = Block::newFromDB( $wgIP ); + $ipblock = Block::newFromDB( wfGetIP() ); if ( $ipblock->isValid() ) { + # If the user is already blocked. Then check if the autoblock would + # excede the user block. If it would excede, then do nothing, else + # prolong block time + if ($userblock->mExpiry && + ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) { + return; + } # Just update the timestamp $ipblock->updateTimestamp(); return; } # Make a new block object with the desired properties - wfDebug( "Autoblocking {$this->mName}@{$wgIP}\n" ); - $ipblock->mAddress = $wgIP; + wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" ); + $ipblock->mAddress = wfGetIP(); $ipblock->mUser = 0; $ipblock->mBy = $userblock->mBy; $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason ); @@ -1079,14 +1509,11 @@ class User { // it will always be 0 when this function is called by parsercache. $confstr = $this->getOption( 'math' ); - $confstr .= '!' . $this->getOption( 'highlightbroken' ); $confstr .= '!' . $this->getOption( 'stubthreshold' ); - $confstr .= '!' . $this->getOption( 'editsection' ); - $confstr .= '!' . $this->getOption( 'editsectiononrightclick' ); - $confstr .= '!' . $this->getOption( 'showtoc' ); $confstr .= '!' . $this->getOption( 'date' ); $confstr .= '!' . $this->getOption( 'numberheadings' ); $confstr .= '!' . $this->getOption( 'language' ); + $confstr .= '!' . $this->getOption( 'thumbsize' ); // add in language specific options, if any $extra = $wgContLang->getExtraHashOptions(); $confstr .= $extra; @@ -1096,15 +1523,7 @@ class User { } function isAllowedToCreateAccount() { - global $wgWhitelistAccount; - $allowed = false; - - if (!$wgWhitelistAccount) { return 1; }; // default behaviour - foreach ($wgWhitelistAccount as $right => $ok) { - $userHasRight = (!strcmp($right, 'user') || in_array($right, $this->getRights())); - $allowed |= ($ok && $userHasRight); - } - return $allowed; + return $this->isAllowed( 'createaccount' ) && !$this->isBlocked(); } /** @@ -1115,8 +1534,25 @@ class User { return wfSetVar( $this->mDataLoaded, $loaded ); } + /** + * Get this user's personal page title. + * + * @return Title + * @access public + */ function getUserPage() { - return Title::makeTitle( NS_USER, $this->mName ); + return Title::makeTitle( NS_USER, $this->getName() ); + } + + /** + * Get this user's talk page title. + * + * @return Title + * @access public + */ + function getTalkPage() { + $title = $this->getUserPage(); + return $title->getTalkPage(); } /** @@ -1124,7 +1560,7 @@ class User { */ function getMaxID() { $dbr =& wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'max(user_id)', false ); + return $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' ); } /** @@ -1134,7 +1570,7 @@ class User { * @return bool True if it is a newbie. */ function isNewbie() { - return $this->mId > User::getMaxID() * 0.99 && !$this->isSysop() && !$this->isBot() || $this->getID() == 0; + return $this->isAnon() || $this->mId > User::getMaxID() * 0.99 && !$this->isAllowed( 'delete' ) && !$this->isBot(); } /** @@ -1143,9 +1579,18 @@ class User { * @return bool True if the given password is correct otherwise False. */ function checkPassword( $password ) { - global $wgAuth; + global $wgAuth, $wgMinimalPasswordLength; $this->loadFromDatabase(); - + + // Even though we stop people from creating passwords that + // are shorter than this, doesn't mean people wont be able + // to. Certain authentication plugins do NOT want to save + // domain passwords in a mysql database, so we should + // check this (incase $wgAuth->strict() is false). + if( strlen( $password ) < $wgMinimalPasswordLength ) { + return false; + } + if( $wgAuth->authenticate( $this->getName(), $password ) ) { return true; } elseif( $wgAuth->strict() ) { @@ -1156,20 +1601,261 @@ 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 ! - $this->saveSettings(); return true; } elseif ( function_exists( 'iconv' ) ) { # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted # Check for this with iconv -/* $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) ); + $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) ); if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) { return true; - }*/ + } } return false; } + + /** + * Initialize (if necessary) and return a session token value + * which can be used in edit forms to show that the user's + * login credentials aren't being hijacked with a foreign form + * submission. + * + * @param mixed $salt - Optional function-specific data for hash. + * Use a string or an array of strings. + * @return string + * @access public + */ + function editToken( $salt = '' ) { + if( !isset( $_SESSION['wsEditToken'] ) ) { + $token = $this->generateToken(); + $_SESSION['wsEditToken'] = $token; + } else { + $token = $_SESSION['wsEditToken']; + } + if( is_array( $salt ) ) { + $salt = implode( '|', $salt ); + } + 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 + * user's own login session, not a form submission from a third-party + * site. + * + * @param string $val - the input value to compare + * @param string $salt - Optional function-specific data for hash + * @return bool + * @access public + */ + function matchEditToken( $val, $salt = '' ) { + global $wgMemc; + +/* + if ( !isset( $_SESSION['wsEditToken'] ) ) { + $logfile = '/home/wikipedia/logs/session_debug/session.log'; + $mckey = memsess_key( session_id() ); + $uname = @posix_uname(); + $msg = "wsEditToken not set!\n" . + 'apache server=' . $uname['nodename'] . "\n" . + 'session_id = ' . session_id() . "\n" . + '$_SESSION=' . var_export( $_SESSION, true ) . "\n" . + '$_COOKIE=' . var_export( $_COOKIE, true ) . "\n" . + "mc get($mckey) = " . var_export( $wgMemc->get( $mckey ), true ) . "\n\n\n"; + + @error_log( $msg, 3, $logfile ); + } +*/ + 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 $wgContLang; + $url = $this->confirmationTokenUrl( $expiration ); + return $this->sendMail( wfMsg( 'confirmemail_subject' ), + wfMsg( 'confirmemail_body', + wfGetIP(), + $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; + } + + /** + * @param array $groups list of groups + * @return array list of permission key names for given groups combined + * @static + */ + function getGroupPermissions( $groups ) { + global $wgGroupPermissions; + $rights = array(); + foreach( $groups as $group ) { + if( isset( $wgGroupPermissions[$group] ) ) { + $rights = array_merge( $rights, + array_keys( array_filter( $wgGroupPermissions[$group] ) ) ); + } + } + return $rights; + } + + /** + * @param string $group key name + * @return string localized descriptive name, if provided + * @static + */ + function getGroupName( $group ) { + $key = "group-$group-name"; + $name = wfMsg( $key ); + if( $name == '' || $name == "<$key>" ) { + return $group; + } else { + return $name; + } + } + + /** + * Return the set of defined explicit groups. + * The * and 'user' groups are not included. + * @return array + * @static + */ + function getAllGroups() { + global $wgGroupPermissions; + return array_diff( + array_keys( $wgGroupPermissions ), + array( '*', 'user' ) ); + } + } ?>