Removes deprecated $wgUseCommaCount variable
[lhc/web/wiklou.git] / includes / User.php
index 8228f1c..62e35e8 100644 (file)
@@ -56,7 +56,7 @@ class PasswordError extends MWException {
  * for rendering normal pages are set in the cookie to minimize use
  * of the database.
  */
-class User {
+class User implements IDBAccessObject {
        /**
         * Global constants made accessible as class constants so that autoloader
         * magic can be used.
@@ -76,7 +76,7 @@ class User {
         * corresponding database fields must call a cache-clearing function.
         * @showinitializer
         */
-       static $mCacheVars = array(
+       protected static $mCacheVars = array(
                // user table
                'mId',
                'mName',
@@ -105,7 +105,7 @@ class User {
         * "right-$right".
         * @showinitializer
         */
-       static $mCoreRights = array(
+       protected static $mCoreRights = array(
                'apihighlimits',
                'autoconfirmed',
                'autopatrol',
@@ -149,6 +149,7 @@ class User {
                'nominornewtalk',
                'noratelimit',
                'override-export-depth',
+               'pagelang',
                'passwordreset',
                'patrol',
                'patrolmarks',
@@ -176,17 +177,45 @@ class User {
                'viewmywatchlist',
                'writeapi',
        );
+
        /**
         * String Cached results of getAllRights()
         */
-       static $mAllRights = false;
+       protected static $mAllRights = false;
 
        /** @name Cache variables */
        //@{
-       var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
-               $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
-               $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount,
-               $mGroups, $mOptionOverrides;
+       public $mId;
+
+       public $mName;
+
+       public $mRealName;
+
+       public $mPassword;
+
+       public $mNewpassword;
+
+       public $mNewpassTime;
+
+       public $mEmail;
+
+       public $mTouched;
+
+       protected $mToken;
+
+       public $mEmailAuthenticated;
+
+       protected $mEmailToken;
+
+       protected $mEmailTokenExpires;
+
+       protected $mRegistration;
+
+       protected $mEditCount;
+
+       public $mGroups;
+
+       protected $mOptionOverrides;
 
        protected $mPasswordExpires;
        //@}
@@ -195,12 +224,12 @@ class User {
         * Bool Whether the cache variables have been loaded.
         */
        //@{
-       var $mOptionsLoaded;
+       public $mOptionsLoaded;
 
        /**
         * Array with already loaded items or true if all items have been loaded.
         */
-       private $mLoadedItems = array();
+       protected $mLoadedItems = array();
        //@}
 
        /**
@@ -212,41 +241,55 @@ class User {
         *
         * Use the User::newFrom*() family of functions to set this.
         */
-       var $mFrom;
+       public $mFrom;
 
        /**
         * Lazy-initialized variables, invalidated with clearInstanceCache
         */
-       var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mRights,
-               $mBlockreason, $mEffectiveGroups, $mImplicitGroups, $mFormerGroups, $mBlockedGlobally,
-               $mLocked, $mHideName, $mOptions;
+       protected $mNewtalk;
+
+       protected $mDatePreference;
+
+       public $mBlockedby;
+
+       protected $mHash;
+
+       public $mRights;
+
+       protected $mBlockreason;
+
+       protected $mEffectiveGroups;
+
+       protected $mImplicitGroups;
+
+       protected $mFormerGroups;
+
+       protected $mBlockedGlobally;
+
+       protected $mLocked;
+
+       public $mHideName;
+
+       public $mOptions;
 
        /**
         * @var WebRequest
         */
        private $mRequest;
 
-       /**
-        * @var Block
-        */
-       var $mBlock;
+       /** @var Block */
+       public $mBlock;
 
-       /**
-        * @var bool
-        */
-       var $mAllowUsertalk;
+       /** @var bool */
+       protected $mAllowUsertalk;
 
-       /**
-        * @var Block
-        */
+       /** @var Block */
        private $mBlockedFromCreateAccount = false;
 
-       /**
-        * @var array
-        */
+       /** @var array */
        private $mWatchedItems = array();
 
-       static $idCacheByName = array();
+       public static $idCacheByName = array();
 
        /**
         * Lightweight constructor for an anonymous user.
@@ -517,7 +560,12 @@ class User {
                }
 
                $dbr = wfGetDB( DB_SLAVE );
-               $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
+               $s = $dbr->selectRow(
+                       'user',
+                       array( 'user_id' ),
+                       array( 'user_name' => $nt->getText() ),
+                       __METHOD__
+               );
 
                if ( $s === false ) {
                        $result = null;
@@ -558,7 +606,8 @@ class User {
         * @return bool
         */
        public static function isIP( $name ) {
-               return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name ) || IP::isIPv6( $name );
+               return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
+                       || IP::isIPv6( $name );
        }
 
        /**
@@ -756,7 +805,9 @@ class User {
                        } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
                                $status->error( 'password-name-match' );
                                return $status;
-                       } elseif ( isset( $blockedLogins[$this->getName()] ) && $password == $blockedLogins[$this->getName()] ) {
+                       } elseif ( isset( $blockedLogins[$this->getName()] )
+                               && $password == $blockedLogins[$this->getName()]
+                       ) {
                                $status->error( 'password-login-forbidden' );
                                return $status;
                        } else {
@@ -841,38 +892,6 @@ class User {
                return $this->mPasswordExpires;
        }
 
-       /**
-        * Does a string look like an e-mail address?
-        *
-        * This validates an email address using an HTML5 specification found at:
-        * http://www.whatwg.org/html/states-of-the-type-attribute.html#valid-e-mail-address
-        * Which as of 2011-01-24 says:
-        *
-        *     A valid e-mail address is a string that matches the ABNF production
-        *   1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
-        *   in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
-        *   3.5.
-        *
-        * This function is an implementation of the specification as requested in
-        * bug 22449.
-        *
-        * Client-side forms will use the same standard validation rules via JS or
-        * HTML 5 validation; additional restrictions can be enforced server-side
-        * by extensions via the 'isValidEmailAddr' hook.
-        *
-        * Note that this validation doesn't 100% match RFC 2822, but is believed
-        * to be liberal enough for wide use. Some invalid addresses will still
-        * pass validation here.
-        *
-        * @param string $addr E-mail address
-        * @return bool
-        * @deprecated since 1.18 call Sanitizer::isValidEmail() directly
-        */
-       public static function isValidEmailAddr( $addr ) {
-               wfDeprecated( __METHOD__, '1.18' );
-               return Sanitizer::validateEmail( $addr );
-       }
-
        /**
         * Given unvalidated user input, return a canonical username, or false if
         * the username is invalid.
@@ -955,7 +974,8 @@ class User {
         */
        public static function randomPassword() {
                global $wgMinimalPasswordLength;
-               // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
+               // Decide the final password length based on our min password length,
+               // stopping at a minimum of 10 chars.
                $length = max( 10, $wgMinimalPasswordLength );
                // Multiply by 1.25 to get the number of hex characters we need
                $length = $length * 1.25;
@@ -1086,14 +1106,16 @@ class User {
                }
 
                if ( $request->getSessionData( 'wsToken' ) ) {
-                       $passwordCorrect = ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
+                       $passwordCorrect =
+                               ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
                        $from = 'session';
                } elseif ( $request->getCookie( 'Token' ) ) {
                        # Get the token from DB/cache and clean it up to remove garbage padding.
                        # This deals with historical problems with bugs and the default column value.
                        $token = rtrim( $proposedUser->getToken( false ) ); // correct token
                        // Make comparison in constant time (bug 61346)
-                       $passwordCorrect = strlen( $token ) && $this->compareSecrets( $token, $request->getCookie( 'Token' ) );
+                       $passwordCorrect = strlen( $token )
+                               && hash_equals( $token, $request->getCookie( 'Token' ) );
                        $from = 'cookie';
                } else {
                        // No session or persistent login cookie
@@ -1112,32 +1134,14 @@ class User {
                }
        }
 
-       /**
-        * A comparison of two strings, not vulnerable to timing attacks
-        * @param string $answer The secret string that you are comparing against.
-        * @param string $test Compare this string to the $answer.
-        * @return bool True if the strings are the same, false otherwise
-        */
-       protected function compareSecrets( $answer, $test ) {
-               if ( strlen( $answer ) !== strlen( $test ) ) {
-                       $passwordCorrect = false;
-               } else {
-                       $result = 0;
-                       for ( $i = 0; $i < strlen( $answer ); $i++ ) {
-                               $result |= ord( $answer[$i] ) ^ ord( $test[$i] );
-                       }
-                       $passwordCorrect = ( $result == 0 );
-               }
-               return $passwordCorrect;
-       }
-
        /**
         * Load user and user_group data from the database.
         * $this->mId must be set, this is how the user is identified.
         *
+        * @param integer $flags Supports User::READ_LOCKING
         * @return bool True if the user exists, false if the user is anonymous
         */
-       public function loadFromDatabase() {
+       public function loadFromDatabase( $flags = 0 ) {
                // Paranoia
                $this->mId = intval( $this->mId );
 
@@ -1148,7 +1152,15 @@ class User {
                }
 
                $dbr = wfGetDB( DB_MASTER );
-               $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ );
+               $s = $dbr->selectRow(
+                       'user',
+                       self::selectFields(),
+                       array( 'user_id' => $this->mId ),
+                       __METHOD__,
+                       ( $flags & self::READ_LOCKING == self::READ_LOCKING )
+                               ? array( 'LOCK IN SHARE MODE' )
+                               : array()
+               );
 
                wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
 
@@ -1214,9 +1226,6 @@ class User {
                        $this->mNewpassword = $row->user_newpassword;
                        $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
                        $this->mEmail = $row->user_email;
-                       if ( isset( $row->user_options ) ) {
-                               $this->decodeOptions( $row->user_options );
-                       }
                        $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
                        $this->mToken = $row->user_token;
                        if ( $this->mToken == '' ) {
@@ -1612,6 +1621,9 @@ class User {
         * Primitive rate limits: enforce maximum actions per time period
         * to put a brake on flooding.
         *
+        * The method generates both a generic profiling point and a per action one
+        * (suffix being "-$action".
+        *
         * @note When using a shared cache like memcached, IP-address
         * last-hit counters will be shared across wikis.
         *
@@ -1638,6 +1650,7 @@ class User {
 
                global $wgMemc;
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $action );
 
                $limits = $wgRateLimits[$action];
                $keys = array();
@@ -1699,7 +1712,8 @@ class User {
                        // Already pinged?
                        if ( $count ) {
                                if ( $count >= $max ) {
-                                       wfDebugLog( 'ratelimit', $this->getName() . " tripped! $key at $count $summary" );
+                                       wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
+                                               "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
                                        $triggered = true;
                                } else {
                                        wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
@@ -1715,6 +1729,7 @@ class User {
                        }
                }
 
+               wfProfileOut( __METHOD__ . '-' . $action );
                wfProfileOut( __METHOD__ );
                return $triggered;
        }
@@ -1722,10 +1737,11 @@ class User {
        /**
         * Check if user is blocked
         *
-        * @param bool $bFromSlave Whether to check the slave database instead of the master
+        * @param bool $bFromSlave Whether to check the slave database instead of
+        *   the master. Hacked from false due to horrible probs on site.
         * @return bool True if blocked, false otherwise
         */
-       public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
+       public function isBlocked( $bFromSlave = true ) {
                return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
        }
 
@@ -2082,7 +2098,8 @@ class User {
        /**
         * Update the 'You have new messages!' status.
         * @param bool $val Whether the user has new messages
-        * @param Revision $curRev New, as yet unseen revision of the user talk page. Ignored if null or !$val.
+        * @param Revision $curRev New, as yet unseen revision of the user talk
+        *   page. Ignored if null or !$val.
         */
        public function setNewtalk( $val, $curRev = null ) {
                if ( wfReadOnly() ) {
@@ -2266,7 +2283,8 @@ class User {
 
        /**
         * Get the user's current token.
-        * @param bool $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility)
+        * @param bool $forceCreation Force the generation of a new token if the
+        *   user doesn't have one (default=true for backwards compatibility).
         * @return string Token
         */
        public function getToken( $forceCreation = true ) {
@@ -2504,6 +2522,8 @@ class User {
        /**
         * Set the given option for a user.
         *
+        * You need to call saveSettings() to actually write to the database.
+        *
         * @param string $oname The option to set
         * @param mixed $val New value to set
         */
@@ -2603,7 +2623,8 @@ class User {
         *
         * @see User::listOptionKinds
         * @param IContextSource $context
-        * @param array $options Assoc. array with options keys to check as keys. Defaults to $this->mOptions.
+        * @param array $options Assoc. array with options keys to check as keys.
+        *   Defaults to $this->mOptions.
         * @return array the key => kind mapping data
         */
        public function getOptionKinds( IContextSource $context, $options = null ) {
@@ -2725,6 +2746,8 @@ class User {
                        }
                }
 
+               wfRunHooks( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
+
                $this->mOptions = $newOptions;
                $this->mOptionsLoaded = true;
        }
@@ -2893,7 +2916,7 @@ class User {
                        return null;
                }
 
-               if ( !isset( $this->mEditCount ) ) {
+               if ( $this->mEditCount === null ) {
                        /* Populate the count, if it has not been populated yet */
                        wfProfileIn( __METHOD__ );
                        $dbr = wfGetDB( DB_SLAVE );
@@ -3253,34 +3276,6 @@ class User {
                }
        }
 
-       /**
-        * Set this user's options from an encoded string
-        * @param string $str Encoded options to import
-        *
-        * @deprecated since 1.19 due to removal of user_options from the user table
-        */
-       private function decodeOptions( $str ) {
-               wfDeprecated( __METHOD__, '1.19' );
-               if ( !$str ) {
-                       return;
-               }
-
-               $this->mOptionsLoaded = true;
-               $this->mOptionOverrides = array();
-
-               // If an option is not set in $str, use the default value
-               $this->mOptions = self::getDefaultOptions();
-
-               $a = explode( "\n", $str );
-               foreach ( $a as $s ) {
-                       $m = array();
-                       if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
-                               $this->mOptions[$m[1]] = $m[2];
-                               $this->mOptionOverrides[$m[1]] = $m[2];
-                       }
-               }
-       }
-
        /**
         * Set a cookie on the user's client. Wrapper for
         * WebResponse::setCookie
@@ -3478,17 +3473,20 @@ class User {
         * Add a user to the database, return the user object
         *
         * @param string $name Username to add
-        * @param array $params Array of Strings Non-default parameters to save to the database as user_* fields:
-        *   - password             The user's password hash. Password logins will be disabled if this is omitted.
-        *   - newpassword          Hash for a temporary password that has been mailed to the user
-        *   - email                The user's email address
-        *   - email_authenticated  The email authentication timestamp
-        *   - real_name            The user's real name
-        *   - options              An associative array of non-default options
-        *   - token                Random authentication token. Do not set.
-        *   - registration         Registration timestamp. Do not set.
-        *
-        * @return User|null User object, or null if the username already exists
+        * @param array $params Array of Strings Non-default parameters to save to
+        *   the database as user_* fields:
+        *   - password: The user's password hash. Password logins will be disabled
+        *     if this is omitted.
+        *   - newpassword: Hash for a temporary password that has been mailed to
+        *     the user.
+        *   - email: The user's email address.
+        *   - email_authenticated: The email authentication timestamp.
+        *   - real_name: The user's real name.
+        *   - options: An associative array of non-default options.
+        *   - token: Random authentication token. Do not set.
+        *   - registration: Registration timestamp. Do not set.
+        *
+        * @return User|null User object, or null if the username already exists.
         */
        public static function createNew( $name, $params = array() ) {
                $user = new User;
@@ -3582,17 +3580,25 @@ class User {
                        array( 'IGNORE' )
                );
                if ( !$dbw->affectedRows() ) {
-                       if ( !$inWrite ) {
-                               // XXX: Get out of REPEATABLE-READ so the SELECT below works.
-                               // Often this case happens early in views before any writes.
-                               // This shows up at least with CentralAuth.
+                       // The queries below cannot happen in the same REPEATABLE-READ snapshot.
+                       // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
+                       if ( $inWrite ) {
+                               // Can't commit due to pending writes that may need atomicity.
+                               // This may cause some lock contention unlike the case below.
+                               $options = array( 'LOCK IN SHARE MODE' );
+                               $flags = self::READ_LOCKING;
+                       } else {
+                               // Often, this case happens early in views before any writes when
+                               // using CentralAuth. It's should be OK to commit and break the snapshot.
                                $dbw->commit( __METHOD__, 'flush' );
+                               $options = array();
+                               $flags = 0;
                        }
                        $this->mId = $dbw->selectField( 'user', 'user_id',
-                               array( 'user_name' => $this->mName ), __METHOD__ );
+                               array( 'user_name' => $this->mName ), __METHOD__, $options );
                        $loaded = false;
                        if ( $this->mId ) {
-                               if ( $this->loadFromDatabase() ) {
+                               if ( $this->loadFromDatabase( $flags ) ) {
                                        $loaded = true;
                                }
                        }
@@ -3659,7 +3665,8 @@ class User {
                if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
                        $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
                }
-               return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
+               return $this->mBlockedFromCreateAccount instanceof Block
+                       && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
                        ? $this->mBlockedFromCreateAccount
                        : false;
        }
@@ -3818,7 +3825,8 @@ class User {
         * Generate a looking random token for various uses.
         *
         * @return string The new random token
-        * @deprecated since 1.20: Use MWCryptRand for secure purposes or wfRandomString for pseudo-randomness
+        * @deprecated since 1.20: Use MWCryptRand for secure purposes or
+        *   wfRandomString for pseudo-randomness.
         */
        public static function generateToken() {
                return MWCryptRand::generateHex( 32 );
@@ -3840,6 +3848,7 @@ class User {
                if ( $val != $sessionToken ) {
                        wfDebug( "User::matchEditToken: broken session data\n" );
                }
+
                return $val == $sessionToken;
        }
 
@@ -3898,7 +3907,8 @@ class User {
         *
         * @param string $subject Message subject
         * @param string $body Message body
-        * @param string $from Optional From address; if unspecified, default $wgPasswordSender will be used
+        * @param string $from Optional From address; if unspecified, default
+        *   $wgPasswordSender will be used.
         * @param string $replyto Reply-To address
         * @return Status
         */
@@ -4284,8 +4294,11 @@ class User {
         */
        public static function getImplicitGroups() {
                global $wgImplicitGroups;
+
                $groups = $wgImplicitGroups;
-               wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );       #deprecated, use $wgImplictGroups instead
+               # Deprecated, use $wgImplictGroups instead
+               wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );
+
                return $groups;
        }
 
@@ -4359,7 +4372,13 @@ class User {
        public static function changeableByGroup( $group ) {
                global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
 
-               $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
+               $groups = array(
+                       'add' => array(),
+                       'remove' => array(),
+                       'add-self' => array(),
+                       'remove-self' => array()
+               );
+
                if ( empty( $wgAddGroups[$group] ) ) {
                        // Don't add anything to $groups
                } elseif ( $wgAddGroups[$group] === true ) {
@@ -4742,7 +4761,9 @@ class User {
        }
 
        /**
-        * @todo document
+        * Saves the non-default options for this user, as previously set e.g. via
+        * setOption(), in the database's "user_properties" (preferences) table.
+        * Usually used via saveSettings().
         */
        protected function saveOptions() {
                $this->loadOptions();
@@ -4757,13 +4778,13 @@ class User {
                }
 
                $userId = $this->getId();
-               $insert_rows = array();
+
+               $insert_rows = array(); // all the new preference rows
                foreach ( $saveOptions as $key => $value ) {
                        // Don't bother storing default values
                        $defaultOption = self::getDefaultOption( $key );
-                       if ( ( is_null( $defaultOption ) &&
-                               !( $value === false || is_null( $value ) ) ) ||
-                               $value != $defaultOption
+                       if ( ( $defaultOption === null && $value !== false && $value !== null )
+                               || $value != $defaultOption
                        ) {
                                $insert_rows[] = array(
                                        'up_user' => $userId,
@@ -4774,14 +4795,22 @@ class User {
                }
 
                $dbw = wfGetDB( DB_MASTER );
-               // Find and delete any prior preference rows...
+
                $res = $dbw->select( 'user_properties',
-                       array( 'up_property' ), array( 'up_user' => $userId ), __METHOD__ );
-               $priorKeys = array();
+                       array( 'up_property', 'up_value' ), array( 'up_user' => $userId ), __METHOD__ );
+
+               // Find prior rows that need to be removed or updated. These rows will
+               // all be deleted (the later so that INSERT IGNORE applies the new values).
+               $keysDelete = array();
                foreach ( $res as $row ) {
-                       $priorKeys[] = $row->up_property;
+                       if ( !isset( $saveOptions[$row->up_property] )
+                               || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
+                       ) {
+                               $keysDelete[] = $row->up_property;
+                       }
                }
-               if ( count( $priorKeys ) ) {
+
+               if ( count( $keysDelete ) ) {
                        // Do the DELETE by PRIMARY KEY for prior rows.
                        // In the past a very large portion of calls to this function are for setting
                        // 'rememberpassword' for new accounts (a preference that has since been removed).
@@ -4790,7 +4819,7 @@ class User {
                        // updates would pile up on each other as they are for higher (newer) user IDs.
                        // It might not be necessary these days, but it shouldn't hurt either.
                        $dbw->delete( 'user_properties',
-                               array( 'up_user' => $userId, 'up_property' => $priorKeys ), __METHOD__ );
+                               array( 'up_user' => $userId, 'up_property' => $keysDelete ), __METHOD__ );
                }
                // Insert the new preference rows
                $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );