diff options
Diffstat (limited to 'includes/User.php')
-rw-r--r-- | includes/User.php | 284 |
1 files changed, 180 insertions, 104 deletions
diff --git a/includes/User.php b/includes/User.php index 4ecd49de..51b0b2ec 100644 --- a/includes/User.php +++ b/includes/User.php @@ -11,9 +11,7 @@ define( 'USER_TOKEN_LENGTH', 32 ); 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 @@ -36,8 +34,8 @@ class PasswordError extends MWException { class User { /** - * A list of default user toggles, i.e. boolean user preferences that are - * displayed by Special:Preferences as checkboxes. This list can be + * A list of default user toggles, i.e. boolean user preferences that are + * displayed by Special:Preferences as checkboxes. This list can be * extended via the UserToggles hook or $wgContLang->getExtraUserToggles(). */ static public $mToggles = array( @@ -82,7 +80,7 @@ class User { /** * List of member variables which are saved to the shared cache (memcached). - * Any operation which changes the corresponding database fields must + * Any operation which changes the corresponding database fields must * call a cache-clearing function. */ static $mCacheVars = array( @@ -109,8 +107,8 @@ class User { /** * The cache variable declarations */ - var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, - $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, + var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, + $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups; /** @@ -135,7 +133,7 @@ class User { var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights, $mBlockreason, $mBlock, $mEffectiveGroups; - /** + /** * Lightweight constructor for anonymous user * Use the User::newFrom* factory functions for other kinds of users */ @@ -190,7 +188,7 @@ class User { if ( $this->mId == 0 ) { $this->loadDefaults(); return false; - } + } # Try cache $key = wfMemcKey( 'user', 'id', $this->mId ); @@ -199,7 +197,7 @@ class User { # Object is expired, load from DB $data = false; } - + if ( !$data ) { wfDebug( "Cache miss for user {$this->mId}\n" ); # Load from DB @@ -229,16 +227,16 @@ class User { * Static factory method for creation from username. * * This is slightly less efficient than newFromId(), so use newFromId() if - * you have both an ID and a name handy. + * you have both an ID and a name handy. * * @param string $name Username, validated by Title:newFromText() - * @param mixed $validate Validate username. Takes the same parameters as - * User::getCanonicalName(), except that true is accepted as an alias + * @param mixed $validate Validate username. Takes the same parameters as + * User::getCanonicalName(), except that true is accepted as an alias * for 'valid', for BC. - * - * @return User object, or null if the username is invalid. If the username + * + * @return User object, or null if the username is invalid. If the username * is not present in the database, the result will be a user object with - * a name, zero user ID and default settings. + * a name, zero user ID and default settings. * @static */ static function newFromName( $name, $validate = 'valid' ) { @@ -287,7 +285,7 @@ class User { return null; } } - + /** * Create a new user object using data from session or cookies. If the * login credentials are invalid, the result is an anonymous user. @@ -313,14 +311,14 @@ class User { } /** - * 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' ); + return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ ); } /** @@ -350,9 +348,9 @@ class User { * * This function exists for username validation, in order to reject * usernames which are similar in form to IP addresses. Strings such - * as 300.300.300.300 will return true because it looks like an IP + * as 300.300.300.300 will return true because it looks like an IP * address, despite not being strictly valid. - * + * * 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 @@ -376,8 +374,8 @@ class User { * 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 + /* + * if it has any non-valid characters, it can't be a valid IPv6 * address. */ if (preg_match("/[^:a-fA-F0-9]/", $name)) @@ -422,7 +420,7 @@ class User { || $parsed->getNamespace() || strcmp( $name, $parsed->getPrefixedText() ) ) return false; - + // Check an additional blacklist of troublemaker characters. // Should these be merged into the title char list? $unicodeBlacklist = '/[' . @@ -436,10 +434,10 @@ class User { if( preg_match( $unicodeBlacklist, $name ) ) { return false; } - + return true; } - + /** * Usernames which fail to pass this function will be blocked * from user login and new account registrations, but may be used @@ -454,13 +452,13 @@ 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. !in_array( $name, $wgReservedUsernames ); } - + /** * Usernames which fail to pass this function will be blocked * from new account registrations, but may be used internally @@ -477,29 +475,33 @@ class User { static function isCreatableName( $name ) { return self::isUsableName( $name ) && - + // Registration-time character blacklisting... strpos( $name, '@' ) === false; } /** - * 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 */ function isValidPassword( $password ) { global $wgMinimalPasswordLength, $wgContLang; $result = null; - if( !wfRunHooks( 'isValidPassword', array( $password, &$result ) ) ) return $result; - if ($result === false) return false; - return (strlen( $password ) >= $wgMinimalPasswordLength) && - ($wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName )); + 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 @@ -508,16 +510,14 @@ class User { * @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 ) { + return strpos( $addr, '@' ) !== false; } /** - * Given unvalidated user input, return a canonical username, or false if + * Given unvalidated user input, return a canonical username, or false if * the username is invalid. * @param string $name * @param mixed $validate Type of validation to use: @@ -531,6 +531,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 ) ) { @@ -570,7 +576,7 @@ 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 @@ -627,7 +633,7 @@ class User { } /** - * Set cached properties to default. Note: this no longer clears + * Set cached properties to default. Note: this no longer clears * uncached lazy-initialised properties. The constructor does that instead. * * @private @@ -660,7 +666,7 @@ class User { wfProfileOut( __METHOD__ ); } - + /** * Initialise php session * @deprecated use wfSetupSession() @@ -707,7 +713,7 @@ class User { # Not a valid ID, loadFromId has switched the object to anon for us return false; } - + if ( isset( $_SESSION['wsToken'] ) ) { $passwordCorrect = $_SESSION['wsToken'] == $this->mToken; $from = 'session'; @@ -721,6 +727,7 @@ class User { } if ( ( $sName == $this->mName ) && $passwordCorrect ) { + $_SESSION['wsToken'] = $this->mToken; wfDebug( "Logged in from $from\n" ); return true; } else { @@ -730,11 +737,11 @@ class User { return false; } } - + /** * Load user and user_group data from the database * $this->mId must be set, this is how the user is identified. - * + * * @return true if the user exists, false if the user is anonymous * @private */ @@ -766,7 +773,7 @@ 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->mEditCount = $s->user_editcount; $this->getEditCount(); // revalidation for nulls # Load group data @@ -788,9 +795,9 @@ class User { } /** - * Clear various cached data stored in this object. - * @param string $reloadFrom Reload user and user_groups table data from a - * given source. May be "name", "id", "defaults", "session" or false for + * Clear various cached data stored in this object. + * @param string $reloadFrom Reload user and user_groups table data from a + * given source. May be "name", "id", "defaults", "session" or false for * no reload. */ function clearInstanceCache( $reloadFrom = false ) { @@ -884,7 +891,7 @@ class User { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__.": checking...\n" ); - $this->mBlockedby = 0; + $this->mBlockedby = 0; $this->mHideName = 0; $ip = wfGetIP(); @@ -1112,9 +1119,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; } /** @@ -1144,12 +1158,12 @@ class User { } /** - * Set the user name. + * Set the user name. * - * This does not reload fields from the database according to the given + * This does not reload fields from the database according to the given * name. Rather, it is used to create a temporary "nonexistent user" for - * later addition to the database. It can also be used to set the IP - * address for an anonymous user to something other than the current + * later addition to the database. It can also be used to set the IP + * address for an anonymous user to something other than the current * remote IP. * * User::newFromName() has rougly the same function, when the named user @@ -1211,7 +1225,7 @@ class User { return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL())); } - + /** * 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 @@ -1311,7 +1325,7 @@ class User { $this->invalidateCache(); } } - + /** * Generate a current or new-future timestamp to be stored in the * user_touched field when we update things. @@ -1320,7 +1334,7 @@ class User { global $wgClockSkewFudge; return wfTimestamp( TS_MW, time() + $wgClockSkewFudge ); } - + /** * Clear user data from memcached. * Use after applying fun updates to the database; caller's @@ -1344,13 +1358,13 @@ class User { $this->load(); if( $this->mId ) { $this->mTouched = self::newTouchedTimestamp(); - + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'user', array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ), array( 'user_id' => $this->mId ), __METHOD__ ); - + $this->clearSharedCache(); } } @@ -1362,7 +1376,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. */ @@ -1387,12 +1402,12 @@ class User { */ function setPassword( $str ) { global $wgAuth; - + if( $str !== null ) { if( !$wgAuth->allowPasswordChange() ) { throw new PasswordError( wfMsg( 'password-change-forbidden' ) ); } - + if( !$this->isValidPassword( $str ) ) { global $wgMinimalPasswordLength; throw new PasswordError( wfMsg( 'passwordtooshort', @@ -1403,7 +1418,7 @@ class User { if( !$wgAuth->setPassword( $this, $str ) ) { throw new PasswordError( wfMsg( 'externaldberror' ) ); } - + $this->setInternalPassword( $str ); return true; @@ -1418,7 +1433,7 @@ class User { function setInternalPassword( $str ) { $this->load(); $this->setToken(); - + if( $str === null ) { // Save an invalid hash... $this->mPassword = ''; @@ -1480,7 +1495,7 @@ class User { $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600; return time() < $expiry; } - + function getEmail() { $this->load(); return $this->mEmail; @@ -1529,7 +1544,7 @@ class User { } /** - * Get the user's date preference, including some important migration for + * Get the user's date preference, including some important migration for * old user rows. */ function getDatePreference() { @@ -1552,7 +1567,7 @@ class User { function getBoolOption( $oname ) { return (bool)$this->getOption( $oname ); } - + /** * Get an option as an integer value from the source string. * @param string $oname The option to check @@ -1587,6 +1602,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; } @@ -1615,7 +1631,7 @@ class User { $this->mEffectiveGroups[] = '*'; if( $this->mId ) { $this->mEffectiveGroups[] = 'user'; - + global $wgAutoConfirmAge, $wgAutoConfirmCount; $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration ); @@ -1632,25 +1648,27 @@ class User { $this->mEffectiveGroups[] = 'emailconfirmed'; } } + # 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. @@ -1704,7 +1722,11 @@ class User { * @return bool */ function isLoggedIn() { - return( $this->getID() != 0 ); + if( $this->mId === null and $this->mName !== null ) { + // Special-case optimization + return !self::isIP( $this->mName ); + } + return $this->getID() != 0; } /** @@ -1955,7 +1977,7 @@ class User { $this->load(); if ( wfReadOnly() ) { return; } if ( 0 == $this->mId ) { return; } - + $this->mTouched = self::newTouchedTimestamp(); $dbw = wfGetDB( DB_MASTER ); @@ -2044,7 +2066,7 @@ class User { } return $newUser; } - + /** * Add an existing user object to the database */ @@ -2132,6 +2154,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; } @@ -2141,6 +2165,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(); } @@ -2231,7 +2266,7 @@ class User { } return false; } - + /** * Check if the given clear-text password matches the temporary password * sent by e-mail for password reset operations. @@ -2254,16 +2289,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; } /** @@ -2288,7 +2327,6 @@ class User { * @public */ function matchEditToken( $val, $salt = '' ) { - global $wgMemc; $sessionToken = $this->editToken( $salt ); if ( $val != $sessionToken ) { wfDebug( "User::matchEditToken: broken session data\n" ); @@ -2297,6 +2335,14 @@ class User { } /** + * 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. * @@ -2433,7 +2479,7 @@ class User { return $confirmed; } } - + /** * Return true if there is an outstanding request for e-mail confirmation. * @return bool @@ -2447,6 +2493,18 @@ class User { } /** + * 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 * @return array list of permission key names for given groups combined * @static @@ -2469,7 +2527,8 @@ class User { * @static */ static function getGroupName( $group ) { - MessageCache::loadAllMessages(); + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $key = "group-$group"; $name = wfMsg( $key ); return $name == '' || wfEmptyMsg( $key, $name ) @@ -2483,7 +2542,8 @@ class User { * @static */ static function getGroupMember( $group ) { - MessageCache::loadAllMessages(); + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $key = "group-$group-member"; $name = wfMsg( $key ); return $name == '' || wfEmptyMsg( $key, $name ) @@ -2503,7 +2563,22 @@ 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() { + static $groups = null; + if( !is_array( $groups ) ) { + $groups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ); + wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); + } + return $groups; } /** @@ -2513,7 +2588,8 @@ class User { * @return mixed */ static function getGroupPage( $group ) { - MessageCache::loadAllMessages(); + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $page = wfMsgForContent( 'grouppage-' . $group ); if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) { $title = Title::newFromText( $page ); @@ -2563,7 +2639,7 @@ class User { return $text; } } - + /** * Increment the user's edit-count field. * Will have no effect for anonymous users. @@ -2575,7 +2651,7 @@ class User { array( 'user_editcount=user_editcount+1' ), array( 'user_id' => $this->getId() ), __METHOD__ ); - + // Lazy initialization check... if( $dbw->affectedRows() == 0 ) { // Pull from a slave to be less cruel to servers @@ -2585,7 +2661,7 @@ class User { 'COUNT(rev_user)', array( 'rev_user' => $this->getId() ), __METHOD__ ); - + // Now here's a goddamn hack... if( $dbr !== $dbw ) { // If we actually have a slave server, the count is @@ -2597,7 +2673,7 @@ class User { // count we just read includes the revision that was // just added in the working transaction. } - + $dbw->update( 'user', array( 'user_editcount' => $count ), array( 'user_id' => $this->getId() ), @@ -2609,4 +2685,4 @@ class User { } } -?> + |