PageRenderTime 78ms CodeModel.GetById 3ms app.highlight 55ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/User.php

https://github.com/tav/confluence
PHP | 3404 lines | 2066 code | 280 blank | 1058 comment | 307 complexity | 8653b2a9a6500407554f9532e5a27a41 MD5 | raw 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		$this->mRealName = $str;
1858	}
1859
1860	/**
1861	 * Get the user's current setting for a given option.
1862	 *
1863	 * @param $oname \string The option to check
1864	 * @param $defaultOverride \string A default value returned if the option does not exist
1865	 * @return \string User's current value for the option
1866	 * @see getBoolOption()
1867	 * @see getIntOption()
1868	 */
1869	function getOption( $oname, $defaultOverride = '' ) {
1870		$this->load();
1871
1872		if ( is_null( $this->mOptions ) ) {
1873			if($defaultOverride != '') {
1874				return $defaultOverride;
1875			}
1876			$this->mOptions = User::getDefaultOptions();
1877		}
1878
1879		if ( array_key_exists( $oname, $this->mOptions ) ) {
1880			return trim( $this->mOptions[$oname] );
1881		} else {
1882			return $defaultOverride;
1883		}
1884	}
1885	
1886	/**
1887	 * Get the user's current setting for a given option, as a boolean value.
1888	 *
1889	 * @param $oname \string The option to check
1890	 * @return \bool User's current value for the option
1891	 * @see getOption()
1892	 */
1893	function getBoolOption( $oname ) {
1894		return (bool)$this->getOption( $oname );
1895	}
1896
1897	
1898	/**
1899	 * Get the user's current setting for a given option, as a boolean value.
1900	 *
1901	 * @param $oname \string The option to check
1902	 * @param $defaultOverride \int A default value returned if the option does not exist
1903	 * @return \int User's current value for the option
1904	 * @see getOption()
1905	 */
1906	function getIntOption( $oname, $defaultOverride=0 ) {
1907		$val = $this->getOption( $oname );
1908		if( $val == '' ) {
1909			$val = $defaultOverride;
1910		}
1911		return intval( $val );
1912	}
1913
1914	/**
1915	 * Set the given option for a user.
1916	 *
1917	 * @param $oname \string The option to set
1918	 * @param $val \mixed New value to set
1919	 */
1920	function setOption( $oname, $val ) {
1921		$this->load();
1922		if ( is_null( $this->mOptions ) ) {
1923			$this->mOptions = User::getDefaultOptions();
1924		}
1925		if ( $oname == 'skin' ) {
1926			# Clear cached skin, so the new one displays immediately in Special:Preferences
1927			unset( $this->mSkin );
1928		}
1929		// Filter out any newlines that may have passed through input validation.
1930		// Newlines are used to separate items in the options blob.
1931		if( $val ) {
1932			$val = str_replace( "\r\n", "\n", $val );
1933			$val = str_replace( "\r", "\n", $val );
1934			$val = str_replace( "\n", " ", $val );
1935		}
1936		// Explicitly NULL values should refer to defaults
1937		global $wgDefaultUserOptions;
1938		if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
1939			$val = $wgDefaultUserOptions[$oname];
1940		}
1941		$this->mOptions[$oname] = $val;
1942	}
1943	
1944	/**
1945	 * Reset all options to the site defaults
1946	 */	
1947	function restoreOptions() {
1948		$this->mOptions = User::getDefaultOptions();
1949	}
1950
1951	/**
1952	 * Get the user's preferred date format.
1953	 * @return \string User's preferred date format
1954	 */
1955	function getDatePreference() {
1956		// Important migration for old data rows
1957		if ( is_null( $this->mDatePreference ) ) {
1958			global $wgLang;
1959			$value = $this->getOption( 'date' );
1960			$map = $wgLang->getDatePreferenceMigrationMap();
1961			if ( isset( $map[$value] ) ) {
1962				$value = $map[$value];
1963			}
1964			$this->mDatePreference = $value;
1965		}
1966		return $this->mDatePreference;
1967	}
1968
1969	/**
1970	 * Get the permissions this user has.
1971	 * @return \type{\arrayof{\string}} Array of permission names
1972	 */
1973	function getRights() {
1974		if ( is_null( $this->mRights ) ) {
1975			$this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1976			wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
1977			// Force reindexation of rights when a hook has unset one of them
1978			$this->mRights = array_values( $this->mRights );
1979		}
1980		return $this->mRights;
1981	}
1982
1983	/**
1984	 * Get the list of explicit group memberships this user has.
1985	 * The implicit * and user groups are not included.
1986	 * @return \type{\arrayof{\string}} Array of internal group names
1987	 */
1988	function getGroups() {
1989		$this->load();
1990		return $this->mGroups;
1991	}
1992
1993	/**
1994	 * Get the list of implicit group memberships this user has.
1995	 * This includes all explicit groups, plus 'user' if logged in,
1996	 * '*' for all accounts and autopromoted groups
1997	 * @param $recache \bool Whether to avoid the cache
1998	 * @return \type{\arrayof{\string}} Array of internal group names
1999	 */
2000	function getEffectiveGroups( $recache = false ) {
2001		if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2002			$this->mEffectiveGroups = $this->getGroups();
2003			$this->mEffectiveGroups[] = '*';
2004			if( $this->getId() ) {
2005				$this->mEffectiveGroups[] = 'user';
2006
2007				$this->mEffectiveGroups = array_unique( array_merge(
2008					$this->mEffectiveGroups,
2009					Autopromote::getAutopromoteGroups( $this )
2010				) );
2011
2012				# Hook for additional groups
2013				wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2014			}
2015		}
2016		return $this->mEffectiveGroups;
2017	}
2018
2019	/**
2020	 * Get the user's edit count.
2021	 * @return \int User'e edit count
2022	 */
2023	function getEditCount() {
2024		if ($this->getId()) {
2025			if ( !isset( $this->mEditCount ) ) {
2026				/* Populate the count, if it has not been populated yet */
2027				$this->mEditCount = User::edits($this->mId);
2028			}
2029			return $this->mEditCount;
2030		} else {
2031			/* nil */
2032			return null;
2033		}
2034	}
2035
2036	/**
2037	 * Add the user to the given group.
2038	 * This takes immediate effect.
2039	 * @param $group \string Name of the group to add
2040	 */
2041	function addGroup( $group ) {
2042		$dbw = wfGetDB( DB_MASTER );
2043		if( $this->getId() ) {
2044			$dbw->insert( 'user_groups',
2045				array(
2046					'ug_user'  => $this->getID(),
2047					'ug_group' => $group,
2048				),
2049				'User::addGroup',
2050				array( 'IGNORE' ) );
2051		}
2052
2053		$this->loadGroups();
2054		$this->mGroups[] = $group;
2055		$this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2056
2057		$this->invalidateCache();
2058	}
2059
2060	/**
2061	 * Remove the user from the given group.
2062	 * This takes immediate effect.
2063	 * @param $group \string Name of the group to remove
2064	 */
2065	function removeGroup( $group ) {
2066		$this->load();
2067		$dbw = wfGetDB( DB_MASTER );
2068		$dbw->delete( 'user_groups',
2069			array(
2070				'ug_user'  => $this->getID(),
2071				'ug_group' => $group,
2072			),
2073			'User::removeGroup' );
2074
2075		$this->loadGroups();
2076		$this->mGroups = array_diff( $this->mGroups, array( $group ) );
2077		$this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2078
2079		$this->invalidateCache();
2080	}
2081
2082
2083	/**
2084	 * Get whether the user is logged in
2085	 * @return \bool True or false
2086	 */
2087	function isLoggedIn() {
2088		return $this->getID() != 0;
2089	}
2090
2091	/**
2092	 * Get whether the user is anonymous
2093	 * @return \bool True or false
2094	 */
2095	function isAnon() {
2096		return !$this->isLoggedIn();
2097	}
2098
2099	/**
2100	 * Get whether the user is a bot
2101	 * @return \bool True or false
2102	 * @deprecated
2103	 */
2104	function isBot() {
2105		wfDeprecated( __METHOD__ );
2106		return $this->isAllowed( 'bot' );
2107	}
2108
2109	/**
2110	 * Check if user is allowed to access a feature / make an action
2111	 * @param $action \string action to be checked
2112	 * @return \bool True if action is allowed, else false
2113	 */
2114	function isAllowed( $action = '' ) {
2115		if ( $action === '' )
2116			return true; // In the spirit of DWIM
2117		# Patrolling may not be enabled
2118		if( $action === 'patrol' || $action === 'autopatrol' ) {
2119			global $wgUseRCPatrol, $wgUseNPPatrol;
2120			if( !$wgUseRCPatrol && !$wgUseNPPatrol )
2121				return false;
2122		}
2123		# Use strict parameter to avoid matching numeric 0 accidentally inserted 
2124		# by misconfiguration: 0 == 'foo'
2125		return in_array( $action, $this->getRights(), true );
2126	}
2127
2128	/**
2129	* Check whether to enable recent changes patrol features for this user
2130	* @return \bool True or false
2131	*/
2132	public function useRCPatrol() {
2133		global $wgUseRCPatrol;
2134		return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2135	}
2136
2137	/**
2138	* Check whether to enable new pages patrol features for this user
2139	* @return \bool True or false
2140	*/
2141	public function useNPPatrol() {
2142		global $wgUseRCPatrol, $wgUseNPPatrol;
2143		return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2144	}
2145
2146	/**
2147	 * Get the current skin, loading it if required
2148	 * @return \type{Skin} Current skin
2149	 * @todo FIXME : need to check the old failback system [AV]
2150	 */
2151	function &getSkin() {
2152		global $wgRequest, $wgAllowUserSkin, $wgDefaultSkin;
2153		if ( ! isset( $this->mSkin ) ) {
2154			wfProfileIn( __METHOD__ );
2155
2156			if( $wgAllowUserSkin ) {
2157				# get the user skin
2158				$userSkin = $this->getOption( 'skin' );
2159				$userSkin = $wgRequest->getVal('useskin', $userSkin);
2160			} else {
2161				# if we're not allowing users to override, then use the default
2162				$userSkin = $wgDefaultSkin;
2163			}
2164			
2165			$this->mSkin =& Skin::newFromKey( $userSkin );
2166			wfProfileOut( __METHOD__ );
2167		}
2168		return $this->mSkin;
2169	}
2170
2171	/**
2172	 * Check the watched status of an article.
2173	 * @param $title \type{Title} Title of the article to look at
2174	 * @return \bool True if article is watched
2175	 */
2176	function isWatched( $title ) {
2177		$wl = WatchedItem::fromUserTitle( $this, $title );
2178		return $wl->isWatched();
2179	}
2180
2181	/**
2182	 * Watch an article.
2183	 * @param $title \type{Title} Title of the article to look at
2184	 */
2185	function addWatch( $title ) {
2186		$wl = WatchedItem::fromUserTitle( $this, $title );
2187		$wl->addWatch();
2188		$this->invalidateCache();
2189	}
2190
2191	/**
2192	 * Stop watching an article.
2193	 * @param $title \type{Title} Title of the article to look at
2194	 */
2195	function removeWatch( $title ) {
2196		$wl = WatchedItem::fromUserTitle( $this, $title );
2197		$wl->removeWatch();
2198		$this->invalidateCache();
2199	}
2200
2201	/**
2202	 * Clear the user's notification timestamp for the given title.
2203	 * If e-notif e-mails are on, they will receive notification mails on
2204	 * the next change of the page if it's watched etc.
2205	 * @param $title \type{Title} Title of the article to look at
2206	 */
2207	function clearNotification( &$title ) {
2208		global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2209
2210		# Do nothing if the database is locked to writes
2211		if( wfReadOnly() ) {
2212			return;
2213		}
2214
2215		if ($title->getNamespace() == NS_USER_TALK &&
2216			$title->getText() == $this->getName() ) {
2217			if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
2218				return;
2219			$this->setNewtalk( false );
2220		}
2221
2222		if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2223			return;
2224		}
2225
2226		if( $this->isAnon() ) {
2227			// Nothing else to do...
2228			return;
2229		}
2230
2231		// Only update the timestamp if the page is being watched.
2232		// The query to find out if it is watched is cached both in memcached and per-invocation,
2233		// and when it does have to be executed, it can be on a slave
2234		// If this is the user's newtalk page, we always update the timestamp
2235		if ($title->getNamespace() == NS_USER_TALK &&
2236			$title->getText() == $wgUser->getName())
2237		{
2238			$watched = true;
2239		} elseif ( $this->getId() == $wgUser->getId() ) {
2240			$watched = $title->userIsWatching();
2241		} else {
2242			$watched = true;
2243		}
2244
2245		// If the page is watched by the user (or may be watched), update the timestamp on any
2246		// any matching rows
2247		if ( $watched ) {
2248			$dbw = wfGetDB( DB_MASTER );
2249			$dbw->update( 'watchlist',
2250					array( /* SET */
2251						'wl_notificationtimestamp' => NULL
2252					), array( /* WHERE */
2253						'wl_title' => $title->getDBkey(),
2254						'wl_namespace' => $title->getNamespace(),
2255						'wl_user' => $this->getID()
2256					), __METHOD__
2257			);
2258		}
2259	}
2260
2261	/**
2262	 * Resets all of the given user's page-change notification timestamps.
2263	 * If e-notif e-mails are on, they will receive notification mails on
2264	 * the next change of any watched page.
2265	 *
2266	 * @param $currentUser \int User ID
2267	 */
2268	function clearAllNotifications( $currentUser ) {
2269		global $wgUseEnotif, $wgShowUpdatedMarker;
2270		if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2271			$this->setNewtalk( false );
2272			return;
2273		}
2274		if( $currentUser != 0 )  {
2275			$dbw = wfGetDB( DB_MASTER );
2276			$dbw->update( 'watchlist',
2277				array( /* SET */
2278					'wl_notificationtimestamp' => NULL
2279				), array( /* WHERE */
2280					'wl_user' => $currentUser
2281				), __METHOD__
2282			);
2283		# 	We also need to clear here the "you have new message" notification for the own user_talk page
2284		#	This is cleared one page view later in Article::viewUpdates();
2285		}
2286	}
2287
2288	/**
2289	 * Encode this user's options as a string
2290	 * @return \string Encoded options
2291	 * @private
2292	 */
2293	function encodeOptions() {
2294		$this->load();
2295		if ( is_null( $this->mOptions ) ) {
2296			$this->mOptions = User::getDefaultOptions();
2297		}
2298		$a = array();
2299		foreach ( $this->mOptions as $oname => $oval ) {
2300			array_push( $a, $oname.'='.$oval );
2301		}
2302		$s = implode( "\n", $a );
2303		return $s;
2304	}
2305
2306	/**
2307	 * Set this user's options from an encoded string
2308	 * @param $str \string Encoded options to import
2309	 * @private
2310	 */
2311	function decodeOptions( $str ) {
2312		$this->mOptions = array();
2313		$a = explode( "\n", $str );
2314		foreach ( $a as $s ) {
2315			$m = array();
2316			if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2317				$this->mOptions[$m[1]] = $m[2];
2318			}
2319		}
2320	}
2321	
2322	/**
2323	 * Set a cookie on the user's client. Wrapper for 
2324	 * WebResponse::setCookie
2325	 * @param $name \string Name of the cookie to set
2326	 * @param $value \string Value to set
2327	 * @param $exp \int Expiration time, as a UNIX time value; 
2328	 *                   if 0 or not specified, use the default $wgCookieExpiration
2329	 */
2330	protected function setCookie( $name, $value, $exp=0 ) {
2331		global $wgRequest;
2332		$wgRequest->response()->setcookie( $name, $value, $exp );
2333	}
2334	
2335	/**
2336	 * Clear a cookie on the user's client
2337	 * @param $name \string Name of the cookie to clear
2338	 */
2339	protected function clearCookie( $name ) {
2340		$this->setCookie( $name, '', time() - 86400 );
2341	}
2342
2343	/**
2344	 * Set the default cookies for this session on the user's client.
2345	 */
2346	function setCookies() {
2347		$this->load();
2348		if ( 0 == $this->mId ) return;
2349		$session = array( 
2350			'wsUserID' => $this->mId,
2351			'wsToken' => $this->mToken,
2352			'wsUserName' => $this->getName()
2353		);
2354		$cookies = array(
2355			'UserID' => $this->mId,
2356			'UserName' => $this->getName(),
2357		);
2358		if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2359			$cookies['Token'] = $this->mToken;
2360		} else {
2361			$cookies['Token'] = false;
2362		}
2363		
2364		wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2365		#check for null, since the hook could cause a null value 
2366		if ( !is_null( $session ) && isset( $_SESSION ) ){
2367			$_SESSION = $session + $_SESSION;
2368		}
2369		foreach ( $cookies as $name => $value ) {
2370			if ( $value === false ) {
2371				$this->clearCookie( $name );
2372			} else {
2373				$this->setCookie( $name, $value );
2374			}
2375		}
2376	}
2377
2378	/**
2379	 * Log this user out.
2380	 */
2381	function logout() {
2382		global $wgUser;
2383		if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
2384			$this->doLogout();
2385		}
2386	}
2387
2388	/**
2389	 * Clear the user's cookies and session, and reset the instance cache.
2390	 * @private
2391	 * @see logout()
2392	 */
2393	function doLogout() {
2394		$this->clearInstanceCache( 'defaults' );
2395
2396		$_SESSION['wsUserID'] = 0;
2397
2398		$this->clearCookie( 'UserID' );
2399		$this->clearCookie( 'Token' );
2400
2401		# Remember when user logged out, to prevent seeing cached pages
2402		$this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2403	}
2404
2405	/**
2406	 * Save this user's settings into the database.
2407	 * @todo Only rarely do all these fields need to be set!
2408	 */
2409	function saveSettings() {
2410		$this->load();
2411		if ( wfReadOnly() ) { return; }
2412		if ( 0 == $this->mId ) { return; }
2413
2414		$this->mTouched = self::newTouchedTimestamp();
2415
2416		$dbw = wfGetDB( DB_MASTER );
2417		$dbw->update( 'user',
2418			array( /* SET */
2419				'user_name' => $this->mName,
2420				'user_password' => $this->mPassword,
2421				'user_newpassword' => $this->mNewpassword,
2422				'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2423				'user_real_name' => $this->mRealName,
2424		 		'user_email' => $this->mEmail,
2425		 		'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2426				'user_options' => $this->encodeOptions(),
2427				'user_touched' => $dbw->timestamp($this->mTouched),
2428				'user_token' => $this->mToken,
2429				'user_email_token' => $this->mEmailToken,
2430				'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2431			), array( /* WHERE */
2432				'user_id' => $this->mId
2433			), __METHOD__
2434		);
2435		wfRunHooks( 'UserSaveSettings', array( $this ) );
2436		$this->clearSharedCache();
2437		$this->getUserPage()->invalidateCache();
2438	}
2439
2440	/**
2441	 * If only this user's username is known, and it exists, return the user ID.
2442	 */
2443	function idForName() {
2444		$s = trim( $this->getName() );
2445		if ( $s === '' ) return 0;
2446
2447		$dbr = wfGetDB( DB_SLAVE );
2448		$id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2449		if ( $id === false ) {
2450			$id = 0;
2451		}
2452		return $id;
2453	}
2454
2455	/**
2456	 * Add a user to the database, return the user object
2457	 *
2458	 * @param $name \string Username to add
2459	 * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
2460	 *   - password             The user's password. Password logins will be disabled if this is omitted.
2461	 *   - newpassword          A temporary password mailed to the user
2462	 *   - email                The user's email address
2463	 *   - email_authenticated  The email authentication timestamp
2464	 *   - real_name            The user's real name
2465	 *   - options              An associative array of non-default options
2466	 *   - token                Random authentication token. Do not set.
2467	 *   - registration         Registration timestamp. Do not set.
2468	 *
2469	 * @return \type{User} A new User object, or null if the username already exists
2470	 */
2471	static function createNew( $name, $params = array() ) {
2472		$user = new User;
2473		$user->load();
2474		if ( isset( $params['options'] ) ) {
2475			$user->mOptions = $params['options'] + $user->mOptions;
2476			unset( $params['options'] );
2477		}
2478		$dbw = wfGetDB( DB_MASTER );
2479		$seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2480		$fields = array(
2481			'user_id' => $seqVal,
2482			'user_name' => $name,
2483			'user_password' => $user->mPassword,
2484			'user_newpassword' => $user->mNewpassword,
2485			'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2486			'user_email' => $user->mEmail,
2487			'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2488			'user_real_name' => $user->mRealName,
2489			'user_options' => $user->encodeOptions(),
2490			'user_token' => $user->mToken,
2491			'user_registration' => $dbw->timestamp( $user->mRegistration ),
2492			'user_editcount' => 0,
2493		);
2494		foreach ( $params as $name => $value ) {
2495			$fields["user_$name"] = $value;
2496		}
2497		$dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2498		if ( $dbw->affectedRows() ) {
2499			$newUser = User::newFromId( $dbw->insertId() );
2500		} else {
2501			$newUser = null;
2502		}
2503		return $newUser;
2504	}
2505
2506	/**
2507	 * Add this existing user object to the database
2508	 */
2509	function addToDatabase() {
2510		$this->load();
2511		$dbw = wfGetDB( DB_MASTER );
2512		$seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2513		$dbw->insert( 'user',
2514			array(
2515				'user_id' => $seqVal,
2516				'user_name' => $this->mName,
2517				'user_password' => $this->mPassword,
2518				'user_newpassword' => $this->mNewpassword,
2519				'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2520				'user_email' => $this->mEmail,
2521				'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2522				'user_real_name' => $this->mRealName,
2523				'user_options' => $this->encodeOptions(),
2524				'user_token' => $this->mToken,
2525				'user_registration' => $dbw->timestamp( $this->mRegistration ),
2526				'user_editcount' => 0,
2527			), __METHOD__
2528		);
2529		$this->mId = $dbw->insertId();
2530
2531		// Clear instance cache other than user table data, which is already accurate
2532		$this->clearInstanceCache();
2533	}
2534
2535	/**
2536	 * If this (non-anonymous) user is blocked, block any IP address
2537	 * they've successfully logged in from.
2538	 */
2539	function spreadBlock() {
2540		wfDebug( __METHOD__."()\n" );
2541		$this->load();
2542		if ( $this->mId == 0 ) {
2543			return;
2544		}
2545
2546		$userblock = Block::newFromDB( '', $this->mId );
2547		if ( !$userblock ) {
2548			return;
2549		}
2550
2551		$userblock->doAutoblock( wfGetIp() );
2552
2553	}
2554
2555	/**
2556	 * Generate a string which will be different for any combination of
2557	 * user options which would produce different parser output.
2558	 * This will be used as part of the hash key for the parser cache,
2559	 * so users will the same options can share the same cached data
2560	 * safely.
2561	 *
2562	 * Extensions which require it should install 'PageRenderingHash' hook,
2563	 * which will give them a chance to modify this key based on their own
2564	 * settings.
2565	 *
2566	 * @return \string Page rendering hash
2567	 */
2568	function getPageRenderingHash() {
2569		global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2570		if( $this->mHash ){
2571			return $this->mHash;
2572		}
2573
2574		// stubthreshold is only included below for completeness,
2575		// it will always be 0 when this function is called by parsercache.
2576
2577		$confstr =        $this->getOption( 'math' );
2578		$confstr .= '!' . $this->getOption( 'stubthreshold' );
2579		if ( $wgUseDynamicDates ) {
2580			$confstr .= '!' . $this->getDatePreference();
2581		}
2582		$confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2583		$confstr .= '!' . $wgLang->getCode();
2584		$confstr .= '!' . $this->getOption( 'thumbsize' );
2585		// add in language specific options, if any
2586		$extra = $wgContLang->getExtraHashOptions();
2587		$confstr .= $extra;
2588
2589		$confstr .= $wgRenderHashAppend;
2590
2591		// Give a chance for extensions to modify the hash, if they have
2592		// extra options or other effects on the parser cache.
2593		wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2594
2595		// Make it a valid memcached key fragment
2596		$confstr = str_replace( ' ', '_', $confstr );
2597		$this->mHash = $confstr;
2598		return $confstr;
2599	}
2600
2601	/**
2602	 * Get whether the user is explicitly blocked from account creation.
2603	 * @return \bool True if blocked
2604	 */
2605	function isBlockedFromCreateAccount() {
2606		$this->getBlockedStatus();
2607		return $this->mBlock && $this->mBlock->mCreateAccount;
2608	}
2609
2610	/**
2611	 * Get whether the user is blocked from using Special:Emailuser.
2612	 * @return \bool True if blocked
2613	 */
2614	function isBlockedFromEmailuser() {
2615		$this->getBlockedStatus();
2616		return $this->mBlock && $this->mBlock->mBlockEmail;
2617	}
2618
2619	/**
2620	 * Get whether the user is allowed to create an account.
2621	 * @return \bool True if allowed
2622	 */
2623	function isAllowedToCreateAccount() {
2624		return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2625	}
2626
2627	/**
2628	 * @deprecated
2629	 */
2630	function setLoaded( $loaded ) {
2631		wfDeprecated( __METHOD__ );
2632	}
2633
2634	/**
2635	 * Get this user's personal page title.
2636	 *
2637	 * @return \type{Title} User's personal page title
2638	 */
2639	function getUserPage() {
2640		return Title::makeTitle( NS_USER, $this->getName() );
2641	}
2642
2643	/**
2644	 * Get this user's talk page title.
2645	 *
2646	 * @return \type{Title} User's talk page title
2647	 */
2648	function getTalkPage() {
2649		$title = $this->getUserPage();
2650		return $title->getTalkPage();
2651	}
2652
2653	/**
2654	 * Get the maximum valid user ID.
2655	 * @return \int User ID
2656	 * @static
2657	 */
2658	function getMaxID() {
2659		static $res; // cache
2660
2661		if ( isset( $res ) )
2662			return $res;
2663		else {
2664			$dbr = wfGetDB( DB_SLAVE );
2665			return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2666		}
2667	}
2668
2669	/**
2670	 * Determine whether the user is a newbie. Newbies are either
2671	 * anonymous IPs, or the most recently created accounts.
2672	 * @return \bool True if the user is a newbie
2673	 */
2674	function isNewbie() {
2675		return !$this->isAllowed( 'autoconfirmed' );
2676	}
2677	
2678	/**
2679	 * Is the user active? We check to see if they've made at least
2680	 * X number of edits in the last Y days.
2681	 * 
2682	 * @return \bool True if the user is active, false if not.
2683	 */
2684	public function isActiveEditor() {
2685		global $wgActiveUserEditCount, $wgActiveUserDays;
2686		$dbr = wfGetDB( DB_SLAVE );
2687		
2688		// Stolen without shame from RC
2689		$cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
2690		$cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
2691		$oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
2692		
2693		$res = $dbr->select( 'revision', '1',
2694				array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
2695				__METHOD__,
2696				array('LIMIT' => $wgActiveUserEditCount ) );
2697		
2698		$count = $dbr->numRows($res);
2699		$dbr->freeResult($res);
2700
2701		return $count == $wgActiveUserEditCount;
2702	}
2703
2704	/**
2705	 * Check to see if the given clear-text password is one of the accepted passwords
2706	 * @param $password \string user password.
2707	 * @return \bool True if the given password is correct, otherwise False.
2708	 */
2709	function checkPassword( $password ) {
2710		global $wgAuth;
2711		$this->load();
2712
2713		// Even though we stop people from creating passwords that
2714		// are shorter than this, doesn't mean people wont be able
2715		// to. Certain authentication plugins do NOT want to save
2716		// domain passwords in a mysql database, so we should
2717		// check this (incase $wgAuth->strict() is false).
2718		if( !$this->isValidPassword( $password ) ) {
2719			return false;
2720		}
2721
2722		if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2723			return true;
2724		} elseif( $wgAuth->strict() ) {
2725			/* Auth plugin doesn't allow local authentication */
2726			return false;
2727		} elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2728			/* Auth plugin doesn't allow local authentication for this user name */
2729			return false;
2730		}
2731		if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2732			return true;
2733		} elseif ( function_exists( 'iconv' ) ) {
2734			# Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2735			# Check for this with iconv
2736			$cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2737			if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2738				return true;
2739			}
2740		}
2741		return false;
2742	}
2743
2744	/**
2745	 * Check if the given clear-text password matches the temporary password
2746	 * sent by e-mail for password reset operations.
2747	 * @return \bool True if matches, false otherwise
2748	 */
2749	function checkTemporaryPassword( $plaintext ) {
2750		global $wgNewPasswordExpiry;
2751		if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
2752			$this->load();
2753			$expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
2754			return ( time() < $expiry );
2755		} else {
2756			return false;
2757		}
2758	}
2759
2760	/**
2761	 * Initialize (if necessary) and return a session token value
2762	 * which can be used in edit forms to show that the user's
2763	 * login credentials aren't being hijacked with a foreign form
2764	 * submission.
2765	 *
2766	 * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
2767	 * @return \string The new edit token
2768	 */
2769	function editToken( $salt = '' ) {
2770		if ( $this->isAnon() ) {
2771			return EDIT_TOKEN_SUFFIX;
2772		} else {
2773			if( !isset( $_SESSION['wsEditToken'] ) ) {
2774				$token = self::generateToken();
2775				$_SESSION['wsEditToken'] = $token;
2776			} else {
2777				$token = $_SESSION['wsEditToken'];
2778			}
2779			if( is_array( $salt ) ) {
2780				$salt = implode( '|', $salt );
2781			}
2782			return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2783		}
2784	}
2785
2786	/**
2787	 * Generate a looking random token for various uses.
2788	 *
2789	 * @param $salt \string Optional salt value
2790	 * @return \string The new random token
2791	 */
2792	public static function generateToken( $salt = '' ) {
2793		$token = dechex( mt_rand() ) . dechex( mt_rand() );
2794		return md5( $token . $salt );
2795	}
2796
2797	/**
2798	 * Check given value against the token value stored in the session.
2799	 * A match should confirm that the form was submitted from the
2800	 * user's own login session, not a form submission from a third-party
2801	 * site.
2802	 *
2803	 * @param $val \string Input value to compare
2804	 * @param $salt \string Optional function-specific data for hashing
2805	 * @return \bool Whether the token matches
2806	 */
2807	function matchEditToken( $val, $salt = '' ) {
2808		$sessionToken = $this->editToken( $salt );
2809		if ( $val != $sessionToken ) {
2810			wfDebug( "User::matchEditToken: broken session data\n" );
2811		}
2812		return $val == $sessionToken;
2813	}
2814
2815	/**
2816	 * Check given value against the token value stored in the session,
2817	 * ignoring the suffix.
2818	 *
2819	 * @param $val \string Input value to compare
2820	 * @param $salt \string Optional function-specific data for hashing
2821	 * @return \bool Whether the token matches
2822	 */
2823	function matchEditTokenNoSuffix( $val, $salt = '' ) {
2824		$sessionToken = $this->editToken( $salt );
2825		return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2826	}
2827
2828	/**
2829	 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2830	 * mail to the user's given address.
2831	 *
2832	 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
2833	 */
2834	function sendConfirmationMail() {
2835		global $wgLang;
2836		$expiration = null; // gets passed-by-ref and defined in next line.
2837		$token = $this->confirmationToken( $expiration );
2838		$url = $this->confirmationTokenUrl( $token );
2839		$invalidateURL = $this->invalidationTokenUrl( $token );
2840		$this->saveSettings();
2841		
2842		return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2843			wfMsg( 'confirmemail_body',
2844				wfGetIP(),
2845				$this->getName(),
2846				$url,
2847				$wgLang->timeanddate( $expiration, false ),
2848				$invalidateURL ) );
2849	}
2850
2851	/**
2852	 * Send an e-mail to this user's account. Does not check for
2853	 * confirmed status or validity.
2854	 *
2855	 * @param $subject \string Message subject
2856	 * @param $body \string Message body
2857	 * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
2858	 * @param $replyto \string Reply-To address
2859	 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
2860	 */
2861	function sendMail( $subject, $body, $from = null, $replyto = null ) {
2862		if( is_null( $from ) ) {
2863			global $wgPasswordSender;
2864			$from = $wgPasswordSender;
2865		}
2866
2867		$to = new MailAddress( $this );
2868		$sender = new MailAddress( $from );
2869		return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2870	}
2871
2872	/**
2873	 * Generate, store, and return a new e-mail confirmation code.
2874	 * A hash (unsalted, since it's used as a key) is stored.
2875	 *
2876	 * @note Call saveSettings() after calling this function to commit
2877	 * this change to the database.
2878	 *
2879	 * @param[out] &$expiration \mixed Accepts the expiration time
2880	 * @return \string New token
2881	 * @private
2882	 */
2883	function confirmationToken( &$expiration ) {
2884		$now = time();
2885		$expires = $now + 7 * 24 * 60 * 60;
2886		$expiration = wfTimestamp( TS_MW, $expires );
2887		$token = self::generateToken( $this->mId . $this->mEmail . $expires );
2888		$hash = md5( $token );
2889		$this->load();
2890		$this->mEmailToken = $hash;
2891		$this->mEmailTokenExpires = $expiration;
2892		return $token;
2893	}
2894
2895	/**
2896	* Return a URL the user can use to confirm their email address.
2897	 * @param $token \string Accepts the email confirmation token
2898	 * @return \string New token URL
2899	 * @private
2900	 */
2901	function confirmationTokenUrl( $token ) {
2902		return $this->getTokenUrl( 'ConfirmEmail', $token );
2903	}
2904	/**
2905	 * Return a URL the user can use to invalidate their email address.
2906	 * @param $token \string Accepts the email confirmation token
2907	 * @return \string New token URL
2908	 * @private
2909	 */
2910	function invalidationTokenUrl( $token ) {
2911		return $this->getTokenUrl( 'Invalidateemail', $token );
2912	}
2913	
2914	/**
2915	 * Internal function to format the e-mail validation/invalidation URLs.
2916	 * This uses $wgArticlePath directly as a quickie hack to use the
2917	 * hardcoded English names of the Special: pages, for ASCII safety.
2918	 *
2919	 * @note Since these URLs get dropped directly into emails, using the
2920	 * short English names avoids insanely long URL-encoded links, which
2921	 * also sometimes can get corrupted in some browsers/mailers
2922	 * (bug 6957 with Gmail and Internet Explorer).
2923	 *
2924	 * @param $page \string Special page
2925	 * @param $token \string Token
2926	 * @return \string Formatted URL
2927	 */
2928	protected function getTokenUrl( $page, $token ) {
2929		global $wgArticlePath;
2930		return wfExpandUrl(
2931			str_replace(
2932				'$1',
2933				"Special:$page/$token",
2934				$wgArticlePath ) );
2935	}
2936
2937	/**
2938	 * Mark the e-mail address confirmed.
2939	 *
2940	 * @note Call saveSettings() after calling this function to commit the change.
2941	 */
2942	function confirmEmail() {
2943		$this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2944		return true;
2945	}
2946
2947	/**
2948	 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2949	 * address if it was already confirmed.
2950	 *
2951	 * @note Call saveSettings() after calling this function to commit the change.
2952	 */
2953	function invalidateEmail() {
2954		$this->load();
2955		$this->mEmailToken = null;
2956		$this->mEmailTokenExpires = null;
2957		$this->setEmailAuthenticationTimestamp( null );
2958		return true;
2959	}
2960
2961	/**
2962	 * Set the e-mail authentication timestamp.
2963	 * @param $timestamp \string TS_MW timestamp
2964	 */
2965	function setEmailAuthenticationTimestamp( $timestamp ) {
2966		$this->load();
2967		$this->mEmailAuthenticated = $timestamp;
2968		wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2969	}
2970
2971	/**
2972	 * Is this user allowed to send e-mails within limits of current
2973	 * site configuration?
2974	 * @return \bool True if allowed
2975	 */
2976	function canSendEmail() {
2977		global $wgEnableEmail, $wgEnableUserEmail;
2978		if( !$wgEnableEmail || !$wgEnableUserEmail ) {
2979			return false;
2980		}
2981		$canSend = $this->isEmailConfirmed();
2982		wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2983		return $canSend;
2984	}
2985
2986	/**
2987	 * Is this user allowed to receive e-mails within limits of current
2988	 * site configuration?
2989	 * @return \bool True if allowed
2990	 */
2991	function canReceiveEmail() {
2992		return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
2993	}
2994
2995	/**
2996	 * Is this user's e-mail address valid-looking and confirmed within
2997	 * limits of the current site configuration?
2998	 *
2999	 * @note If $wgEmailAuthentication is on, this may require the user to have
3000	 * confirmed their address by returning a code or using a password
3001	 * sent to the address from the wiki.
3002	 *
3003	 * @return \bool True if confirmed
3004	 */
3005	function isEmailConfirmed() {
3006		global $wgEmailAuthentication;
3007		$this->load();
3008		$confirmed = true;
3009		if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
3010			if( $this->isAnon() )
3011				return false;
3012			if( !self::isValidEmailAddr( $this->mEmail ) )
3013				return false;
3014			if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
3015				return false;
3016			return true;
3017		} else {
3018			return $confirmed;
3019		}
3020	}
3021
3022	/**
3023	 * Check whether there is an outstanding request for e-mail confirmation.
3024	 * @return \bool True if pending
3025	 */
3026	function isEmailConfirmationPending() {
3027		global $wgEmailAuthentication;
3028		return $wgEmailAuthentication &&
3029			!$this->isEmailConfirmed() &&
3030			$this->mEmailToken &&
3031			$this->mEmailTokenExpires > wfTimestamp();
3032	}
3033
3034	/**
3035	 * Get the timestamp of account creation.
3036	 *
3037	 * @return \types{\string,\bool} string Timestamp of account creation, or false for
3038	 *                                non-existent/anonymous user accounts.
3039	 */
3040	public function getRegistration() {
3041		return $this->getId() > 0
3042			? $this->mRegistration
3043			: false;
3044	}
3045	
3046	/**
3047	 * Get the timestamp of the first edit
3048	 *
3049	 * @return \types{\string,\bool} string Timestamp of first edit, or false for
3050	 *                                non-existent/anonymous user accounts.
3051	 */
3052	public function getFirstEditTimestamp() {
3053		if( $this->getId() == 0 ) return false; // anons
3054		$dbr = wfGetDB( DB_SLAVE );
3055		$time = $dbr->selectField( 'revision', 'rev_timestamp',
3056			array( 'rev_user' => $this->getId() ),
3057			__METHOD__,
3058			array( 'ORDER BY' => 'rev_timestamp ASC' )
3059		);
3060		if( !$time ) return false; // no edits
3061		return wfTimestamp( TS_MW, $time );
3062	}	
3063
3064	/**
3065	 * Get the permissions associated with a given list of groups
3066	 *
3067	 * @param $groups \type{\arrayof{\string}} List of internal group names
3068	 * @return \type{\arrayof{\string}} List of permission key names for given groups combined
3069	 */
3070	static function getGroupPermissions( $groups ) {
3071		global $wgGroupPermissions;
3072		$rights = array();
3073		foreach( $groups as $group ) {
3074			if( isset( $wgGroupPermissions[$group] ) ) {
3075				$rights = array_merge( $rights,
3076					// array_filter removes empty items
3077					array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
3078			}
3079		}
3080		return array_unique($rights);
3081	}
3082	
3083	/**
3084	 * Get all the groups who have a given permission
3085	 * 
3086	 * @param $role \string Role to check
3087	 * @return \type{\arrayof{\string}} List of internal group names with the given permission
3088	 */
3089	static function getGroupsWithPermission( $role ) {
3090		global $wgGroupPermissions;
3091		$allowedGroups = array();
3092		foreach ( $wgGroupPermissions as $group => $rights ) {
3093			if ( isset( $rights[$role] ) && $rights[$role] ) {
3094				$allowedGroups[] = $group;
3095			}
3096		}
3097		return $allowedGroups;
3098	}
3099
3100	/**
3101	 * Get the localized descriptive name for a group, if it exists
3102	 *
3103	 * @param $group \string Internal group name
3104	 * @return \string Localized descriptive group name
3105	 */
3106	static function getGroupName( $group ) {
3107		global $wgMessageCache;
3108		$wgMessageCache->loadAllMessages();
3109		$key = "group-$group";
3110		$name = wfMsg( $key );
3111		return $name == '' || wfEmptyMsg( $key, $name )
3112			? $group
3113			: $name;
3114	}
3115
3116	/**
3117	 * Get the localized descriptive name for a member of a group, if it exists
3118	 *
3119	 * @param $group \string Internal group name
3120	 * @return \string Localized name for group member
3121	 */
3122	static function getGroupMember( $group ) {
3123		global $wgMessageCache;
3124		$wgMessageCache->loadAllMessages();
3125		$key = "group-$group-member";
3126		$name = wfMsg( $key );
3127		return $name == '' || wfEmptyMsg( $key, $name )
3128			? $group
3129			: $name;
3130	}
3131
3132	/**
3133	 * Return the set of defined explicit groups.
3134	 * The implicit groups (by default *, 'user' and 'autoconfirmed')
3135	 * are not included, as they are defined automatically, not in the database.
3136	 * @return \type{\arrayof{\string}} Array of internal group names
3137	 */
3138	static function getAllGroups() {
3139		global $wgGroupPermissions;
3140		return array_diff(
3141			array_keys( $wgGroupPermissions ),
3142			self::getImplicitGroups()
3143		);
3144	}
3145
3146	/**
3147	 * Get a list of all available permissions.
3148	 * @return \type{\arrayof{\string}} Array of permission names
3149	 */
3150	static function getAllRights() {
3151		if ( self::$mAllRights === false ) {
3152			global $wgAvailableRights;
3153			if ( count( $wgAvailableRights ) ) {
3154				self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3155			} else {
3156				self::$mAllRights = self::$mCoreRights;
3157			}
3158			wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3159		}
3160		return self::$mAllRights;
3161	}
3162
3163	/**
3164	 * Get a list of implicit groups
3165	 * @return \type{\arrayof{\string}} Array of internal group names
3166	 */
3167	public static function getImplicitGroups() {
3168		global $wgImplicitGroups;
3169		$groups = $wgImplicitGroups;
3170		wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );	#deprecated, use $wgImplictGroups instead
3171		return $groups;
3172	}
3173
3174	/**
3175	 * Get the title of a page describing a particular group
3176	 *
3177	 * @param $group \string Internal group name
3178	 * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
3179	 */
3180	static function getGroupPage( $group ) {
3181		global $wgMessageCache;
3182		$wgMessageCache->loadAllMessages();
3183		$page = wfMsgForContent( 'grouppage-' . $group );
3184		if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3185			$title = Title::newFromText( $page );
3186			if( is_object( $title ) )
3187				return $title;
3188		}
3189		return false;
3190	}
3191
3192	/**
3193	 * Create a link to the group in HTML, if available; 
3194	 * else return the group name.
3195	 *
3196	 * @param $group \string Internal name of the group
3197	 * @param $text \string The text of the link
3198	 * @return \string HTML link to the group
3199	 */
3200	static function makeGroupLinkHTML( $group, $text = '' ) {
3201		if( $text == '' ) {
3202			$text = self::getGroupName( $group );
3203		}
3204		$title = self::getGroupPage( $group );
3205		if( $title ) {
3206			global $wgUser;
3207			$sk = $wgUser->getSkin();
3208			return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
3209		} else {
3210			return $text;
3211		}
3212	}
3213
3214	/**
3215	 * Create a link to the group in Wikitext, if available; 
3216	 * else return the group name.
3217	 *
3218	 * @param $group \string Internal name of the group
3219	 * @param $text \string The text of the link
3220	 * @return \string Wikilink to the group
3221	 */
3222	static function makeGroupLinkWiki( $group, $text = '' ) {
3223		if( $text == '' ) {
3224			$text = self::getGroupName( $group );
3225		}
3226		$title = self::getGroupPage( $group );
3227		if( $title ) {
3228			$page = $title->getPrefixedText();
3229			return "[[$page|$text]]";
3230		} else {
3231			return $text;
3232		}
3233	}
3234
3235	/**
3236	 * Increment the user's edit-count field.
3237	 * Will have no effect for anonymous users.
3238	 */
3239	function incEditCount() {
3240		if( !$this->isAnon() ) {
3241			$dbw = wfGetDB( DB_MASTER );
3242			$dbw->update( 'user',
3243				array( 'user_editcount=user_editcount+1' ),
3244				array( 'user_id' => $this->getId() ),
3245				__METHOD__ );
3246
3247			// Lazy initialization check...
3248			if( $dbw->affectedRows() == 0 ) {
3249				// Pull from a slave to be less cruel to servers
3250				// Accuracy isn't the point anyway here
3251				$dbr = wfGetDB( DB_SLAVE );
3252				$count = $dbr->selectField( 'revision',
3253					'COUNT(rev_user)',
3254					array( 'rev_user' => $this->getId() ),
3255					__METHOD__ );
3256
3257				// Now here's a goddamn hack...
3258				if( $dbr !== $dbw ) {
3259					// If we actually have a slave server, the count is
3260					// at least one behind because the current transaction
3261					// has not been committed and replicated.
3262					$count++;
3263				} else {
3264					// But if DB_SLAVE is selecting the master, then the
3265					// count we just read includes the revision that was
3266					// just added in the working transaction.
3267				}
3268
3269				$dbw->update( 'user',
3270					array( 'user_editcount' => $count ),
3271					array( 'user_id' => $this->getId() ),
3272					__METHOD__ );
3273			}
3274		}
3275		// edit count in user cache too
3276		$this->invalidateCache();
3277	}
3278	
3279	/**
3280	 * Get the description of a given right
3281	 *
3282	 * @param $right \string Right to query
3283	 * @return \string Localized description of the right
3284	 */
3285	static function getRightDescription( $right ) {
3286		global $wgMessageCache;
3287		$wgMessageCache->loadAllMessages();
3288		$key = "right-$right";
3289		$name = wfMsg( $key );
3290		return $name == '' || wfEmptyMsg( $key, $name )
3291			? $right
3292			: $name;
3293	}
3294
3295	/**
3296	 * Make an old-style password hash
3297	 *
3298	 * @param $password \string Plain-text password
3299	 * @param $userId \string User ID
3300	 * @return \string Password hash
3301	 */
3302	static function oldCrypt( $password, $userId ) {
3303		global $wgPasswordSalt;
3304		if ( $wgPasswordSalt ) {
3305			return md5( $userId . '-' . md5( $password ) );
3306		} else {
3307			return md5( $password );
3308		}
3309	}
3310
3311	/**
3312	 * Make a new-style password hash
3313	 *
3314	 * @param $password \string Plain-text password
3315	 * @param $salt \string Optional salt, may be random or the user ID. 
3316	 *                     If unspecified or false, will generate one automatically
3317	 * @return \string Password hash
3318	 */
3319	static function crypt( $password, $salt = false ) {
3320		global $wgPasswordSalt;
3321
3322		$hash = '';
3323		if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
3324			return $hash;
3325		}
3326		
3327		if( $wgPasswordSalt ) {
3328			if ( $salt === false ) {
3329				$salt = substr( wfGenerateToken(), 0, 8 );
3330			}
3331			return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3332		} else {
3333			return ':A:' . md5( $password );
3334		}
3335	}
3336
3337	/**
3338	 * Compare a password hash with a plain-text password. Requires the user
3339	 * ID if there's a chance that the hash is an old-style hash.
3340	 *
3341	 * @param $hash \string Password hash
3342	 * @param $password \string Plain-text password to compare
3343	 * @param $userId \string User ID for old-style password salt
3344	 * @return \bool
3345	 */
3346	static function comparePasswords( $hash, $password, $userId = false ) {
3347		$m = false;
3348		$type = substr( $hash, 0, 3 );
3349		
3350		$result = false;
3351		if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
3352			return $result;
3353		}
3354		
3355		if ( $type == ':A:' ) {
3356			# Unsalted
3357			return md5( $password ) === substr( $hash, 3 );
3358		} elseif ( $type == ':B:' ) {
3359			# Salted
3360			list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3361			return md5( $salt.'-'.md5( $password ) ) == $realHash;
3362		} else {
3363			# Old-style
3364			return self::oldCrypt( $password, $userId ) === $hash;
3365		}
3366	}
3367	
3368	/**
3369	 * Add a newuser log entry for this user
3370	 * @param $byEmail Boolean: account made by email?
3371	 */
3372	public function addNewUserLogEntry( $byEmail = false ) {
3373		global $wgUser, $wgContLang, $wgNewUserLog;
3374		if( empty($wgNewUserLog) ) {
3375			return true; // disabled
3376		}
3377		$talk = $wgContLang->getFormattedNsText( NS_TALK );
3378		if( $this->getName() == $wgUser->getName() ) {
3379			$action = 'create';
3380			$message = '';
3381		} else {
3382			$action = 'create2';
3383			$message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
3384		}
3385		$log = new LogPage( 'newusers' );
3386		$log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
3387		return true;
3388	}
3389
3390	/**
3391	 * Add an autocreate newuser log entry for this user
3392	 * Used by things like CentralAuth and perhaps other authplugins.
3393	 */
3394	public function addNewUserLogEntryAutoCreate() {
3395		global $wgNewUserLog;
3396		if( empty($wgNewUserLog) ) {
3397			return true; // disabled
3398		}
3399		$log = new LogPage( 'newusers', false );
3400		$log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3401		return true;
3402	}
3403
3404}