* 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.
* corresponding database fields must call a cache-clearing function.
* @showinitializer
*/
- static $mCacheVars = array(
+ protected static $mCacheVars = array(
// user table
'mId',
'mName',
* "right-$right".
* @showinitializer
*/
- static $mCoreRights = array(
+ protected static $mCoreRights = array(
'apihighlimits',
'autoconfirmed',
'autopatrol',
'nominornewtalk',
'noratelimit',
'override-export-depth',
+ 'pagelang',
'passwordreset',
'patrol',
'patrolmarks',
'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;
//@}
* 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();
//@}
/**
*
* 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.
}
$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;
* @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 );
}
/**
} 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 {
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.
*/
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;
}
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
}
}
- /**
- * 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 );
}
$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 ) );
$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 == '' ) {
* 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.
*
global $wgMemc;
wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-' . $action );
$limits = $wgRateLimits[$action];
$keys = array();
// 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" );
}
}
+ wfProfileOut( __METHOD__ . '-' . $action );
wfProfileOut( __METHOD__ );
return $triggered;
}
/**
* 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' );
}
/**
* 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() ) {
/**
* 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 ) {
/**
* 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
*/
*
* @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 ) {
}
}
+ wfRunHooks( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
+
$this->mOptions = $newOptions;
$this->mOptionsLoaded = true;
}
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 );
}
}
- /**
- * 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
* 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;
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;
}
}
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;
}
* 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 );
if ( $val != $sessionToken ) {
wfDebug( "User::matchEditToken: broken session data\n" );
}
+
return $val == $sessionToken;
}
*
* @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
*/
*/
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;
}
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 ) {
}
/**
- * @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();
}
$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,
}
$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).
// 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' ) );