PageRenderTime 436ms CodeModel.GetById 131ms app.highlight 110ms RepoModel.GetById 91ms app.codeStats 1ms

/includes/User.php

https://github.com/tav/confluence
PHP | 3404 lines | 2066 code | 280 blank | 1058 comment | 307 complexity | 8653b2a9a6500407554f9532e5a27a41 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2/**
   3 * Implements the User class for the %MediaWiki software.
   4 * @file
   5 */
   6
   7/**
   8 * \int Number of characters in user_token field.
   9 * @ingroup Constants
  10 */
  11define( 'USER_TOKEN_LENGTH', 32 );
  12
  13/**
  14 * \int Serialized record version.
  15 * @ingroup Constants
  16 */
  17define( 'MW_USER_VERSION', 6 );
  18
  19/**
  20 * \string Some punctuation to prevent editing from broken text-mangling proxies.
  21 * @ingroup Constants
  22 */
  23define( 'EDIT_TOKEN_SUFFIX', '+\\' );
  24
  25/**
  26 * Thrown by User::setPassword() on error.
  27 * @ingroup Exception
  28 */
  29class PasswordError extends MWException {
  30	// NOP
  31}
  32
  33/**
  34 * The User object encapsulates all of the user-specific settings (user_id,
  35 * name, rights, password, email address, options, last login time). Client
  36 * classes use the getXXX() functions to access these fields. These functions
  37 * do all the work of determining whether the user is logged in,
  38 * whether the requested option can be satisfied from cookies or
  39 * whether a database query is needed. Most of the settings needed
  40 * for rendering normal pages are set in the cookie to minimize use
  41 * of the database.
  42 */
  43class User {
  44
  45	/**
  46	 * \type{\arrayof{\string}} A list of default user toggles, i.e., boolean user 
  47         * preferences that are displayed by Special:Preferences as checkboxes.
  48	 * This list can be extended via the UserToggles hook or by
  49	 * $wgContLang::getExtraUserToggles().
  50	 * @showinitializer
  51	 */
  52	public static $mToggles = array(
  53		'highlightbroken',
  54		'justify',
  55		'hideminor',
  56		'extendwatchlist',
  57		'usenewrc',
  58		'numberheadings',
  59		'showtoolbar',
  60		'editondblclick',
  61		'editsection',
  62		'editsectiononrightclick',
  63		'showtoc',
  64		'rememberpassword',
  65		'editwidth',
  66		'watchcreations',
  67		'watchdefault',
  68		'watchmoves',
  69		'watchdeletion',
  70		'minordefault',
  71		'previewontop',
  72		'previewonfirst',
  73		'nocache',
  74		'enotifwatchlistpages',
  75		'enotifusertalkpages',
  76		'enotifminoredits',
  77		'enotifrevealaddr',
  78		'shownumberswatching',
  79		'fancysig',
  80		'externaleditor',
  81		'externaldiff',
  82		'showjumplinks',
  83		'uselivepreview',
  84		'forceeditsummary',
  85		'watchlisthideminor',
  86		'watchlisthidebots',
  87		'watchlisthideown',
  88		'watchlisthideanons',
  89		'watchlisthideliu',
  90		'ccmeonemails',
  91		'diffonly',
  92		'showhiddencats',
  93		'noconvertlink',
  94		'norollbackdiff',
  95	);
  96
  97	/**
  98	 * \type{\arrayof{\string}} List of member variables which are saved to the 
  99	 * shared cache (memcached). Any operation which changes the 
 100	 * corresponding database fields must call a cache-clearing function.
 101	 * @showinitializer
 102	 */
 103	static $mCacheVars = array(
 104		// user table
 105		'mId',
 106		'mName',
 107		'mRealName',
 108		'mPassword',
 109		'mNewpassword',
 110		'mNewpassTime',
 111		'mEmail',
 112		'mOptions',
 113		'mTouched',
 114		'mToken',
 115		'mEmailAuthenticated',
 116		'mEmailToken',
 117		'mEmailTokenExpires',
 118		'mRegistration',
 119		'mEditCount',
 120		// user_group table
 121		'mGroups',
 122	);
 123
 124	/**
 125	 * \type{\arrayof{\string}} Core rights.
 126	 * Each of these should have a corresponding message of the form 
 127	 * "right-$right".
 128	 * @showinitializer
 129	 */
 130	static $mCoreRights = array(
 131		'apihighlimits',
 132		'autoconfirmed',
 133		'autopatrol',
 134		'bigdelete',
 135		'block',
 136		'blockemail',
 137		'bot',
 138		'browsearchive',
 139		'createaccount',
 140		'createpage',
 141		'createtalk',
 142		'delete',
 143		'deletedhistory',
 144		'deleterevision',
 145		'edit',
 146		'editinterface',
 147		'editusercssjs',
 148		'hideuser',
 149		'import',
 150		'importupload',
 151		'ipblock-exempt',
 152		'markbotedits',
 153		'minoredit',
 154		'move',
 155		'movefile',
 156		'move-rootuserpages',
 157		'move-subpages',
 158		'nominornewtalk',
 159		'noratelimit',
 160		'override-export-depth',
 161		'patrol',
 162		'protect',
 163		'proxyunbannable',
 164		'purge',
 165		'read',
 166		'reupload',
 167		'reupload-shared',
 168		'rollback',
 169		'siteadmin',
 170		'suppressionlog',
 171		'suppressredirect',
 172		'suppressrevision',
 173		'trackback',
 174		'undelete',
 175		'unwatchedpages',
 176		'upload',
 177		'upload_by_url',
 178		'userrights',
 179		'userrights-interwiki',
 180		'writeapi',
 181	);
 182	/**
 183	 * \string Cached results of getAllRights()
 184	 */
 185	static $mAllRights = false;
 186
 187	/** @name Cache variables */
 188	//@{
 189	var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
 190		$mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
 191		$mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
 192	//@}
 193
 194	/**
 195	 * \bool Whether the cache variables have been loaded.
 196	 */
 197	var $mDataLoaded, $mAuthLoaded;
 198
 199	/**
 200	 * \string Initialization data source if mDataLoaded==false. May be one of:
 201	 *  - 'defaults'   anonymous user initialised from class defaults
 202	 *  - 'name'       initialise from mName
 203	 *  - 'id'         initialise from mId
 204	 *  - 'session'    log in from cookies or session if possible
 205	 *
 206	 * Use the User::newFrom*() family of functions to set this.
 207	 */
 208	var $mFrom;
 209
 210	/** @name Lazy-initialized variables, invalidated with clearInstanceCache */
 211	//@{
 212	var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
 213		$mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally, 
 214		$mLocked, $mHideName;
 215	//@}
 216
 217	/**
 218	 * Lightweight constructor for an anonymous user.
 219	 * Use the User::newFrom* factory functions for other kinds of users.
 220	 * 
 221	 * @see newFromName()
 222	 * @see newFromId()
 223	 * @see newFromConfirmationCode()
 224	 * @see newFromSession()
 225	 * @see newFromRow()
 226	 */
 227	function User() {
 228		$this->clearInstanceCache( 'defaults' );
 229	}
 230
 231	/**
 232	 * Load the user table data for this object from the source given by mFrom.
 233	 */
 234	function load() {
 235		if ( $this->mDataLoaded ) {
 236			return;
 237		}
 238		wfProfileIn( __METHOD__ );
 239
 240		# Set it now to avoid infinite recursion in accessors
 241		$this->mDataLoaded = true;
 242
 243		switch ( $this->mFrom ) {
 244			case 'defaults':
 245				$this->loadDefaults();
 246				break;
 247			case 'name':
 248				$this->mId = self::idFromName( $this->mName );
 249				if ( !$this->mId ) {
 250					# Nonexistent user placeholder object
 251					$this->loadDefaults( $this->mName );
 252				} else {
 253					$this->loadFromId();
 254				}
 255				break;
 256			case 'id':
 257				$this->loadFromId();
 258				break;
 259			case 'session':
 260				$this->loadFromSession();
 261				wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
 262				break;
 263			default:
 264				throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
 265		}
 266		wfProfileOut( __METHOD__ );
 267	}
 268
 269	/**
 270	 * Load user table data, given mId has already been set.
 271	 * @return \bool false if the ID does not exist, true otherwise
 272	 * @private
 273	 */
 274	function loadFromId() {
 275		global $wgMemc;
 276		if ( $this->mId == 0 ) {
 277			$this->loadDefaults();
 278			return false;
 279		}
 280
 281		# Try cache
 282		$key = wfMemcKey( 'user', 'id', $this->mId );
 283		$data = $wgMemc->get( $key );
 284		if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
 285			# Object is expired, load from DB
 286			$data = false;
 287		}
 288
 289		if ( !$data ) {
 290			wfDebug( "Cache miss for user {$this->mId}\n" );
 291			# Load from DB
 292			if ( !$this->loadFromDatabase() ) {
 293				# Can't load from ID, user is anonymous
 294				return false;
 295			}
 296			$this->saveToCache();
 297		} else {
 298			wfDebug( "Got user {$this->mId} from cache\n" );
 299			# Restore from cache
 300			foreach ( self::$mCacheVars as $name ) {
 301				$this->$name = $data[$name];
 302			}
 303		}
 304		return true;
 305	}
 306
 307	/**
 308	 * Save user data to the shared cache
 309	 */
 310	function saveToCache() {
 311		$this->load();
 312		$this->loadGroups();
 313		if ( $this->isAnon() ) {
 314			// Anonymous users are uncached
 315			return;
 316		}
 317		$data = array();
 318		foreach ( self::$mCacheVars as $name ) {
 319			$data[$name] = $this->$name;
 320		}
 321		$data['mVersion'] = MW_USER_VERSION;
 322		$key = wfMemcKey( 'user', 'id', $this->mId );
 323		global $wgMemc;
 324		$wgMemc->set( $key, $data );
 325	}
 326	
 327	
 328	/** @name newFrom*() static factory methods */
 329	//@{
 330
 331	/**
 332	 * Static factory method for creation from username.
 333	 *
 334	 * This is slightly less efficient than newFromId(), so use newFromId() if
 335	 * you have both an ID and a name handy.
 336	 *
 337	 * @param $name \string Username, validated by Title::newFromText()
 338	 * @param $validate \mixed Validate username. Takes the same parameters as
 339	 *    User::getCanonicalName(), except that true is accepted as an alias
 340	 *    for 'valid', for BC.
 341	 *
 342	 * @return \type{User} The User object, or null if the username is invalid. If the 
 343	 *    username is not present in the database, the result will be a user object 
 344	 *    with a name, zero user ID and default settings.
 345	 */
 346	static function newFromName( $name, $validate = 'valid' ) {
 347		if ( $validate === true ) {
 348			$validate = 'valid';
 349		}
 350		$name = self::getCanonicalName( $name, $validate );
 351		if ( $name === false ) {
 352			return null;
 353		} else {
 354			# Create unloaded user object
 355			$u = new User;
 356			$u->mName = $name;
 357			$u->mFrom = 'name';
 358			return $u;
 359		}
 360	}
 361
 362	/**
 363	 * Static factory method for creation from a given user ID.
 364	 *
 365	 * @param $id \int Valid user ID
 366	 * @return \type{User} The corresponding User object
 367	 */
 368	static function newFromId( $id ) {
 369		$u = new User;
 370		$u->mId = $id;
 371		$u->mFrom = 'id';
 372		return $u;
 373	}
 374
 375	/**
 376	 * Factory method to fetch whichever user has a given email confirmation code.
 377	 * This code is generated when an account is created or its e-mail address
 378	 * has changed.
 379	 *
 380	 * If the code is invalid or has expired, returns NULL.
 381	 *
 382	 * @param $code \string Confirmation code
 383	 * @return \type{User}
 384	 */
 385	static function newFromConfirmationCode( $code ) {
 386		$dbr = wfGetDB( DB_SLAVE );
 387		$id = $dbr->selectField( 'user', 'user_id', array(
 388			'user_email_token' => md5( $code ),
 389			'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
 390			) );
 391		if( $id !== false ) {
 392			return User::newFromId( $id );
 393		} else {
 394			return null;
 395		}
 396	}
 397
 398	/**
 399	 * Create a new user object using data from session or cookies. If the
 400	 * login credentials are invalid, the result is an anonymous user.
 401	 *
 402	 * @return \type{User}
 403	 */
 404	static function newFromSession() {
 405		$user = new User;
 406		$user->mFrom = 'session';
 407		return $user;
 408	}
 409
 410	/**
 411	 * Create a new user object from a user row.
 412	 * The row should have all fields from the user table in it.
 413	 * @param $row array A row from the user table
 414	 * @return \type{User}
 415	 */
 416	static function newFromRow( $row ) {
 417		$user = new User;
 418		$user->loadFromRow( $row );
 419		return $user;
 420	}
 421	
 422	//@}
 423	
 424
 425	/**
 426	 * Get the username corresponding to a given user ID
 427	 * @param $id \int User ID
 428	 * @return \string The corresponding username
 429	 */
 430	static function whoIs( $id ) {
 431		$dbr = wfGetDB( DB_SLAVE );
 432		return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
 433	}
 434
 435	/**
 436	 * Get the real name of a user given their user ID
 437	 *
 438	 * @param $id \int User ID
 439	 * @return \string The corresponding user's real name
 440	 */
 441	static function whoIsReal( $id ) {
 442		$dbr = wfGetDB( DB_SLAVE );
 443		return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
 444	}
 445
 446	/**
 447	 * Get database id given a user name
 448	 * @param $name \string Username
 449	 * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
 450	 */
 451	static function idFromName( $name ) {
 452		$nt = Title::makeTitleSafe( NS_USER, $name );
 453		if( is_null( $nt ) ) {
 454			# Illegal name
 455			return null;
 456		}
 457		$dbr = wfGetDB( DB_SLAVE );
 458		$s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
 459
 460		if ( $s === false ) {
 461			return 0;
 462		} else {
 463			return $s->user_id;
 464		}
 465	}
 466
 467	/**
 468	 * Does the string match an anonymous IPv4 address?
 469	 *
 470	 * This function exists for username validation, in order to reject
 471	 * usernames which are similar in form to IP addresses. Strings such
 472	 * as 300.300.300.300 will return true because it looks like an IP
 473	 * address, despite not being strictly valid.
 474	 *
 475	 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
 476	 * address because the usemod software would "cloak" anonymous IP
 477	 * addresses like this, if we allowed accounts like this to be created
 478	 * new users could get the old edits of these anonymous users.
 479	 *
 480	 * @param $name \string String to match
 481	 * @return \bool True or false
 482	 */
 483	static function isIP( $name ) {
 484		return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
 485	}
 486
 487	/**
 488	 * Is the input a valid username?
 489	 *
 490	 * Checks if the input is a valid username, we don't want an empty string,
 491	 * an IP address, anything that containins slashes (would mess up subpages),
 492	 * is longer than the maximum allowed username size or doesn't begin with
 493	 * a capital letter.
 494	 *
 495	 * @param $name \string String to match
 496	 * @return \bool True or false
 497	 */
 498	static function isValidUserName( $name ) {
 499		global $wgContLang, $wgMaxNameChars;
 500
 501		if ( $name == ''
 502		|| User::isIP( $name )
 503		|| strpos( $name, '/' ) !== false
 504		|| strlen( $name ) > $wgMaxNameChars
 505		|| $name != $wgContLang->ucfirst( $name ) ) {
 506			wfDebugLog( 'username', __METHOD__ .
 507				": '$name' invalid due to empty, IP, slash, length, or lowercase" );
 508			return false;
 509		}
 510
 511		// Ensure that the name can't be misresolved as a different title,
 512		// such as with extra namespace keys at the start.
 513		$parsed = Title::newFromText( $name );
 514		if( is_null( $parsed )
 515			|| $parsed->getNamespace()
 516			|| strcmp( $name, $parsed->getPrefixedText() ) ) {
 517			wfDebugLog( 'username', __METHOD__ .
 518				": '$name' invalid due to ambiguous prefixes" );
 519			return false;
 520		}
 521
 522		// Check an additional blacklist of troublemaker characters.
 523		// Should these be merged into the title char list?
 524		$unicodeBlacklist = '/[' .
 525			'\x{0080}-\x{009f}' . # iso-8859-1 control chars
 526			'\x{00a0}' .          # non-breaking space
 527			'\x{2000}-\x{200f}' . # various whitespace
 528			'\x{2028}-\x{202f}' . # breaks and control chars
 529			'\x{3000}' .          # ideographic space
 530			'\x{e000}-\x{f8ff}' . # private use
 531			']/u';
 532		if( preg_match( $unicodeBlacklist, $name ) ) {
 533			wfDebugLog( 'username', __METHOD__ .
 534				": '$name' invalid due to blacklisted characters" );
 535			return false;
 536		}
 537
 538		return true;
 539	}
 540
 541	/**
 542	 * Usernames which fail to pass this function will be blocked
 543	 * from user login and new account registrations, but may be used
 544	 * internally by batch processes.
 545	 *
 546	 * If an account already exists in this form, login will be blocked
 547	 * by a failure to pass this function.
 548	 *
 549	 * @param $name \string String to match
 550	 * @return \bool True or false
 551	 */
 552	static function isUsableName( $name ) {
 553		global $wgReservedUsernames;
 554		// Must be a valid username, obviously ;)
 555		if ( !self::isValidUserName( $name ) ) {
 556			return false;
 557		}
 558
 559		static $reservedUsernames = false;
 560		if ( !$reservedUsernames ) {
 561			$reservedUsernames = $wgReservedUsernames;
 562			wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
 563		}
 564
 565		// Certain names may be reserved for batch processes.
 566		foreach ( $reservedUsernames as $reserved ) {
 567			if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
 568				$reserved = wfMsgForContent( substr( $reserved, 4 ) );
 569			}
 570			if ( $reserved == $name ) {
 571				return false;
 572			}
 573		}
 574		return true;
 575	}
 576
 577	/**
 578	 * Usernames which fail to pass this function will be blocked
 579	 * from new account registrations, but may be used internally
 580	 * either by batch processes or by user accounts which have
 581	 * already been created.
 582	 *
 583	 * Additional character blacklisting may be added here
 584	 * rather than in isValidUserName() to avoid disrupting
 585	 * existing accounts.
 586	 *
 587	 * @param $name \string String to match
 588	 * @return \bool True or false
 589	 */
 590	static function isCreatableName( $name ) {
 591		global $wgInvalidUsernameCharacters;
 592		return
 593			self::isUsableName( $name ) &&
 594
 595			// Registration-time character blacklisting...
 596			!preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name );
 597	}
 598
 599	/**
 600	 * Is the input a valid password for this user?
 601	 *
 602	 * @param $password \string Desired password
 603	 * @return \bool True or false
 604	 */
 605	function isValidPassword( $password ) {
 606		global $wgMinimalPasswordLength, $wgContLang;
 607
 608		$result = null;
 609		if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
 610			return $result;
 611		if( $result === false )
 612			return false;
 613
 614		// Password needs to be long enough, and can't be the same as the username
 615		return strlen( $password ) >= $wgMinimalPasswordLength
 616			&& $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
 617	}
 618
 619	/**
 620	 * Does a string look like an e-mail address?
 621	 *
 622	 * There used to be a regular expression here, it got removed because it
 623	 * rejected valid addresses. Actually just check if there is '@' somewhere
 624	 * in the given address.
 625	 *
 626	 * @todo Check for RFC 2822 compilance (bug 959)
 627	 *
 628	 * @param $addr \string E-mail address
 629	 * @return \bool True or false
 630	 */
 631	public static function isValidEmailAddr( $addr ) {
 632		$result = null;
 633		if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
 634			return $result;
 635		}
 636
 637		return strpos( $addr, '@' ) !== false;
 638	}
 639
 640	/**
 641	 * Given unvalidated user input, return a canonical username, or false if
 642	 * the username is invalid.
 643	 * @param $name \string User input
 644	 * @param $validate \types{\string,\bool} Type of validation to use:
 645	 *                - false        No validation
 646	 *                - 'valid'      Valid for batch processes
 647	 *                - 'usable'     Valid for batch processes and login
 648	 *                - 'creatable'  Valid for batch processes, login and account creation
 649	 */
 650	static function getCanonicalName( $name, $validate = 'valid' ) {
 651		# Force usernames to capital
 652		global $wgContLang;
 653		$name = $wgContLang->ucfirst( $name );
 654
 655		# Reject names containing '#'; these will be cleaned up
 656		# with title normalisation, but then it's too late to
 657		# check elsewhere
 658		if( strpos( $name, '#' ) !== false )
 659			return false;
 660
 661		# Clean up name according to title rules
 662		$t = ($validate === 'valid') ? 
 663			Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
 664		# Check for invalid titles
 665		if( is_null( $t ) ) {
 666			return false;
 667		}
 668
 669		# Reject various classes of invalid names
 670		$name = $t->getText();
 671		global $wgAuth;
 672		$name = $wgAuth->getCanonicalName( $t->getText() );
 673
 674		switch ( $validate ) {
 675			case false:
 676				break;
 677			case 'valid':
 678				if ( !User::isValidUserName( $name ) ) {
 679					$name = false;
 680				}
 681				break;
 682			case 'usable':
 683				if ( !User::isUsableName( $name ) ) {
 684					$name = false;
 685				}
 686				break;
 687			case 'creatable':
 688				if ( !User::isCreatableName( $name ) ) {
 689					$name = false;
 690				}
 691				break;
 692			default:
 693				throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
 694		}
 695		return $name;
 696	}
 697
 698	/**
 699	 * Count the number of edits of a user
 700	 * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
 701	 *
 702	 * @param $uid \int User ID to check
 703	 * @return \int The user's edit count
 704	 */
 705	static function edits( $uid ) {
 706		wfProfileIn( __METHOD__ );
 707		$dbr = wfGetDB( DB_SLAVE );
 708		// check if the user_editcount field has been initialized
 709		$field = $dbr->selectField(
 710			'user', 'user_editcount',
 711			array( 'user_id' => $uid ),
 712			__METHOD__
 713		);
 714
 715		if( $field === null ) { // it has not been initialized. do so.
 716			$dbw = wfGetDB( DB_MASTER );
 717			$count = $dbr->selectField(
 718				'revision', 'count(*)',
 719				array( 'rev_user' => $uid ),
 720				__METHOD__
 721			);
 722			$dbw->update(
 723				'user',
 724				array( 'user_editcount' => $count ),
 725				array( 'user_id' => $uid ),
 726				__METHOD__
 727			);
 728		} else {
 729			$count = $field;
 730		}
 731		wfProfileOut( __METHOD__ );
 732		return $count;
 733	}
 734
 735	/**
 736	 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
 737	 * @todo hash random numbers to improve security, like generateToken()
 738	 *
 739	 * @return \string New random password
 740	 */
 741	static function randomPassword() {
 742		global $wgMinimalPasswordLength;
 743		$pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
 744		$l = strlen( $pwchars ) - 1;
 745
 746		$pwlength = max( 7, $wgMinimalPasswordLength );
 747		$digit = mt_rand(0, $pwlength - 1);
 748		$np = '';
 749		for ( $i = 0; $i < $pwlength; $i++ ) {
 750			$np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
 751		}
 752		return $np;
 753	}
 754
 755	/**
 756	 * Set cached properties to default. 
 757	 *
 758	 * @note This no longer clears uncached lazy-initialised properties; 
 759	 *       the constructor does that instead.
 760	 * @private
 761	 */
 762	function loadDefaults( $name = false ) {
 763		wfProfileIn( __METHOD__ );
 764
 765		global $wgCookiePrefix;
 766
 767		$this->mId = 0;
 768		$this->mName = $name;
 769		$this->mRealName = '';
 770		$this->mPassword = $this->mNewpassword = '';
 771		$this->mNewpassTime = null;
 772		$this->mEmail = '';
 773		$this->mOptions = null; # Defer init
 774
 775		if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
 776			$this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
 777		} else {
 778			$this->mTouched = '0'; # Allow any pages to be cached
 779		}
 780
 781		$this->setToken(); # Random
 782		$this->mEmailAuthenticated = null;
 783		$this->mEmailToken = '';
 784		$this->mEmailTokenExpires = null;
 785		$this->mRegistration = wfTimestamp( TS_MW );
 786		$this->mGroups = array();
 787
 788		wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
 789
 790		wfProfileOut( __METHOD__ );
 791	}
 792
 793	/**
 794	 * @deprecated Use wfSetupSession().
 795	 */
 796	function SetupSession() {
 797		wfDeprecated( __METHOD__ );
 798		wfSetupSession();
 799	}
 800
 801	/**
 802	 * Load user data from the session or login cookie. If there are no valid
 803	 * credentials, initialises the user as an anonymous user.
 804	 * @return \bool True if the user is logged in, false otherwise.
 805	 */
 806	private function loadFromSession() {
 807		global $wgMemc, $wgCookiePrefix;
 808
 809		$result = null;
 810		wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
 811		if ( $result !== null ) {
 812			return $result;
 813		}
 814
 815		if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
 816			$sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
 817			if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
 818				$this->loadDefaults(); // Possible collision!
 819				wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and 
 820					cookie user ID ($sId) don't match!" );
 821				return false;
 822			}
 823			$_SESSION['wsUserID'] = $sId;
 824		} else if ( isset( $_SESSION['wsUserID'] ) ) {
 825			if ( $_SESSION['wsUserID'] != 0 ) {
 826				$sId = $_SESSION['wsUserID'];
 827			} else {
 828				$this->loadDefaults();
 829				return false;
 830			}
 831		} else {
 832			$this->loadDefaults();
 833			return false;
 834		}
 835
 836		if ( isset( $_SESSION['wsUserName'] ) ) {
 837			$sName = $_SESSION['wsUserName'];
 838		} else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
 839			$sName = $_COOKIE["{$wgCookiePrefix}UserName"];
 840			$_SESSION['wsUserName'] = $sName;
 841		} else {
 842			$this->loadDefaults();
 843			return false;
 844		}
 845
 846		$passwordCorrect = FALSE;
 847		$this->mId = $sId;
 848		if ( !$this->loadFromId() ) {
 849			# Not a valid ID, loadFromId has switched the object to anon for us
 850			return false;
 851		}
 852
 853		if ( isset( $_SESSION['wsToken'] ) ) {
 854			$passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
 855			$from = 'session';
 856		} else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
 857			$passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
 858			$from = 'cookie';
 859		} else {
 860			# No session or persistent login cookie
 861			$this->loadDefaults();
 862			return false;
 863		}
 864
 865		if ( ( $sName == $this->mName ) && $passwordCorrect ) {
 866			$_SESSION['wsToken'] = $this->mToken;
 867			wfDebug( "Logged in from $from\n" );
 868			return true;
 869		} else {
 870			# Invalid credentials
 871			wfDebug( "Can't log in from $from, invalid credentials\n" );
 872			$this->loadDefaults();
 873			return false;
 874		}
 875	}
 876
 877	/**
 878	 * Load user and user_group data from the database.
 879	 * $this::mId must be set, this is how the user is identified.
 880	 *
 881	 * @return \bool True if the user exists, false if the user is anonymous
 882	 * @private
 883	 */
 884	function loadFromDatabase() {
 885		# Paranoia
 886		$this->mId = intval( $this->mId );
 887
 888		/** Anonymous user */
 889		if( !$this->mId ) {
 890			$this->loadDefaults();
 891			return false;
 892		}
 893
 894		$dbr = wfGetDB( DB_MASTER );
 895		$s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
 896
 897		wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
 898
 899		if ( $s !== false ) {
 900			# Initialise user table data
 901			$this->loadFromRow( $s );
 902			$this->mGroups = null; // deferred
 903			$this->getEditCount(); // revalidation for nulls
 904			return true;
 905		} else {
 906			# Invalid user_id
 907			$this->mId = 0;
 908			$this->loadDefaults();
 909			return false;
 910		}
 911	}
 912
 913	/**
 914	 * Initialize this object from a row from the user table.
 915	 *
 916	 * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
 917	 */
 918	function loadFromRow( $row ) {
 919		$this->mDataLoaded = true;
 920
 921		if ( isset( $row->user_id ) ) {
 922			$this->mId = intval( $row->user_id );
 923		}
 924		$this->mName = $row->user_name;
 925		$this->mRealName = $row->user_real_name;
 926		$this->mPassword = $row->user_password;
 927		$this->mNewpassword = $row->user_newpassword;
 928		$this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
 929		$this->mEmail = $row->user_email;
 930		$this->decodeOptions( $row->user_options );
 931		$this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
 932		$this->mToken = $row->user_token;
 933		$this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
 934		$this->mEmailToken = $row->user_email_token;
 935		$this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
 936		$this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
 937		$this->mEditCount = $row->user_editcount; 
 938	}
 939
 940	/**
 941	 * Load the groups from the database if they aren't already loaded.
 942	 * @private
 943	 */
 944	function loadGroups() {
 945		if ( is_null( $this->mGroups ) ) {
 946			$dbr = wfGetDB( DB_MASTER );
 947			$res = $dbr->select( 'user_groups',
 948				array( 'ug_group' ),
 949				array( 'ug_user' => $this->mId ),
 950				__METHOD__ );
 951			$this->mGroups = array();
 952			while( $row = $dbr->fetchObject( $res ) ) {
 953				$this->mGroups[] = $row->ug_group;
 954			}
 955		}
 956	}
 957
 958	/**
 959	 * Clear various cached data stored in this object.
 960	 * @param $reloadFrom \string Reload user and user_groups table data from a
 961	 *   given source. May be "name", "id", "defaults", "session", or false for
 962	 *   no reload.
 963	 */
 964	function clearInstanceCache( $reloadFrom = false ) {
 965		$this->mNewtalk = -1;
 966		$this->mDatePreference = null;
 967		$this->mBlockedby = -1; # Unset
 968		$this->mHash = false;
 969		$this->mSkin = null;
 970		$this->mRights = null;
 971		$this->mEffectiveGroups = null;
 972
 973		if ( $reloadFrom ) {
 974			$this->mDataLoaded = false;
 975			$this->mFrom = $reloadFrom;
 976		}
 977	}
 978
 979	/**
 980	 * Combine the language default options with any site-specific options
 981	 * and add the default language variants.
 982	 *
 983	 * @return \type{\arrayof{\string}} Array of options
 984	 */
 985	static function getDefaultOptions() {
 986		global $wgNamespacesToBeSearchedDefault;
 987		/**
 988		 * Site defaults will override the global/language defaults
 989		 */
 990		global $wgDefaultUserOptions, $wgContLang;
 991		$defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
 992
 993		/**
 994		 * default language setting
 995		 */
 996		$variant = $wgContLang->getPreferredVariant( false );
 997		$defOpt['variant'] = $variant;
 998		$defOpt['language'] = $variant;
 999
1000		foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
1001			$defOpt['searchNs'.$nsnum] = $val;
1002		}
1003		return $defOpt;
1004	}
1005
1006	/**
1007	 * Get a given default option value.
1008	 *
1009	 * @param $opt \string Name of option to retrieve
1010	 * @return \string Default option value
1011	 */
1012	public static function getDefaultOption( $opt ) {
1013		$defOpts = self::getDefaultOptions();
1014		if( isset( $defOpts[$opt] ) ) {
1015			return $defOpts[$opt];
1016		} else {
1017			return '';
1018		}
1019	}
1020
1021	/**
1022	 * Get a list of user toggle names
1023	 * @return \type{\arrayof{\string}} Array of user toggle names
1024	 */
1025	static function getToggles() {
1026		global $wgContLang, $wgUseRCPatrol;
1027		$extraToggles = array();
1028		wfRunHooks( 'UserToggles', array( &$extraToggles ) );
1029		if( $wgUseRCPatrol ) {
1030			$extraToggles[] = 'hidepatrolled';
1031			$extraToggles[] = 'newpageshidepatrolled';
1032			$extraToggles[] = 'watchlisthidepatrolled';
1033		}
1034		return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
1035	}
1036
1037
1038	/**
1039	 * Get blocking information
1040	 * @private
1041	 * @param $bFromSlave \bool Whether to check the slave database first. To
1042	 *                    improve performance, non-critical checks are done
1043	 *                    against slaves. Check when actually saving should be
1044	 *                    done against master.
1045	 */
1046	function getBlockedStatus( $bFromSlave = true ) {
1047		global $wgEnableSorbs, $wgProxyWhitelist;
1048
1049		if ( -1 != $this->mBlockedby ) {
1050			wfDebug( "User::getBlockedStatus: already loaded.\n" );
1051			return;
1052		}
1053
1054		wfProfileIn( __METHOD__ );
1055		wfDebug( __METHOD__.": checking...\n" );
1056
1057		// Initialize data...
1058		// Otherwise something ends up stomping on $this->mBlockedby when
1059		// things get lazy-loaded later, causing false positive block hits
1060		// due to -1 !== 0. Probably session-related... Nothing should be
1061		// overwriting mBlockedby, surely?
1062		$this->load();
1063		
1064		$this->mBlockedby = 0;
1065		$this->mHideName = 0;
1066		$this->mAllowUsertalk = 0;
1067		$ip = wfGetIP();
1068
1069		if ($this->isAllowed( 'ipblock-exempt' ) ) {
1070			# Exempt from all types of IP-block
1071			$ip = '';
1072		}
1073
1074		# User/IP blocking
1075		$this->mBlock = new Block();
1076		$this->mBlock->fromMaster( !$bFromSlave );
1077		if ( $this->mBlock->load( $ip , $this->mId ) ) {
1078			wfDebug( __METHOD__.": Found block.\n" );
1079			$this->mBlockedby = $this->mBlock->mBy;
1080			$this->mBlockreason = $this->mBlock->mReason;
1081			$this->mHideName = $this->mBlock->mHideName;
1082			$this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
1083			if ( $this->isLoggedIn() ) {
1084				$this->spreadBlock();
1085			}
1086		} else {
1087			// Bug 13611: don't remove mBlock here, to allow account creation blocks to 
1088			// apply to users. Note that the existence of $this->mBlock is not used to 
1089			// check for edit blocks, $this->mBlockedby is instead.
1090		}
1091
1092		# Proxy blocking
1093		if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
1094			# Local list
1095			if ( wfIsLocallyBlockedProxy( $ip ) ) {
1096				$this->mBlockedby = wfMsg( 'proxyblocker' );
1097				$this->mBlockreason = wfMsg( 'proxyblockreason' );
1098			}
1099
1100			# DNSBL
1101			if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
1102				if ( $this->inSorbsBlacklist( $ip ) ) {
1103					$this->mBlockedby = wfMsg( 'sorbs' );
1104					$this->mBlockreason = wfMsg( 'sorbsreason' );
1105				}
1106			}
1107		}
1108
1109		# Extensions
1110		wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1111
1112		wfProfileOut( __METHOD__ );
1113	}
1114
1115	/**
1116	 * Whether the given IP is in the SORBS blacklist.
1117	 *
1118	 * @param $ip \string IP to check
1119	 * @return \bool True if blacklisted.
1120	 */
1121	function inSorbsBlacklist( $ip ) {
1122		global $wgEnableSorbs, $wgSorbsUrl;
1123
1124		return $wgEnableSorbs &&
1125			$this->inDnsBlacklist( $ip, $wgSorbsUrl );
1126	}
1127
1128	/**
1129	 * Whether the given IP is in a given DNS blacklist.
1130	 *
1131	 * @param $ip \string IP to check
1132	 * @param $base \string URL of the DNS blacklist
1133	 * @return \bool True if blacklisted.
1134	 */
1135	function inDnsBlacklist( $ip, $base ) {
1136		wfProfileIn( __METHOD__ );
1137
1138		$found = false;
1139		$host = '';
1140		// FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
1141		if( IP::isIPv4($ip) ) {
1142			# Make hostname
1143			$host = "$ip.$base";
1144
1145			# Send query
1146			$ipList = gethostbynamel( $host );
1147
1148			if( $ipList ) {
1149				wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1150				$found = true;
1151			} else {
1152				wfDebug( "Requested $host, not found in $base.\n" );
1153			}
1154		}
1155
1156		wfProfileOut( __METHOD__ );
1157		return $found;
1158	}
1159
1160	/**
1161	 * Is this user subject to rate limiting?
1162	 *
1163	 * @return \bool True if rate limited
1164	 */
1165	public function isPingLimitable() {
1166		global $wgRateLimitsExcludedGroups;
1167		global $wgRateLimitsExcludedIPs;
1168		if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1169			// Deprecated, but kept for backwards-compatibility config
1170			return false;
1171		}
1172		if( in_array( wfGetIP(), $wgRateLimitsExcludedIPs ) ) {
1173			// No other good way currently to disable rate limits
1174			// for specific IPs. :P
1175			// But this is a crappy hack and should die.
1176			return false;
1177		}
1178		return !$this->isAllowed('noratelimit');
1179	}
1180
1181	/**
1182	 * Primitive rate limits: enforce maximum actions per time period
1183	 * to put a brake on flooding.
1184	 *
1185	 * @note When using a shared cache like memcached, IP-address
1186	 * last-hit counters will be shared across wikis.
1187	 *
1188	 * @param $action \string Action to enforce; 'edit' if unspecified
1189	 * @return \bool True if a rate limiter was tripped
1190	 */
1191	function pingLimiter( $action='edit' ) {
1192
1193		# Call the 'PingLimiter' hook
1194		$result = false;
1195		if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1196			return $result;
1197		}
1198
1199		global $wgRateLimits;
1200		if( !isset( $wgRateLimits[$action] ) ) {
1201			return false;
1202		}
1203
1204		# Some groups shouldn't trigger the ping limiter, ever
1205		if( !$this->isPingLimitable() )
1206			return false;
1207
1208		global $wgMemc, $wgRateLimitLog;
1209		wfProfileIn( __METHOD__ );
1210
1211		$limits = $wgRateLimits[$action];
1212		$keys = array();
1213		$id = $this->getId();
1214		$ip = wfGetIP();
1215		$userLimit = false;
1216
1217		if( isset( $limits['anon'] ) && $id == 0 ) {
1218			$keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1219		}
1220
1221		if( isset( $limits['user'] ) && $id != 0 ) {
1222			$userLimit = $limits['user'];
1223		}
1224		if( $this->isNewbie() ) {
1225			if( isset( $limits['newbie'] ) && $id != 0 ) {
1226				$keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1227			}
1228			if( isset( $limits['ip'] ) ) {
1229				$keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1230			}
1231			$matches = array();
1232			if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1233				$subnet = $matches[1];
1234				$keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1235			}
1236		}
1237		// Check for group-specific permissions
1238		// If more than one group applies, use the group with the highest limit
1239		foreach ( $this->getGroups() as $group ) {
1240			if ( isset( $limits[$group] ) ) {
1241				if ( $userLimit === false || $limits[$group] > $userLimit ) {
1242					$userLimit = $limits[$group];
1243				}
1244			}
1245		}
1246		// Set the user limit key
1247		if ( $userLimit !== false ) {
1248			wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
1249			$keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1250		}
1251
1252		$triggered = false;
1253		foreach( $keys as $key => $limit ) {
1254			list( $max, $period ) = $limit;
1255			$summary = "(limit $max in {$period}s)";
1256			$count = $wgMemc->get( $key );
1257			if( $count ) {
1258				if( $count > $max ) {
1259					wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1260					if( $wgRateLimitLog ) {
1261						@error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1262					}
1263					$triggered = true;
1264				} else {
1265					wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1266				}
1267			} else {
1268				wfDebug( __METHOD__.": adding record for $key $summary\n" );
1269				$wgMemc->add( $key, 1, intval( $period ) );
1270			}
1271			$wgMemc->incr( $key );
1272		}
1273
1274		wfProfileOut( __METHOD__ );
1275		return $triggered;
1276	}
1277
1278	/**
1279	 * Check if user is blocked
1280	 * 
1281	 * @param $bFromSlave \bool Whether to check the slave database instead of the master
1282	 * @return \bool True if blocked, false otherwise
1283	 */
1284	function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1285		wfDebug( "User::isBlocked: enter\n" );
1286		$this->getBlockedStatus( $bFromSlave );
1287		return $this->mBlockedby !== 0;
1288	}
1289
1290	/**
1291	 * Check if user is blocked from editing a particular article
1292	 * 
1293	 * @param $title      \string Title to check
1294	 * @param $bFromSlave \bool   Whether to check the slave database instead of the master
1295	 * @return \bool True if blocked, false otherwise
1296	 */
1297	function isBlockedFrom( $title, $bFromSlave = false ) {
1298		global $wgBlockAllowsUTEdit;
1299		wfProfileIn( __METHOD__ );
1300		wfDebug( __METHOD__.": enter\n" );
1301
1302		wfDebug( __METHOD__.": asking isBlocked()\n" );
1303		$blocked = $this->isBlocked( $bFromSlave );
1304		$allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
1305		# If a user's name is suppressed, they cannot make edits anywhere
1306		if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1307		  $title->getNamespace() == NS_USER_TALK ) {
1308			$blocked = false;
1309			wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1310		}
1311		wfProfileOut( __METHOD__ );
1312		return $blocked;
1313	}
1314
1315	/**
1316	 * If user is blocked, return the name of the user who placed the block
1317	 * @return \string name of blocker
1318	 */
1319	function blockedBy() {
1320		$this->getBlockedStatus();
1321		return $this->mBlockedby;
1322	}
1323
1324	/**
1325	 * If user is blocked, return the specified reason for the block
1326	 * @return \string Blocking reason
1327	 */
1328	function blockedFor() {
1329		$this->getBlockedStatus();
1330		return $this->mBlockreason;
1331	}
1332	
1333	/**
1334	 * If user is blocked, return the ID for the block
1335	 * @return \int Block ID
1336	 */
1337	function getBlockId() {
1338		$this->getBlockedStatus();
1339		return ($this->mBlock ? $this->mBlock->mId : false);
1340	}
1341	
1342	/**
1343	 * Check if user is blocked on all wikis.
1344	 * Do not use for actual edit permission checks!
1345	 * This is intented for quick UI checks.
1346	 * 
1347	 * @param $ip \type{\string} IP address, uses current client if none given
1348	 * @return \type{\bool} True if blocked, false otherwise
1349	 */
1350	function isBlockedGlobally( $ip = '' ) {
1351		if( $this->mBlockedGlobally !== null ) {
1352			return $this->mBlockedGlobally;
1353		}
1354		// User is already an IP?
1355		if( IP::isIPAddress( $this->getName() ) ) {
1356			$ip = $this->getName();
1357		} else if( !$ip ) {
1358			$ip = wfGetIP();
1359		}
1360		$blocked = false;
1361		wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1362		$this->mBlockedGlobally = (bool)$blocked;
1363		return $this->mBlockedGlobally;
1364	}
1365	
1366	/**
1367	 * Check if user account is locked
1368	 * 
1369	 * @return \type{\bool} True if locked, false otherwise
1370	 */
1371	function isLocked() {
1372		if( $this->mLocked !== null ) {
1373			return $this->mLocked;
1374		}
1375		global $wgAuth;
1376		$authUser = $wgAuth->getUserInstance( $this );
1377		$this->mLocked = (bool)$authUser->isLocked();
1378		return $this->mLocked;
1379	}
1380	
1381	/**
1382	 * Check if user account is hidden
1383	 * 
1384	 * @return \type{\bool} True if hidden, false otherwise
1385	 */
1386	function isHidden() {
1387		if( $this->mHideName !== null ) {
1388			return $this->mHideName;
1389		}
1390		$this->getBlockedStatus();
1391		if( !$this->mHideName ) {
1392			global $wgAuth;
1393			$authUser = $wgAuth->getUserInstance( $this );
1394			$this->mHideName = (bool)$authUser->isHidden();
1395		}
1396		return $this->mHideName;
1397	}
1398
1399	/**
1400	 * Get the user's ID.
1401	 * @return \int The user's ID; 0 if the user is anonymous or nonexistent
1402	 */
1403	function getId() {
1404		if( $this->mId === null and $this->mName !== null
1405		and User::isIP( $this->mName ) ) {
1406			// Special case, we know the user is anonymous
1407			return 0;
1408		} elseif( $this->mId === null ) {
1409			// Don't load if this was initialized from an ID
1410			$this->load();
1411		}
1412		return $this->mId;
1413	}
1414
1415	/**
1416	 * Set the user and reload all fields according to a given ID
1417	 * @param $v \int User ID to reload
1418	 */
1419	function setId( $v ) {
1420		$this->mId = $v;
1421		$this->clearInstanceCache( 'id' );
1422	}
1423
1424	/**
1425	 * Get the user name, or the IP of an anonymous user
1426	 * @return \string User's name or IP address
1427	 */
1428	function getName() {
1429		if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1430			# Special case optimisation
1431			return $this->mName;
1432		} else {
1433			$this->load();
1434			if ( $this->mName === false ) {
1435				# Clean up IPs
1436				$this->mName = IP::sanitizeIP( wfGetIP() );
1437			}
1438			return $this->mName;
1439		}
1440	}
1441
1442	/**
1443	 * Set the user name.
1444	 *
1445	 * This does not reload fields from the database according to the given
1446	 * name. Rather, it is used to create a temporary "nonexistent user" for
1447	 * later addition to the database. It can also be used to set the IP
1448	 * address for an anonymous user to something other than the current
1449	 * remote IP.
1450	 *
1451	 * @note User::newFromName() has rougly the same function, when the named user
1452	 * does not exist.
1453	 * @param $str \string New user name to set
1454	 */
1455	function setName( $str ) {
1456		$this->load();
1457		$this->mName = $str;
1458	}
1459
1460	/**
1461	 * Get the user's name escaped by underscores.
1462	 * @return \string Username escaped by underscores.
1463	 */
1464	function getTitleKey() {
1465		return str_replace( ' ', '_', $this->getName() );
1466	}
1467
1468	/**
1469	 * Check if the user has new messages.
1470	 * @return \bool True if the user has new messages
1471	 */
1472	function getNewtalk() {
1473		$this->load();
1474
1475		# Load the newtalk status if it is unloaded (mNewtalk=-1)
1476		if( $this->mNewtalk === -1 ) {
1477			$this->mNewtalk = false; # reset talk page status
1478
1479			# Check memcached separately for anons, who have no
1480			# entire User object stored in there.
1481			if( !$this->mId ) {
1482				global $wgMemc;
1483				$key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1484				$newtalk = $wgMemc->get( $key );
1485				if( strval( $newtalk ) !== '' ) {
1486					$this->mNewtalk = (bool)$newtalk;
1487				} else {
1488					// Since we are caching this, make sure it is up to date by getting it
1489					// from the master
1490					$this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1491					$wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1492				}
1493			} else {
1494				$this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1495			}
1496		}
1497
1498		return (bool)$this->mNewtalk;
1499	}
1500
1501	/**
1502	 * Return the talk page(s) this user has new messages on.
1503	 * @return \type{\arrayof{\string}} Array of page URLs
1504	 */
1505	function getNewMessageLinks() {
1506		$talks = array();
1507		if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1508			return $talks;
1509
1510		if (!$this->getNewtalk())
1511			return array();
1512		$up = $this->getUserPage();
1513		$utp = $up->getTalkPage();
1514		return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1515	}
1516
1517
1518	/**
1519	 * Internal uncached check for new messages
1520	 *
1521	 * @see getNewtalk()
1522	 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1523	 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1524	 * @param $fromMaster \bool true to fetch from the master, false for a slave
1525	 * @return \bool True if the user has new messages
1526	 * @private
1527	 */
1528	function checkNewtalk( $field, $id, $fromMaster = false ) {
1529		if ( $fromMaster ) {
1530			$db = wfGetDB( DB_MASTER );
1531		} else {
1532			$db = wfGetDB( DB_SLAVE );
1533		}
1534		$ok = $db->selectField( 'user_newtalk', $field,
1535			array( $field => $id ), __METHOD__ );
1536		return $ok !== false;
1537	}
1538
1539	/**
1540	 * Add or update the new messages flag
1541	 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1542	 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1543	 * @return \bool True if successful, false otherwise
1544	 * @private
1545	 */
1546	function updateNewtalk( $field, $id ) {
1547		$dbw = wfGetDB( DB_MASTER );
1548		$dbw->insert( 'user_newtalk',
1549			array( $field => $id ),
1550			__METHOD__,
1551			'IGNORE' );
1552		if ( $dbw->affectedRows() ) {
1553			wfDebug( __METHOD__.": set on ($field, $id)\n" );
1554			return true;
1555		} else {
1556			wfDebug( __METHOD__." already set ($field, $id)\n" );
1557			return false;
1558		}
1559	}
1560
1561	/**
1562	 * Clear the new messages flag for the given user
1563	 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1564	 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1565	 * @return \bool True if successful, false otherwise
1566	 * @private
1567	 */
1568	function deleteNewtalk( $field, $id ) {
1569		$dbw = wfGetDB( DB_MASTER );
1570		$dbw->delete( 'user_newtalk',
1571			array( $field => $id ),
1572			__METHOD__ );
1573		if ( $dbw->affectedRows() ) {
1574			wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1575			return true;
1576		} else {
1577			wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1578			return false;
1579		}
1580	}
1581
1582	/**
1583	 * Update the 'You have new messages!' status.
1584	 * @param $val \bool Whether the user has new messages
1585	 */
1586	function setNewtalk( $val ) {
1587		if( wfReadOnly() ) {
1588			return;
1589		}
1590
1591		$this->load();
1592		$this->mNewtalk = $val;
1593
1594		if( $this->isAnon() ) {
1595			$field = 'user_ip';
1596			$id = $this->getName();
1597		} else {
1598			$field = 'user_id';
1599			$id = $this->getId();
1600		}
1601		global $wgMemc;
1602
1603		if( $val ) {
1604			$changed = $this->updateNewtalk( $field, $id );
1605		} else {
1606			$changed = $this->deleteNewtalk( $field, $id );
1607		}
1608
1609		if( $this->isAnon() ) {
1610			// Anons have a separate memcached space, since
1611			// user records aren't kept for them.
1612			$key = wfMemcKey( 'newtalk', 'ip', $id );
1613			$wgMemc->set( $key, $val ? 1 : 0, 1800 );
1614		}
1615		if ( $changed ) {
1616			$this->invalidateCache();
1617		}
1618	}
1619
1620	/**
1621	 * Generate a current or new-future timestamp to be stored in the
1622	 * user_touched field when we update things.
1623	 * @return \string Timestamp in TS_MW format
1624	 */
1625	private static function newTouchedTimestamp() {
1626		global $wgClockSkewFudge;
1627		return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1628	}
1629
1630	/**
1631	 * Clear user data from memcached.
1632	 * Use after applying fun updates to the database; caller's
1633	 * responsibility to update user_touched if appropriate.
1634	 *
1635	 * Called implicitly from invalidateCache() and saveSettings().
1636	 */
1637	private function clearSharedCache() {
1638		$this->load();
1639		if( $this->mId ) {
1640			global $wgMemc;
1641			$wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1642		}
1643	}
1644
1645	/**
1646	 * Immediately touch the user data cache for this account.
1647	 * Updates user_touched field, and removes account data from memcached
1648	 * for reload on the next hit.
1649	 */
1650	function invalidateCache() {
1651		$this->load();
1652		if( $this->mId ) {
1653			$this->mTouched = self::newTouchedTimestamp();
1654
1655			$dbw = wfGetDB( DB_MASTER );
1656			$dbw->update( 'user',
1657				array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1658				array( 'user_id' => $this->mId ),
1659				__METHOD__ );
1660
1661			$this->clearSharedCache();
1662		}
1663	}
1664
1665	/**
1666	 * Validate the cache for this account.
1667	 * @param $timestamp \string A timestamp in TS_MW format
1668	 */
1669	function validateCache( $timestamp ) {
1670		$this->load();
1671		return ($timestamp >= $this->mTouched);
1672	}
1673
1674	/**
1675	 * Get the user touched timestamp
1676	 */
1677	function getTouched() {
1678		$this->load();
1679		return $this->mTouched;
1680	}
1681
1682	/**
1683	 * Set the password and reset the random token.
1684	 * Calls through to authentication plugin if necessary;
1685	 * will have no effect if the auth plugin refuses to
1686	 * pass the change through or if the legal password
1687	 * checks fail.
1688	 *
1689	 * As a special case, setting the password to null
1690	 * wipes it, so the account cannot be logged in until
1691	 * a new password is set, for instance via e-mail.
1692	 *
1693	 * @param $str \string New password to set
1694	 * @throws PasswordError on failure
1695	 */
1696	function setPassword( $str ) {
1697		global $wgAuth;
1698
1699		if( $str !== null ) {
1700			if( !$wgAuth->allowPasswordChange() ) {
1701				throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1702			}
1703
1704			if( !$this->isValidPassword( $str ) ) {
1705				global $wgMinimalPasswordLength;
1706				throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
1707					$wgMinimalPasswordLength ) );
1708			}
1709		}
1710
1711		if( !$wgAuth->setPassword( $this, $str ) ) {
1712			throw new PasswordError( wfMsg( 'externaldberror' ) );
1713		}
1714
1715		$this->setInternalPassword( $str );
1716
1717		return true;
1718	}
1719
1720	/**
1721	 * Set the password and reset the random token unconditionally.
1722	 *
1723	 * @param $str \string New password to set
1724	 */
1725	function setInternalPassword( $str ) {
1726		$this->load();
1727		$this->setToken();
1728
1729		if( $str === null ) {
1730			// Save an invalid hash...
1731			$this->mPassword = '';
1732		} else {
1733			$this->mPassword = self::crypt( $str );
1734		}
1735		$this->mNewpassword = '';
1736		$this->mNewpassTime = null;
1737	}
1738	
1739	/**
1740	 * Get the user's current token.
1741	 * @return \string Token
1742	 */
1743	function getToken() {
1744		$this->load();
1745		return $this->mToken;
1746	}
1747	
1748	/**
1749	 * Set the random token (used for persistent authentication)
1750	 * Called from loadDefaults() among other places.
1751	 *
1752	 * @param $token \string If specified, set the token to this value
1753	 * @private
1754	 */
1755	function setToken( $token = false ) {
1756		global $wgSecretKey, $wgProxyKey;
1757		$this->load();
1758		if ( !$token ) {
1759			if ( $wgSecretKey ) {
1760				$key = $wgSecretKey;
1761			} elseif ( $wgProxyKey ) {
1762				$key = $wgProxyKey;
1763			} else {
1764				$key = microtime();
1765			}
1766			$this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1767		} else {
1768			$this->mToken = $token;
1769		}
1770	}
1771	
1772	/**
1773	 * Set the cookie password
1774	 *
1775	 * @param $str \string New cookie password
1776	 * @private
1777	 */
1778	function setCookiePassword( $str ) {
1779		$this->load();
1780		$this->mCookiePassword = md5( $str );
1781	}
1782
1783	/**
1784	 * Set the password for a password reminder or new account email
1785	 *
1786	 * @param $str \string New password to set
1787	 * @param $throttle \bool If true, reset the throttle timestamp to the present
1788	 */
1789	function setNewpassword( $str, $throttle = true ) {
1790		$this->load();
1791		$this->mNewpassword = self::crypt( $str );
1792		if ( $throttle ) {
1793			$this->mNewpassTime = wfTimestampNow();
1794		}
1795	}
1796
1797	/**
1798	 * Has password reminder email been sent within the last 
1799	 * $wgPasswordReminderResendTime hours?
1800	 * @return \bool True or false
1801	 */
1802	function isPasswordReminderThrottled() {
1803		global $wgPasswordReminderResendTime;
1804		$this->load();
1805		if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1806			return false;
1807		}
1808		$expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1809		return time() < $expiry;
1810	}
1811
1812	/**
1813	 * Get the user's e-mail address
1814	 * @return \string User's email address
1815	 */
1816	function getEmail() {
1817		$this->load();
1818		wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1819		return $this->mEmail;
1820	}
1821
1822	/**
1823	 * Get the timestamp of the user's e-mail authentication
1824	 * @return \string TS_MW timestamp
1825	 */
1826	function getEmailAuthenticationTimestamp() {
1827		$this->load();
1828		wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1829		return $this->mEmailAuthenticated;
1830	}
1831
1832	/**
1833	 * Set the user's e-mail address
1834	 * @param $str \string New e-mail address
1835	 */
1836	function setEmail( $str ) {
1837		$this->load();
1838		$this->mEmail = $str;
1839		wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1840	}
1841
1842	/**
1843	 * Get the user's real name
1844	 * @return \string User's real name
1845	 */
1846	function getRealName() {
1847		$this->load();
1848		return $this->mRealName;
1849	}
1850
1851	/**
1852	 * Set the user's real name
1853	 * @param $str \string New real name
1854	 */
1855	function setRealName( $str ) {
1856		$this->load();
1857		

Large files files are truncated, but you can click here to view the full file