X-Git-Url: https://git.cyclocoop.org/?a=blobdiff_plain;f=includes%2FUser.php;h=10ccd4795dc602fbec024f1b0d8368806119b570;hb=9463b17980147e7e99a6488d6111cb9d1a52f951;hp=44dada4ef6ce8352e1cbbc5fc503721cdb412541;hpb=b342a81da0454cc39a2e404f7208c8cbb2eed3c6;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/User.php b/includes/User.php index 44dada4ef6..10ccd4795d 100644 --- a/includes/User.php +++ b/includes/User.php @@ -2,30 +2,34 @@ /** * See user.txt * - * @package MediaWiki */ # Number of characters in user_token field define( 'USER_TOKEN_LENGTH', 32 ); # Serialized record version -define( 'MW_USER_VERSION', 4 ); +define( 'MW_USER_VERSION', 5 ); # Some punctuation to prevent editing from broken text-mangling proxies. -# FIXME: this is embedded unescaped into HTML attributes in various -# places, so we can't safely include ' or " even though we really should. -define( 'EDIT_TOKEN_SUFFIX', '\\' ); +define( 'EDIT_TOKEN_SUFFIX', '+\\' ); /** * Thrown by User::setPassword() on error + * @addtogroup Exception */ class PasswordError extends MWException { // NOP } /** - * - * @package MediaWiki + * The User object encapsulates all of the user-specific settings (user_id, + * name, rights, password, email address, options, last login time). Client + * classes use the getXXX() functions to access these fields. These functions + * do all the work of determining whether the user is logged in, + * whether the requested option can be satisfied from cookies or + * whether a database query is needed. Most of the settings needed + * for rendering normal pages are set in the cookie to minimize use + * of the database. */ class User { @@ -71,6 +75,8 @@ class User { 'watchlisthidebots', 'watchlisthideminor', 'ccmeonemails', + 'diffonly', + 'showhiddencats', ); /** @@ -94,7 +100,7 @@ class User { 'mEmailToken', 'mEmailTokenExpires', 'mRegistration', - + 'mEditCount', # user_group table 'mGroups', ); @@ -188,7 +194,6 @@ class User { # Try cache $key = wfMemcKey( 'user', 'id', $this->mId ); $data = $wgMemc->get( $key ); - if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) { # Object is expired, load from DB $data = false; @@ -202,13 +207,7 @@ class User { return false; } - # Save to cache - $data = array(); - foreach ( self::$mCacheVars as $name ) { - $data[$name] = $this->$name; - } - $data['mVersion'] = MW_USER_VERSION; - $wgMemc->set( $key, $data ); + $this->saveToCache(); } else { wfDebug( "Got user {$this->mId} from cache\n" ); # Restore from cache @@ -219,6 +218,25 @@ class User { return true; } + /** + * Save user data to the shared cache + */ + function saveToCache() { + $this->load(); + if ( $this->isAnon() ) { + // Anonymous users are uncached + return; + } + $data = array(); + foreach ( self::$mCacheVars as $name ) { + $data[$name] = $this->$name; + } + $data['mVersion'] = MW_USER_VERSION; + $key = wfMemcKey( 'user', 'id', $this->mId ); + global $wgMemc; + $wgMemc->set( $key, $data ); + } + /** * Static factory method for creation from username. * @@ -270,7 +288,7 @@ class User { * @static */ static function newFromConfirmationCode( $code ) { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $id = $dbr->selectField( 'user', 'user_id', array( 'user_email_token' => md5( $code ), 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ), @@ -302,19 +320,19 @@ class User { * @static */ static function whoIs( $id ) { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' ); } /** - * Get real username given an id. - * @param integer $id Database user id - * @return string Realname of a user - * @static + * Get the real name of a user given their identifier + * + * @param int $id Database user id + * @return string Real name of a user */ static function whoIsReal( $id ) { - $dbr =& wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' ); + $dbr = wfGetDB( DB_SLAVE ); + return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ ); } /** @@ -329,7 +347,7 @@ class User { # Illegal name return null; } - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ ); if ( $s === false ) { @@ -352,14 +370,12 @@ class User { * 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 */ static function isIP( $name ) { - return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name); + return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || User::isIPv6($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]))\. @@ -368,6 +384,27 @@ class User { $/x", $name);*/ } + /** + * Check if $name is an IPv6 IP. + */ + static function isIPv6($name) { + /* + * if it has any non-valid characters, it can't be a valid IPv6 + * address. + */ + if (preg_match("/[^:a-fA-F0-9]/", $name)) + return false; + + $parts = explode(":", $name); + if (count($parts) < 3) + return false; + foreach ($parts as $part) { + if (!preg_match("/^[0-9a-fA-F]{0,4}$/", $part)) + return false; + } + return true; + } + /** * Is the input a valid username? * @@ -387,16 +424,22 @@ class User { || User::isIP( $name ) || strpos( $name, '/' ) !== false || strlen( $name ) > $wgMaxNameChars - || $name != $wgContLang->ucfirst( $name ) ) + || $name != $wgContLang->ucfirst( $name ) ) { + wfDebugLog( 'username', __METHOD__ . + ": '$name' invalid due to empty, IP, slash, length, or lowercase" ); 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() ) ) + || strcmp( $name, $parsed->getPrefixedText() ) ) { + wfDebugLog( 'username', __METHOD__ . + ": '$name' invalid due to ambiguous prefixes" ); return false; + } // Check an additional blacklist of troublemaker characters. // Should these be merged into the title char list? @@ -409,6 +452,8 @@ class User { '\x{e000}-\x{f8ff}' . # private use ']/u'; if( preg_match( $unicodeBlacklist, $name ) ) { + wfDebugLog( 'username', __METHOD__ . + ": '$name' invalid due to blacklisted characters" ); return false; } @@ -429,7 +474,7 @@ class User { static function isUsableName( $name ) { global $wgReservedUsernames; return - // Must be a usable username, obviously ;) + // Must be a valid username, obviously ;) self::isValidUserName( $name ) && // Certain names may be reserved for batch processes. @@ -458,34 +503,44 @@ class User { } /** - * Is the input a valid password? + * Is the input a valid password for this user? * - * @param string $password + * @param string $password Desired password * @return bool - * @static */ - static function isValidPassword( $password ) { - global $wgMinimalPasswordLength; - return strlen( $password ) >= $wgMinimalPasswordLength; + function isValidPassword( $password ) { + global $wgMinimalPasswordLength, $wgContLang; + + $result = null; + if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) + return $result; + if( $result === false ) + return false; + + // Password needs to be long enough, and can't be the same as the username + return strlen( $password ) >= $wgMinimalPasswordLength + && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName ); } /** - * Does the string match roughly an email address ? + * Does a string look like 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 + * @todo Check for RFC 2822 compilance (bug 959) * * @param string $addr email address - * @static * @return bool */ - static function isValidEmailAddr ( $addr ) { - return ( trim( $addr ) != '' ) && - (false !== strpos( $addr, '@' ) ); + public static function isValidEmailAddr( $addr ) { + $result = null; + if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) { + return $result; + } + + return strpos( $addr, '@' ) !== false; } /** @@ -503,6 +558,12 @@ class User { global $wgContLang; $name = $wgContLang->ucfirst( $name ); + # Reject names containing '#'; these will be cleaned up + # with title normalisation, but then it's too late to + # check elsewhere + if( strpos( $name, '#' ) !== false ) + return false; + # Clean up name according to title rules $t = Title::newFromText( $name ); if( is_null( $t ) ) { @@ -541,22 +602,45 @@ class User { /** * Count the number of edits of a user * + * It should not be static and some day should be merged as proper member function / deprecated -- domas + * * @param int $uid The user ID to check * @return int * @static */ static function edits( $uid ) { - $dbr =& wfGetDB( DB_SLAVE ); - return $dbr->selectField( - 'revision', 'count(*)', - array( 'rev_user' => $uid ), + wfProfileIn( __METHOD__ ); + $dbr = wfGetDB( DB_SLAVE ); + // check if the user_editcount field has been initialized + $field = $dbr->selectField( + 'user', 'user_editcount', + array( 'user_id' => $uid ), __METHOD__ ); + + if( $field === null ) { // it has not been initialized. do so. + $dbw = wfGetDB( DB_MASTER ); + $count = $dbr->selectField( + 'revision', 'count(*)', + array( 'rev_user' => $uid ), + __METHOD__ + ); + $dbw->update( + 'user', + array( 'user_editcount' => $count ), + array( 'user_id' => $uid ), + __METHOD__ + ); + } else { + $count = $field; + } + wfProfileOut( __METHOD__ ); + return $count; } /** * Return a random password. Sourced from mt_rand, so it's not particularly secure. - * @todo: hash random numbers to improve security, like generateToken() + * @todo hash random numbers to improve security, like generateToken() * * @return string * @static @@ -622,10 +706,8 @@ class User { * Load user data from the session or login cookie. If there are no valid * credentials, initialises the user as an anon. * @return true if the user is logged in, false otherwise - * - * @private */ - function loadFromSession() { + private function loadFromSession() { global $wgMemc, $wgCookiePrefix; if ( isset( $_SESSION['wsUserID'] ) ) { @@ -672,6 +754,7 @@ class User { } if ( ( $sName == $this->mName ) && $passwordCorrect ) { + $_SESSION['wsToken'] = $this->mToken; wfDebug( "Logged in from $from\n" ); return true; } else { @@ -699,7 +782,7 @@ class User { return false; } - $dbr =& wfGetDB( DB_MASTER ); + $dbr = wfGetDB( DB_MASTER ); $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ ); if ( $s !== false ) { @@ -717,6 +800,8 @@ class User { $this->mEmailToken = $s->user_email_token; $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires ); $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration ); + $this->mEditCount = $s->user_editcount; + $this->getEditCount(); // revalidation for nulls # Load group data $res = $dbr->select( 'user_groups', @@ -760,12 +845,12 @@ class User { /** * Combine the language default options with any site-specific options * and add the default language variants. - * + * Not really private cause it's called by Language class * @return array * @static * @private */ - function getDefaultOptions() { + static function getDefaultOptions() { global $wgNamespacesToBeSearchedDefault; /** * Site defaults will override the global/language defaults @@ -833,7 +918,8 @@ class User { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__.": checking...\n" ); - $this->mBlockedby = 0; + $this->mBlockedby = 0; + $this->mHideName = 0; $ip = wfGetIP(); if ($this->isAllowed( 'ipblock-exempt' ) ) { @@ -848,6 +934,7 @@ class User { wfDebug( __METHOD__.": Found block.\n" ); $this->mBlockedby = $this->mBlock->mBy; $this->mBlockreason = $this->mBlock->mReason; + $this->mHideName = $this->mBlock->mHideName; if ( $this->isLoggedIn() ) { $this->spreadBlock(); } @@ -916,6 +1003,16 @@ class User { return $found; } + /** + * Is this user subject to rate limiting? + * + * @return bool + */ + public function isPingLimitable() { + global $wgRateLimitsExcludedGroups; + return array_intersect($this->getEffectiveGroups(), $wgRateLimitsExcludedGroups) == array(); + } + /** * Primitive rate limits: enforce maximum actions per time period * to put a brake on flooding. @@ -927,24 +1024,22 @@ class User { * @public */ function pingLimiter( $action='edit' ) { - + # Call the 'PingLimiter' hook $result = false; if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) { return $result; } - - global $wgRateLimits, $wgRateLimitsExcludedGroups; + + global $wgRateLimits; if( !isset( $wgRateLimits[$action] ) ) { return false; } - + # Some groups shouldn't trigger the ping limiter, ever - foreach( $this->getGroups() as $group ) { - if( array_search( $group, $wgRateLimitsExcludedGroups ) !== false ) - return false; - } - + if( !$this->isPingLimitable() ) + return false; + global $wgMemc, $wgRateLimitLog; wfProfileIn( __METHOD__ ); @@ -1018,14 +1113,13 @@ class User { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__.": enter\n" ); - if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() && - $title->getNamespace() == NS_USER_TALK ) - { + wfDebug( __METHOD__.": asking isBlocked()\n" ); + $blocked = $this->isBlocked( $bFromSlave ); + # If a user's name is suppressed, they cannot make edits anywhere + if ( !$this->mHideName && $wgBlockAllowsUTEdit && $title->getText() === $this->getName() && + $title->getNamespace() == NS_USER_TALK ) { $blocked = false; wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" ); - } else { - wfDebug( __METHOD__.": asking isBlocked()\n" ); - $blocked = $this->isBlocked( $bFromSlave ); } wfProfileOut( __METHOD__ ); return $blocked; @@ -1052,9 +1146,16 @@ class User { /** * Get the user ID. Returns 0 if the user is anonymous or nonexistent. */ - function getID() { - $this->load(); - return $this->mId; + function getID() { + if( $this->mId === null and $this->mName !== null + and User::isIP( $this->mName ) ) { + // Special case, we know the user is anonymous + return 0; + } elseif( $this->mId === null ) { + // Don't load if this was initialized from an ID + $this->load(); + } + return $this->mId; } /** @@ -1076,7 +1177,8 @@ class User { } else { $this->load(); if ( $this->mName === false ) { - $this->mName = wfGetIP(); + # Clean up IPs + $this->mName = IP::sanitizeIP( wfGetIP() ); } return $this->mName; } @@ -1121,11 +1223,13 @@ class User { global $wgMemc; $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); $newtalk = $wgMemc->get( $key ); - if( is_integer( $newtalk ) ) { + if( strval( $newtalk ) !== '' ) { $this->mNewtalk = (bool)$newtalk; } else { - $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() ); - $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 ); + // Since we are caching this, make sure it is up to date by getting it + // from the master + $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true ); + $wgMemc->set( $key, (int)$this->mNewtalk, 1800 ); } } else { $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId ); @@ -1152,18 +1256,22 @@ class User { /** - * Perform a user_newtalk check on current slaves; if the memcached data - * is funky we don't want newtalk state to get stuck on save, as that's - * damn annoying. - * + * Perform a user_newtalk check, uncached. + * Use getNewtalk for a cached check. + * * @param string $field * @param mixed $id + * @param bool $fromMaster True to fetch from the master, false for a slave * @return bool * @private */ - function checkNewtalk( $field, $id ) { - $dbr =& wfGetDB( DB_SLAVE ); - $ok = $dbr->selectField( 'user_newtalk', $field, + function checkNewtalk( $field, $id, $fromMaster = false ) { + if ( $fromMaster ) { + $db = wfGetDB( DB_MASTER ); + } else { + $db = wfGetDB( DB_SLAVE ); + } + $ok = $db->selectField( 'user_newtalk', $field, array( $field => $id ), __METHOD__ ); return $ok !== false; } @@ -1175,17 +1283,18 @@ class User { * @private */ function updateNewtalk( $field, $id ) { - if( $this->checkNewtalk( $field, $id ) ) { - wfDebug( __METHOD__." already set ($field, $id), ignoring\n" ); - return false; - } - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->insert( 'user_newtalk', array( $field => $id ), __METHOD__, 'IGNORE' ); - wfDebug( __METHOD__.": set on ($field, $id)\n" ); - return true; + if ( $dbw->affectedRows() ) { + wfDebug( __METHOD__.": set on ($field, $id)\n" ); + return true; + } else { + wfDebug( __METHOD__." already set ($field, $id)\n" ); + return false; + } } /** @@ -1195,16 +1304,17 @@ class User { * @private */ function deleteNewtalk( $field, $id ) { - if( !$this->checkNewtalk( $field, $id ) ) { - wfDebug( __METHOD__.": already gone ($field, $id), ignoring\n" ); - return false; - } - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'user_newtalk', array( $field => $id ), __METHOD__ ); - wfDebug( __METHOD__.": killed on ($field, $id)\n" ); - return true; + if ( $dbw->affectedRows() ) { + wfDebug( __METHOD__.": killed on ($field, $id)\n" ); + return true; + } else { + wfDebug( __METHOD__.": already gone ($field, $id)\n" ); + return false; + } } /** @@ -1226,6 +1336,7 @@ class User { $field = 'user_id'; $id = $this->getId(); } + global $wgMemc; if( $val ) { $changed = $this->updateNewtalk( $field, $id ); @@ -1233,20 +1344,13 @@ class User { $changed = $this->deleteNewtalk( $field, $id ); } - if( $changed ) { - if( $this->isAnon() ) { - // Anons have a separate memcached space, since - // user records aren't kept for them. - global $wgMemc; - $key = wfMemcKey( 'newtalk', 'ip', $val ); - $wgMemc->set( $key, $val ? 1 : 0 ); - } else { - if( $val ) { - // Make sure the user page is watched, so a notification - // will be sent out if enabled. - $this->addWatch( $this->getTalkPage() ); - } - } + if( $this->isAnon() ) { + // Anons have a separate memcached space, since + // user records aren't kept for them. + $key = wfMemcKey( 'newtalk', 'ip', $id ); + $wgMemc->set( $key, $val ? 1 : 0, 1800 ); + } + if ( $changed ) { $this->invalidateCache(); } } @@ -1284,7 +1388,7 @@ class User { if( $this->mId ) { $this->mTouched = self::newTouchedTimestamp(); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'user', array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ), array( 'user_id' => $this->mId ), @@ -1301,7 +1405,8 @@ class User { /** * Encrypt a password. - * It can eventuall salt a password @see User::addSalt() + * It can eventually salt a password. + * @see User::addSalt() * @param string $p clear Password. * @return string Encrypted password. */ @@ -1338,11 +1443,23 @@ class User { $wgMinimalPasswordLength ) ); } } - + if( !$wgAuth->setPassword( $this, $str ) ) { throw new PasswordError( wfMsg( 'externaldberror' ) ); } + $this->setInternalPassword( $str ); + + return true; + } + + /** + * Set the password and reset the random token no matter + * what. + * + * @param string $str + */ + function setInternalPassword( $str ) { $this->load(); $this->setToken(); @@ -1354,10 +1471,7 @@ class User { } $this->mNewpassword = ''; $this->mNewpassTime = null; - - return true; } - /** * Set the random token (used for persistent authentication) * Called from loadDefaults() among other places. @@ -1517,6 +1631,7 @@ class User { function getRights() { if ( is_null( $this->mRights ) ) { $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); + wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) ); } return $this->mRights; } @@ -1533,8 +1648,8 @@ class User { /** * Get the list of implicit group memberships this user has. - * This includes all explicit groups, plus 'user' if logged in - * and '*' for all accounts. + * This includes all explicit groups, plus 'user' if logged in, + * '*' for all accounts and autopromoted groups * @param boolean $recache Don't use the cache * @return array of strings */ @@ -1545,43 +1660,50 @@ class User { $this->mEffectiveGroups[] = '*'; if( $this->mId ) { $this->mEffectiveGroups[] = 'user'; - - global $wgAutoConfirmAge; - $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration ); - if( $accountAge >= $wgAutoConfirmAge ) { - $this->mEffectiveGroups[] = 'autoconfirmed'; - } - - # Implicit group for users whose email addresses are confirmed - global $wgEmailAuthentication; - if( self::isValidEmailAddr( $this->mEmail ) ) { - if( $wgEmailAuthentication ) { - if( $this->mEmailAuthenticated ) - $this->mEffectiveGroups[] = 'emailconfirmed'; - } else { - $this->mEffectiveGroups[] = 'emailconfirmed'; - } - } + + $this->mEffectiveGroups = array_unique( array_merge( + $this->mEffectiveGroups, + Autopromote::getAutopromoteGroups( $this ) + ) ); + + # Hook for additional groups + wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) ); } } return $this->mEffectiveGroups; } - + + /* Return the edit count for the user. This is where User::edits should have been */ + function getEditCount() { + if ($this->mId) { + if ( !isset( $this->mEditCount ) ) { + /* Populate the count, if it has not been populated yet */ + $this->mEditCount = User::edits($this->mId); + } + return $this->mEditCount; + } else { + /* nil */ + return null; + } + } + /** * Add the user to the given group. * This takes immediate effect. - * @string $group + * @param string $group */ function addGroup( $group ) { $this->load(); - $dbw =& wfGetDB( DB_MASTER ); - $dbw->insert( 'user_groups', - array( - 'ug_user' => $this->getID(), - 'ug_group' => $group, - ), - 'User::addGroup', - array( 'IGNORE' ) ); + $dbw = wfGetDB( DB_MASTER ); + if( $this->getId() ) { + $dbw->insert( 'user_groups', + array( + 'ug_user' => $this->getID(), + 'ug_group' => $group, + ), + 'User::addGroup', + array( 'IGNORE' ) ); + } $this->mGroups[] = $group; $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) ); @@ -1592,11 +1714,11 @@ class User { /** * Remove the user from the given group. * This takes immediate effect. - * @string $group + * @param string $group */ function removeGroup( $group ) { $this->load(); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'user_groups', array( 'ug_user' => $this->getID(), @@ -1618,7 +1740,7 @@ class User { * @return bool */ function isLoggedIn() { - return( $this->getID() != 0 ); + return $this->getID() != 0; } /** @@ -1748,7 +1870,7 @@ class User { // 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 ); + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'watchlist', array( /* SET */ 'wl_notificationtimestamp' => NULL @@ -1779,13 +1901,13 @@ class User { } if( $currentUser != 0 ) { - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'watchlist', array( /* SET */ 'wl_notificationtimestamp' => NULL ), array( /* WHERE */ 'wl_user' => $currentUser - ), 'UserMailer::clearAll' + ), __METHOD__ ); # we also need to clear here the "you have new message" notification for the own user_talk page @@ -1845,10 +1967,21 @@ class User { } /** - * Logout user - * Clears the cookies and session, resets the instance cache + * Logout user. */ function logout() { + global $wgUser; + if( wfRunHooks( 'UserLogout', array(&$this) ) ) { + $this->doLogout(); + wfRunHooks( 'UserLogoutComplete', array(&$wgUser) ); + } + } + + /** + * Really logout user + * Clears the cookies and session, resets the instance cache + */ + function doLogout() { global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix; $this->clearInstanceCache( 'defaults' ); @@ -1863,7 +1996,7 @@ class User { /** * Save object settings into database - * @fixme Only rarely do all these fields need to be set! + * @todo Only rarely do all these fields need to be set! */ function saveSettings() { $this->load(); @@ -1872,7 +2005,7 @@ class User { $this->mTouched = self::newTouchedTimestamp(); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'user', array( /* SET */ 'user_name' => $this->mName, @@ -1894,13 +2027,13 @@ class User { /** - * Checks if a user with the given name exists, returns the ID + * Checks if a user with the given name exists, returns the ID. */ function idForName() { $s = trim( $this->getName() ); - if ( 0 == strcmp( '', $s ) ) return 0; + if ( $s === '' ) return 0; - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ ); if ( $id === false ) { $id = 0; @@ -1931,7 +2064,7 @@ class User { $user->mOptions = $params['options'] + $user->mOptions; unset( $params['options'] ); } - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); $fields = array( 'user_id' => $seqVal, @@ -1964,7 +2097,7 @@ class User { */ function addToDatabase() { $this->load(); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); $dbw->insert( 'user', array( @@ -2046,6 +2179,8 @@ class User { // extra options or other effects on the parser cache. wfRunHooks( 'PageRenderingHash', array( &$confstr ) ); + // Make it a valid memcached key fragment + $confstr = str_replace( ' ', '_', $confstr ); $this->mHash = $confstr; return $confstr; } @@ -2055,6 +2190,17 @@ class User { return $this->mBlock && $this->mBlock->mCreateAccount; } + /** + * Determine if the user is blocked from using Special:Emailuser. + * + * @public + * @return boolean + */ + function isBlockedFromEmailuser() { + $this->getBlockedStatus(); + return $this->mBlock && $this->mBlock->mBlockEmail; + } + function isAllowedToCreateAccount() { return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); } @@ -2094,7 +2240,7 @@ class User { if ( isset( $res ) ) return $res; else { - $dbr =& wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' ); } } @@ -2131,6 +2277,9 @@ class User { } elseif( $wgAuth->strict() ) { /* Auth plugin doesn't allow local authentication */ return false; + } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) { + /* Auth plugin doesn't allow local authentication for this user name */ + return false; } $ep = $this->encryptPassword( $password ); if ( 0 == strcmp( $ep, $this->mPassword ) ) { @@ -2168,16 +2317,20 @@ class User { * @public */ function editToken( $salt = '' ) { - if( !isset( $_SESSION['wsEditToken'] ) ) { - $token = $this->generateToken(); - $_SESSION['wsEditToken'] = $token; + if ( $this->isAnon() ) { + return EDIT_TOKEN_SUFFIX; } else { - $token = $_SESSION['wsEditToken']; - } - if( is_array( $salt ) ) { - $salt = implode( '|', $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 ) . EDIT_TOKEN_SUFFIX; } - return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX; } /** @@ -2202,7 +2355,6 @@ class User { * @public */ function matchEditToken( $val, $salt = '' ) { - global $wgMemc; $sessionToken = $this->editToken( $salt ); if ( $val != $sessionToken ) { wfDebug( "User::matchEditToken: broken session data\n" ); @@ -2210,6 +2362,14 @@ class User { return $val == $sessionToken; } + /** + * Check whether the edit token is fine except for the suffix + */ + function matchEditTokenNoSuffix( $val, $salt = '' ) { + $sessionToken = $this->editToken( $salt ); + return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 ); + } + /** * Generate a new e-mail confirmation token and send a confirmation * mail to the user's given address. @@ -2234,25 +2394,18 @@ class User { * * @param string $subject * @param string $body - * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise. + * @param string $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 ) { + function sendMail( $subject, $body, $from = null, $replyto = null ) { if( is_null( $from ) ) { global $wgPasswordSender; $from = $wgPasswordSender; } - require_once( 'UserMailer.php' ); $to = new MailAddress( $this ); $sender = new MailAddress( $from ); - $error = userMailer( $to, $sender, $subject, $body ); - - if( $error == '' ) { - return true; - } else { - return new WikiError( $error ); - } + return UserMailer::send( $to, $sender, $subject, $body, $replyto ); } /** @@ -2270,7 +2423,7 @@ class User { $token = $this->generateToken( $this->mId . $this->mEmail . $expires ); $hash = md5( $token ); - $dbw =& wfGetDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'user', array( 'user_email_token' => $hash, 'user_email_token_expires' => $dbw->timestamp( $expires ) ), @@ -2309,7 +2462,9 @@ class User { * @return bool */ function canSendEmail() { - return $this->isEmailConfirmed(); + $canSend = $this->isEmailConfirmed(); + wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) ); + return $canSend; } /** @@ -2318,7 +2473,7 @@ class User { * @return bool */ function canReceiveEmail() { - return $this->canSendEmail() && !$this->getOption( 'disablemail' ); + return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' ); } /** @@ -2359,6 +2514,18 @@ class User { $this->mEmailToken && $this->mEmailTokenExpires > wfTimestamp(); } + + /** + * Get the timestamp of account creation, or false for + * non-existent/anonymous user accounts + * + * @return mixed + */ + public function getRegistration() { + return $this->mId > 0 + ? $this->mRegistration + : false; + } /** * @param array $groups list of groups @@ -2383,13 +2550,13 @@ class User { * @static */ static function getGroupName( $group ) { + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $key = "group-$group"; $name = wfMsg( $key ); - if( $name == '' || wfEmptyMsg( $key, $name ) ) { - return $group; - } else { - return $name; - } + return $name == '' || wfEmptyMsg( $key, $name ) + ? $group + : $name; } /** @@ -2398,13 +2565,13 @@ class User { * @static */ static function getGroupMember( $group ) { + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $key = "group-$group-member"; $name = wfMsg( $key ); - if( $name == '' || wfEmptyMsg( $key, $name ) ) { - return $group; - } else { - return $name; - } + return $name == '' || wfEmptyMsg( $key, $name ) + ? $group + : $name; } /** @@ -2419,7 +2586,20 @@ class User { global $wgGroupPermissions; return array_diff( array_keys( $wgGroupPermissions ), - array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) ); + self::getImplicitGroups() + ); + } + + /** + * Get a list of implicit groups + * + * @return array + */ + public static function getImplicitGroups() { + global $wgImplicitGroups; + $groups = $wgImplicitGroups; + wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead + return $groups; } /** @@ -2429,6 +2609,8 @@ class User { * @return mixed */ static function getGroupPage( $group ) { + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $page = wfMsgForContent( 'grouppage-' . $group ); if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) { $title = Title::newFromText( $page ); @@ -2453,7 +2635,7 @@ class User { if( $title ) { global $wgUser; $sk = $wgUser->getSkin(); - return $sk->makeLinkObj( $title, $text ); + return $sk->makeLinkObj( $title, htmlspecialchars( $text ) ); } else { return $text; } @@ -2519,7 +2701,10 @@ class User { __METHOD__ ); } } + // edit count in user cache too + $this->invalidateCache(); } } -?> + +