Followup r81034, remove the global statements
[mediawiki.git] / includes / User.php
blob9cd33f99286e466a1c88fa1ffae9096c1732f39e
1 <?php
2 /**
3 * Implements the User class for the %MediaWiki software.
4 * @file
5 */
7 /**
8 * Int Number of characters in user_token field.
9 * @ingroup Constants
11 define( 'USER_TOKEN_LENGTH', 32 );
13 /**
14 * Int Serialized record version.
15 * @ingroup Constants
17 define( 'MW_USER_VERSION', 8 );
19 /**
20 * String Some punctuation to prevent editing from broken text-mangling proxies.
21 * @ingroup Constants
23 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
25 /**
26 * Thrown by User::setPassword() on error.
27 * @ingroup Exception
29 class PasswordError extends MWException {
30 // NOP
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.
43 class User {
44 /**
45 * Global constants made accessible as class constants so that autoloader
46 * magic can be used.
48 const USER_TOKEN_LENGTH = USER_TOKEN_LENGTH;
49 const MW_USER_VERSION = MW_USER_VERSION;
50 const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
52 /**
53 * Array of Strings List of member variables which are saved to the
54 * shared cache (memcached). Any operation which changes the
55 * corresponding database fields must call a cache-clearing function.
56 * @showinitializer
58 static $mCacheVars = array(
59 // user table
60 'mId',
61 'mName',
62 'mRealName',
63 'mPassword',
64 'mNewpassword',
65 'mNewpassTime',
66 'mEmail',
67 'mTouched',
68 'mToken',
69 'mEmailAuthenticated',
70 'mEmailToken',
71 'mEmailTokenExpires',
72 'mRegistration',
73 'mEditCount',
74 // user_group table
75 'mGroups',
76 // user_properties table
77 'mOptionOverrides',
80 /**
81 * Array of Strings Core rights.
82 * Each of these should have a corresponding message of the form
83 * "right-$right".
84 * @showinitializer
86 static $mCoreRights = array(
87 'apihighlimits',
88 'autoconfirmed',
89 'autopatrol',
90 'bigdelete',
91 'block',
92 'blockemail',
93 'bot',
94 'browsearchive',
95 'createaccount',
96 'createpage',
97 'createtalk',
98 'delete',
99 'deletedhistory',
100 'deletedtext',
101 'deleterevision',
102 'disableaccount',
103 'edit',
104 'editinterface',
105 'editusercssjs',
106 'hideuser',
107 'import',
108 'importupload',
109 'ipblock-exempt',
110 'markbotedits',
111 'minoredit',
112 'move',
113 'movefile',
114 'move-rootuserpages',
115 'move-subpages',
116 'nominornewtalk',
117 'noratelimit',
118 'override-export-depth',
119 'patrol',
120 'protect',
121 'proxyunbannable',
122 'purge',
123 'read',
124 'reupload',
125 'reupload-shared',
126 'rollback',
127 'selenium',
128 'sendemail',
129 'siteadmin',
130 'suppressionlog',
131 'suppressredirect',
132 'suppressrevision',
133 'trackback',
134 'undelete',
135 'unwatchedpages',
136 'upload',
137 'upload_by_url',
138 'userrights',
139 'userrights-interwiki',
140 'writeapi',
143 * String Cached results of getAllRights()
145 static $mAllRights = false;
147 /** @name Cache variables */
148 //@{
149 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
150 $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
151 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides;
152 //@}
155 * Bool Whether the cache variables have been loaded.
157 var $mDataLoaded, $mAuthLoaded, $mOptionsLoaded;
160 * String Initialization data source if mDataLoaded==false. May be one of:
161 * - 'defaults' anonymous user initialised from class defaults
162 * - 'name' initialise from mName
163 * - 'id' initialise from mId
164 * - 'session' log in from cookies or session if possible
166 * Use the User::newFrom*() family of functions to set this.
168 var $mFrom;
171 * Lazy-initialized variables, invalidated with clearInstanceCache
173 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
174 $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
175 $mLocked, $mHideName, $mOptions;
177 static $idCacheByName = array();
180 * Lightweight constructor for an anonymous user.
181 * Use the User::newFrom* factory functions for other kinds of users.
183 * @see newFromName()
184 * @see newFromId()
185 * @see newFromConfirmationCode()
186 * @see newFromSession()
187 * @see newFromRow()
189 function __construct() {
190 $this->clearInstanceCache( 'defaults' );
194 * Load the user table data for this object from the source given by mFrom.
196 function load() {
197 if ( $this->mDataLoaded ) {
198 return;
200 wfProfileIn( __METHOD__ );
202 # Set it now to avoid infinite recursion in accessors
203 $this->mDataLoaded = true;
205 switch ( $this->mFrom ) {
206 case 'defaults':
207 $this->loadDefaults();
208 break;
209 case 'name':
210 $this->mId = self::idFromName( $this->mName );
211 if ( !$this->mId ) {
212 # Nonexistent user placeholder object
213 $this->loadDefaults( $this->mName );
214 } else {
215 $this->loadFromId();
217 break;
218 case 'id':
219 $this->loadFromId();
220 break;
221 case 'session':
222 $this->loadFromSession();
223 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
224 break;
225 default:
226 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
228 wfProfileOut( __METHOD__ );
232 * Load user table data, given mId has already been set.
233 * @return Bool false if the ID does not exist, true otherwise
234 * @private
236 function loadFromId() {
237 global $wgMemc;
238 if ( $this->mId == 0 ) {
239 $this->loadDefaults();
240 return false;
243 # Try cache
244 $key = wfMemcKey( 'user', 'id', $this->mId );
245 $data = $wgMemc->get( $key );
246 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
247 # Object is expired, load from DB
248 $data = false;
251 if ( !$data ) {
252 wfDebug( "User: cache miss for user {$this->mId}\n" );
253 # Load from DB
254 if ( !$this->loadFromDatabase() ) {
255 # Can't load from ID, user is anonymous
256 return false;
258 $this->saveToCache();
259 } else {
260 wfDebug( "User: got user {$this->mId} from cache\n" );
261 # Restore from cache
262 foreach ( self::$mCacheVars as $name ) {
263 $this->$name = $data[$name];
266 return true;
270 * Save user data to the shared cache
272 function saveToCache() {
273 $this->load();
274 $this->loadGroups();
275 $this->loadOptions();
276 if ( $this->isAnon() ) {
277 // Anonymous users are uncached
278 return;
280 $data = array();
281 foreach ( self::$mCacheVars as $name ) {
282 $data[$name] = $this->$name;
284 $data['mVersion'] = MW_USER_VERSION;
285 $key = wfMemcKey( 'user', 'id', $this->mId );
286 global $wgMemc;
287 $wgMemc->set( $key, $data );
291 /** @name newFrom*() static factory methods */
292 //@{
295 * Static factory method for creation from username.
297 * This is slightly less efficient than newFromId(), so use newFromId() if
298 * you have both an ID and a name handy.
300 * @param $name String Username, validated by Title::newFromText()
301 * @param $validate String|Bool Validate username. Takes the same parameters as
302 * User::getCanonicalName(), except that true is accepted as an alias
303 * for 'valid', for BC.
305 * @return User object, or false if the username is invalid
306 * (e.g. if it contains illegal characters or is an IP address). If the
307 * username is not present in the database, the result will be a user object
308 * with a name, zero user ID and default settings.
310 static function newFromName( $name, $validate = 'valid' ) {
311 if ( $validate === true ) {
312 $validate = 'valid';
314 $name = self::getCanonicalName( $name, $validate );
315 if ( $name === false ) {
316 return false;
317 } else {
318 # Create unloaded user object
319 $u = new User;
320 $u->mName = $name;
321 $u->mFrom = 'name';
322 return $u;
327 * Static factory method for creation from a given user ID.
329 * @param $id Int Valid user ID
330 * @return User The corresponding User object
332 static function newFromId( $id ) {
333 $u = new User;
334 $u->mId = $id;
335 $u->mFrom = 'id';
336 return $u;
340 * Factory method to fetch whichever user has a given email confirmation code.
341 * This code is generated when an account is created or its e-mail address
342 * has changed.
344 * If the code is invalid or has expired, returns NULL.
346 * @param $code String Confirmation code
347 * @return User
349 static function newFromConfirmationCode( $code ) {
350 $dbr = wfGetDB( DB_SLAVE );
351 $id = $dbr->selectField( 'user', 'user_id', array(
352 'user_email_token' => md5( $code ),
353 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
354 ) );
355 if( $id !== false ) {
356 return User::newFromId( $id );
357 } else {
358 return null;
363 * Create a new user object using data from session or cookies. If the
364 * login credentials are invalid, the result is an anonymous user.
366 * @return User
368 static function newFromSession() {
369 $user = new User;
370 $user->mFrom = 'session';
371 return $user;
375 * Create a new user object from a user row.
376 * The row should have all fields from the user table in it.
377 * @param $row Array A row from the user table
378 * @return User
380 static function newFromRow( $row ) {
381 $user = new User;
382 $user->loadFromRow( $row );
383 return $user;
386 //@}
390 * Get the username corresponding to a given user ID
391 * @param $id Int User ID
392 * @return String The corresponding username
394 static function whoIs( $id ) {
395 $dbr = wfGetDB( DB_SLAVE );
396 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), __METHOD__ );
400 * Get the real name of a user given their user ID
402 * @param $id Int User ID
403 * @return String The corresponding user's real name
405 static function whoIsReal( $id ) {
406 $dbr = wfGetDB( DB_SLAVE );
407 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
411 * Get database id given a user name
412 * @param $name String Username
413 * @return Int|Null The corresponding user's ID, or null if user is nonexistent
415 static function idFromName( $name ) {
416 $nt = Title::makeTitleSafe( NS_USER, $name );
417 if( is_null( $nt ) ) {
418 # Illegal name
419 return null;
422 if ( isset( self::$idCacheByName[$name] ) ) {
423 return self::$idCacheByName[$name];
426 $dbr = wfGetDB( DB_SLAVE );
427 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
429 if ( $s === false ) {
430 $result = null;
431 } else {
432 $result = $s->user_id;
435 self::$idCacheByName[$name] = $result;
437 if ( count( self::$idCacheByName ) > 1000 ) {
438 self::$idCacheByName = array();
441 return $result;
445 * Reset the cache used in idFromName(). For use in tests.
447 public static function resetIdByNameCache() {
448 self::$idCacheByName = array();
452 * Does the string match an anonymous IPv4 address?
454 * This function exists for username validation, in order to reject
455 * usernames which are similar in form to IP addresses. Strings such
456 * as 300.300.300.300 will return true because it looks like an IP
457 * address, despite not being strictly valid.
459 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
460 * address because the usemod software would "cloak" anonymous IP
461 * addresses like this, if we allowed accounts like this to be created
462 * new users could get the old edits of these anonymous users.
464 * @param $name String to match
465 * @return Bool
467 static function isIP( $name ) {
468 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
472 * Is the input a valid username?
474 * Checks if the input is a valid username, we don't want an empty string,
475 * an IP address, anything that containins slashes (would mess up subpages),
476 * is longer than the maximum allowed username size or doesn't begin with
477 * a capital letter.
479 * @param $name String to match
480 * @return Bool
482 static function isValidUserName( $name ) {
483 global $wgContLang, $wgMaxNameChars;
485 if ( $name == ''
486 || User::isIP( $name )
487 || strpos( $name, '/' ) !== false
488 || strlen( $name ) > $wgMaxNameChars
489 || $name != $wgContLang->ucfirst( $name ) ) {
490 wfDebugLog( 'username', __METHOD__ .
491 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
492 return false;
495 // Ensure that the name can't be misresolved as a different title,
496 // such as with extra namespace keys at the start.
497 $parsed = Title::newFromText( $name );
498 if( is_null( $parsed )
499 || $parsed->getNamespace()
500 || strcmp( $name, $parsed->getPrefixedText() ) ) {
501 wfDebugLog( 'username', __METHOD__ .
502 ": '$name' invalid due to ambiguous prefixes" );
503 return false;
506 // Check an additional blacklist of troublemaker characters.
507 // Should these be merged into the title char list?
508 $unicodeBlacklist = '/[' .
509 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
510 '\x{00a0}' . # non-breaking space
511 '\x{2000}-\x{200f}' . # various whitespace
512 '\x{2028}-\x{202f}' . # breaks and control chars
513 '\x{3000}' . # ideographic space
514 '\x{e000}-\x{f8ff}' . # private use
515 ']/u';
516 if( preg_match( $unicodeBlacklist, $name ) ) {
517 wfDebugLog( 'username', __METHOD__ .
518 ": '$name' invalid due to blacklisted characters" );
519 return false;
522 return true;
526 * Usernames which fail to pass this function will be blocked
527 * from user login and new account registrations, but may be used
528 * internally by batch processes.
530 * If an account already exists in this form, login will be blocked
531 * by a failure to pass this function.
533 * @param $name String to match
534 * @return Bool
536 static function isUsableName( $name ) {
537 global $wgReservedUsernames;
538 // Must be a valid username, obviously ;)
539 if ( !self::isValidUserName( $name ) ) {
540 return false;
543 static $reservedUsernames = false;
544 if ( !$reservedUsernames ) {
545 $reservedUsernames = $wgReservedUsernames;
546 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
549 // Certain names may be reserved for batch processes.
550 foreach ( $reservedUsernames as $reserved ) {
551 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
552 $reserved = wfMsgForContent( substr( $reserved, 4 ) );
554 if ( $reserved == $name ) {
555 return false;
558 return true;
562 * Usernames which fail to pass this function will be blocked
563 * from new account registrations, but may be used internally
564 * either by batch processes or by user accounts which have
565 * already been created.
567 * Additional blacklisting may be added here rather than in
568 * isValidUserName() to avoid disrupting existing accounts.
570 * @param $name String to match
571 * @return Bool
573 static function isCreatableName( $name ) {
574 global $wgInvalidUsernameCharacters;
576 // Ensure that the username isn't longer than 235 bytes, so that
577 // (at least for the builtin skins) user javascript and css files
578 // will work. (bug 23080)
579 if( strlen( $name ) > 235 ) {
580 wfDebugLog( 'username', __METHOD__ .
581 ": '$name' invalid due to length" );
582 return false;
585 if( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
586 wfDebugLog( 'username', __METHOD__ .
587 ": '$name' invalid due to wgInvalidUsernameCharacters" );
588 return false;
591 return self::isUsableName( $name );
595 * Is the input a valid password for this user?
597 * @param $password String Desired password
598 * @return Bool
600 function isValidPassword( $password ) {
601 //simple boolean wrapper for getPasswordValidity
602 return $this->getPasswordValidity( $password ) === true;
606 * Given unvalidated password input, return error message on failure.
608 * @param $password String Desired password
609 * @return mixed: true on success, string or array of error message on failure
611 function getPasswordValidity( $password ) {
612 global $wgMinimalPasswordLength, $wgContLang;
614 static $blockedLogins = array(
615 'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589
616 'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605
619 $result = false; //init $result to false for the internal checks
621 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
622 return $result;
624 if ( $result === false ) {
625 if( strlen( $password ) < $wgMinimalPasswordLength ) {
626 return 'passwordtooshort';
627 } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
628 return 'password-name-match';
629 } elseif ( isset( $blockedLogins[ $this->getName() ] ) && $password == $blockedLogins[ $this->getName() ] ) {
630 return 'password-login-forbidden';
631 } else {
632 //it seems weird returning true here, but this is because of the
633 //initialization of $result to false above. If the hook is never run or it
634 //doesn't modify $result, then we will likely get down into this if with
635 //a valid password.
636 return true;
638 } elseif( $result === true ) {
639 return true;
640 } else {
641 return $result; //the isValidPassword hook set a string $result and returned true
646 * Does a string look like an e-mail address?
648 * This validates an email address using an HTML5 specification found at:
649 * http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
650 * Which as of 2011-01-24 says:
652 * A valid e-mail address is a string that matches the ABNF production
653 * 1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
654 * in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
655 * 3.5.
657 * This function is an implementation of the specification as requested in
658 * bug 22449.
660 * Client-side forms will use the same standard validation rules via JS or
661 * HTML 5 validation; additional restrictions can be enforced server-side
662 * by extensions via the 'isValidEmailAddr' hook.
664 * Note that this validation doesn't 100% match RFC 2822, but is believed
665 * to be liberal enough for wide use. Some invalid addresses will still
666 * pass validation here.
668 * @param $addr String E-mail address
669 * @return Bool
671 public static function isValidEmailAddr( $addr ) {
672 $result = null;
673 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
674 return $result;
676 $rfc5322_atext = "a-z0-9!#$%&'*+-\/=?^_`{|}~" ;
677 $rfc1034_ldh_str = "a-z0-9-" ;
679 $HTML5_email_regexp = "/
680 ^ # start of string
681 [$rfc5322_atext\\.]+ # user part which is liberal :p
682 @ # 'apostrophe'
683 [$rfc1034_ldh_str]+ # First domain part
684 (\\.[$rfc1034_ldh_str]+)* # Following part prefixed with a dot
685 $ # End of string
686 /ix" ; // case Insensitive, eXtended
688 return (bool) preg_match( $HTML5_email_regexp, $addr );
692 * Given unvalidated user input, return a canonical username, or false if
693 * the username is invalid.
694 * @param $name String User input
695 * @param $validate String|Bool type of validation to use:
696 * - false No validation
697 * - 'valid' Valid for batch processes
698 * - 'usable' Valid for batch processes and login
699 * - 'creatable' Valid for batch processes, login and account creation
701 static function getCanonicalName( $name, $validate = 'valid' ) {
702 # Force usernames to capital
703 global $wgContLang;
704 $name = $wgContLang->ucfirst( $name );
706 # Reject names containing '#'; these will be cleaned up
707 # with title normalisation, but then it's too late to
708 # check elsewhere
709 if( strpos( $name, '#' ) !== false )
710 return false;
712 # Clean up name according to title rules
713 $t = ( $validate === 'valid' ) ?
714 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
715 # Check for invalid titles
716 if( is_null( $t ) ) {
717 return false;
720 # Reject various classes of invalid names
721 global $wgAuth;
722 $name = $wgAuth->getCanonicalName( $t->getText() );
724 switch ( $validate ) {
725 case false:
726 break;
727 case 'valid':
728 if ( !User::isValidUserName( $name ) ) {
729 $name = false;
731 break;
732 case 'usable':
733 if ( !User::isUsableName( $name ) ) {
734 $name = false;
736 break;
737 case 'creatable':
738 if ( !User::isCreatableName( $name ) ) {
739 $name = false;
741 break;
742 default:
743 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
745 return $name;
749 * Count the number of edits of a user
750 * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
752 * @param $uid Int User ID to check
753 * @return Int the user's edit count
755 static function edits( $uid ) {
756 wfProfileIn( __METHOD__ );
757 $dbr = wfGetDB( DB_SLAVE );
758 // check if the user_editcount field has been initialized
759 $field = $dbr->selectField(
760 'user', 'user_editcount',
761 array( 'user_id' => $uid ),
762 __METHOD__
765 if( $field === null ) { // it has not been initialized. do so.
766 $dbw = wfGetDB( DB_MASTER );
767 $count = $dbr->selectField(
768 'revision', 'count(*)',
769 array( 'rev_user' => $uid ),
770 __METHOD__
772 $dbw->update(
773 'user',
774 array( 'user_editcount' => $count ),
775 array( 'user_id' => $uid ),
776 __METHOD__
778 } else {
779 $count = $field;
781 wfProfileOut( __METHOD__ );
782 return $count;
786 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
787 * @todo hash random numbers to improve security, like generateToken()
789 * @return String new random password
791 static function randomPassword() {
792 global $wgMinimalPasswordLength;
793 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
794 $l = strlen( $pwchars ) - 1;
796 $pwlength = max( 7, $wgMinimalPasswordLength );
797 $digit = mt_rand( 0, $pwlength - 1 );
798 $np = '';
799 for ( $i = 0; $i < $pwlength; $i++ ) {
800 $np .= $i == $digit ? chr( mt_rand( 48, 57 ) ) : $pwchars{ mt_rand( 0, $l ) };
802 return $np;
806 * Set cached properties to default.
808 * @note This no longer clears uncached lazy-initialised properties;
809 * the constructor does that instead.
810 * @private
812 function loadDefaults( $name = false ) {
813 wfProfileIn( __METHOD__ );
815 global $wgRequest;
817 $this->mId = 0;
818 $this->mName = $name;
819 $this->mRealName = '';
820 $this->mPassword = $this->mNewpassword = '';
821 $this->mNewpassTime = null;
822 $this->mEmail = '';
823 $this->mOptionOverrides = null;
824 $this->mOptionsLoaded = false;
826 if( $wgRequest->getCookie( 'LoggedOut' ) !== null ) {
827 $this->mTouched = wfTimestamp( TS_MW, $wgRequest->getCookie( 'LoggedOut' ) );
828 } else {
829 $this->mTouched = '0'; # Allow any pages to be cached
832 $this->setToken(); # Random
833 $this->mEmailAuthenticated = null;
834 $this->mEmailToken = '';
835 $this->mEmailTokenExpires = null;
836 $this->mRegistration = wfTimestamp( TS_MW );
837 $this->mGroups = array();
839 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
841 wfProfileOut( __METHOD__ );
845 * Load user data from the session or login cookie. If there are no valid
846 * credentials, initialises the user as an anonymous user.
847 * @return Bool True if the user is logged in, false otherwise.
849 private function loadFromSession() {
850 global $wgRequest, $wgExternalAuthType, $wgAutocreatePolicy;
852 $result = null;
853 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
854 if ( $result !== null ) {
855 return $result;
858 if ( $wgExternalAuthType && $wgAutocreatePolicy == 'view' ) {
859 $extUser = ExternalUser::newFromCookie();
860 if ( $extUser ) {
861 # TODO: Automatically create the user here (or probably a bit
862 # lower down, in fact)
866 if ( $wgRequest->getCookie( 'UserID' ) !== null ) {
867 $sId = intval( $wgRequest->getCookie( 'UserID' ) );
868 if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
869 $this->loadDefaults(); // Possible collision!
870 wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
871 cookie user ID ($sId) don't match!" );
872 return false;
874 $_SESSION['wsUserID'] = $sId;
875 } else if ( isset( $_SESSION['wsUserID'] ) ) {
876 if ( $_SESSION['wsUserID'] != 0 ) {
877 $sId = $_SESSION['wsUserID'];
878 } else {
879 $this->loadDefaults();
880 return false;
882 } else {
883 $this->loadDefaults();
884 return false;
887 if ( isset( $_SESSION['wsUserName'] ) ) {
888 $sName = $_SESSION['wsUserName'];
889 } else if ( $wgRequest->getCookie('UserName') !== null ) {
890 $sName = $wgRequest->getCookie('UserName');
891 $_SESSION['wsUserName'] = $sName;
892 } else {
893 $this->loadDefaults();
894 return false;
897 $this->mId = $sId;
898 if ( !$this->loadFromId() ) {
899 # Not a valid ID, loadFromId has switched the object to anon for us
900 return false;
903 global $wgBlockDisablesLogin;
904 if( $wgBlockDisablesLogin && $this->isBlocked() ) {
905 # User blocked and we've disabled blocked user logins
906 $this->loadDefaults();
907 return false;
910 if ( isset( $_SESSION['wsToken'] ) ) {
911 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
912 $from = 'session';
913 } else if ( $wgRequest->getCookie( 'Token' ) !== null ) {
914 $passwordCorrect = $this->mToken == $wgRequest->getCookie( 'Token' );
915 $from = 'cookie';
916 } else {
917 # No session or persistent login cookie
918 $this->loadDefaults();
919 return false;
922 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
923 $_SESSION['wsToken'] = $this->mToken;
924 wfDebug( "User: logged in from $from\n" );
925 return true;
926 } else {
927 # Invalid credentials
928 wfDebug( "User: can't log in from $from, invalid credentials\n" );
929 $this->loadDefaults();
930 return false;
935 * Load user and user_group data from the database.
936 * $this::mId must be set, this is how the user is identified.
938 * @return Bool True if the user exists, false if the user is anonymous
939 * @private
941 function loadFromDatabase() {
942 # Paranoia
943 $this->mId = intval( $this->mId );
945 /** Anonymous user */
946 if( !$this->mId ) {
947 $this->loadDefaults();
948 return false;
951 $dbr = wfGetDB( DB_MASTER );
952 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
954 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
956 if ( $s !== false ) {
957 # Initialise user table data
958 $this->loadFromRow( $s );
959 $this->mGroups = null; // deferred
960 $this->getEditCount(); // revalidation for nulls
961 return true;
962 } else {
963 # Invalid user_id
964 $this->mId = 0;
965 $this->loadDefaults();
966 return false;
971 * Initialize this object from a row from the user table.
973 * @param $row Array Row from the user table to load.
975 function loadFromRow( $row ) {
976 $this->mDataLoaded = true;
978 if ( isset( $row->user_id ) ) {
979 $this->mId = intval( $row->user_id );
981 $this->mName = $row->user_name;
982 $this->mRealName = $row->user_real_name;
983 $this->mPassword = $row->user_password;
984 $this->mNewpassword = $row->user_newpassword;
985 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
986 $this->mEmail = $row->user_email;
987 $this->decodeOptions( $row->user_options );
988 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
989 $this->mToken = $row->user_token;
990 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
991 $this->mEmailToken = $row->user_email_token;
992 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
993 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
994 $this->mEditCount = $row->user_editcount;
998 * Load the groups from the database if they aren't already loaded.
999 * @private
1001 function loadGroups() {
1002 if ( is_null( $this->mGroups ) ) {
1003 $dbr = wfGetDB( DB_MASTER );
1004 $res = $dbr->select( 'user_groups',
1005 array( 'ug_group' ),
1006 array( 'ug_user' => $this->mId ),
1007 __METHOD__ );
1008 $this->mGroups = array();
1009 foreach ( $res as $row ) {
1010 $this->mGroups[] = $row->ug_group;
1016 * Clear various cached data stored in this object.
1017 * @param $reloadFrom String Reload user and user_groups table data from a
1018 * given source. May be "name", "id", "defaults", "session", or false for
1019 * no reload.
1021 function clearInstanceCache( $reloadFrom = false ) {
1022 $this->mNewtalk = -1;
1023 $this->mDatePreference = null;
1024 $this->mBlockedby = -1; # Unset
1025 $this->mHash = false;
1026 $this->mSkin = null;
1027 $this->mRights = null;
1028 $this->mEffectiveGroups = null;
1029 $this->mOptions = null;
1031 if ( $reloadFrom ) {
1032 $this->mDataLoaded = false;
1033 $this->mFrom = $reloadFrom;
1038 * Combine the language default options with any site-specific options
1039 * and add the default language variants.
1041 * @return Array of String options
1043 static function getDefaultOptions() {
1044 global $wgNamespacesToBeSearchedDefault;
1046 * Site defaults will override the global/language defaults
1048 global $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
1049 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
1052 * default language setting
1054 $variant = $wgContLang->getDefaultVariant();
1055 $defOpt['variant'] = $variant;
1056 $defOpt['language'] = $variant;
1057 foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
1058 $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
1060 $defOpt['skin'] = $wgDefaultSkin;
1062 return $defOpt;
1066 * Get a given default option value.
1068 * @param $opt String Name of option to retrieve
1069 * @return String Default option value
1071 public static function getDefaultOption( $opt ) {
1072 $defOpts = self::getDefaultOptions();
1073 if( isset( $defOpts[$opt] ) ) {
1074 return $defOpts[$opt];
1075 } else {
1076 return null;
1082 * Get blocking information
1083 * @private
1084 * @param $bFromSlave Bool Whether to check the slave database first. To
1085 * improve performance, non-critical checks are done
1086 * against slaves. Check when actually saving should be
1087 * done against master.
1089 function getBlockedStatus( $bFromSlave = true ) {
1090 global $wgProxyWhitelist, $wgUser;
1092 if ( -1 != $this->mBlockedby ) {
1093 return;
1096 wfProfileIn( __METHOD__ );
1097 wfDebug( __METHOD__.": checking...\n" );
1099 // Initialize data...
1100 // Otherwise something ends up stomping on $this->mBlockedby when
1101 // things get lazy-loaded later, causing false positive block hits
1102 // due to -1 !== 0. Probably session-related... Nothing should be
1103 // overwriting mBlockedby, surely?
1104 $this->load();
1106 $this->mBlockedby = 0;
1107 $this->mHideName = 0;
1108 $this->mAllowUsertalk = 0;
1110 # Check if we are looking at an IP or a logged-in user
1111 if ( $this->isIP( $this->getName() ) ) {
1112 $ip = $this->getName();
1113 } else {
1114 # Check if we are looking at the current user
1115 # If we don't, and the user is logged in, we don't know about
1116 # his IP / autoblock status, so ignore autoblock of current user's IP
1117 if ( $this->getID() != $wgUser->getID() ) {
1118 $ip = '';
1119 } else {
1120 # Get IP of current user
1121 $ip = wfGetIP();
1125 if ( $this->isAllowed( 'ipblock-exempt' ) ) {
1126 # Exempt from all types of IP-block
1127 $ip = '';
1130 # User/IP blocking
1131 $this->mBlock = new Block();
1132 $this->mBlock->fromMaster( !$bFromSlave );
1133 if ( $this->mBlock->load( $ip , $this->mId ) ) {
1134 wfDebug( __METHOD__ . ": Found block.\n" );
1135 $this->mBlockedby = $this->mBlock->mBy;
1136 if( $this->mBlockedby == 0 )
1137 $this->mBlockedby = $this->mBlock->mByName;
1138 $this->mBlockreason = $this->mBlock->mReason;
1139 $this->mHideName = $this->mBlock->mHideName;
1140 $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
1141 if ( $this->isLoggedIn() && $wgUser->getID() == $this->getID() ) {
1142 $this->spreadBlock();
1144 } else {
1145 // Bug 13611: don't remove mBlock here, to allow account creation blocks to
1146 // apply to users. Note that the existence of $this->mBlock is not used to
1147 // check for edit blocks, $this->mBlockedby is instead.
1150 # Proxy blocking
1151 if ( !$this->isAllowed( 'proxyunbannable' ) && !in_array( $ip, $wgProxyWhitelist ) ) {
1152 # Local list
1153 if ( wfIsLocallyBlockedProxy( $ip ) ) {
1154 $this->mBlockedby = wfMsg( 'proxyblocker' );
1155 $this->mBlockreason = wfMsg( 'proxyblockreason' );
1158 # DNSBL
1159 if ( !$this->mBlockedby && !$this->getID() ) {
1160 if ( $this->isDnsBlacklisted( $ip ) ) {
1161 $this->mBlockedby = wfMsg( 'sorbs' );
1162 $this->mBlockreason = wfMsg( 'sorbsreason' );
1167 # Extensions
1168 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1170 wfProfileOut( __METHOD__ );
1174 * Whether the given IP is in a DNS blacklist.
1176 * @param $ip String IP to check
1177 * @param $checkWhitelist Bool: whether to check the whitelist first
1178 * @return Bool True if blacklisted.
1180 function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
1181 global $wgEnableSorbs, $wgEnableDnsBlacklist,
1182 $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
1184 if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs )
1185 return false;
1187 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
1188 return false;
1190 $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
1191 return $this->inDnsBlacklist( $ip, $urls );
1195 * Whether the given IP is in a given DNS blacklist.
1197 * @param $ip String IP to check
1198 * @param $bases String|Array of Strings: URL of the DNS blacklist
1199 * @return Bool True if blacklisted.
1201 function inDnsBlacklist( $ip, $bases ) {
1202 wfProfileIn( __METHOD__ );
1204 $found = false;
1205 // FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
1206 if( IP::isIPv4( $ip ) ) {
1207 # Reverse IP, bug 21255
1208 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
1210 foreach( (array)$bases as $base ) {
1211 # Make hostname
1212 $host = "$ipReversed.$base";
1214 # Send query
1215 $ipList = gethostbynamel( $host );
1217 if( $ipList ) {
1218 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1219 $found = true;
1220 break;
1221 } else {
1222 wfDebug( "Requested $host, not found in $base.\n" );
1227 wfProfileOut( __METHOD__ );
1228 return $found;
1232 * Is this user subject to rate limiting?
1234 * @return Bool True if rate limited
1236 public function isPingLimitable() {
1237 global $wgRateLimitsExcludedGroups;
1238 global $wgRateLimitsExcludedIPs;
1239 if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1240 // Deprecated, but kept for backwards-compatibility config
1241 return false;
1243 if( in_array( wfGetIP(), $wgRateLimitsExcludedIPs ) ) {
1244 // No other good way currently to disable rate limits
1245 // for specific IPs. :P
1246 // But this is a crappy hack and should die.
1247 return false;
1249 return !$this->isAllowed('noratelimit');
1253 * Primitive rate limits: enforce maximum actions per time period
1254 * to put a brake on flooding.
1256 * @note When using a shared cache like memcached, IP-address
1257 * last-hit counters will be shared across wikis.
1259 * @param $action String Action to enforce; 'edit' if unspecified
1260 * @return Bool True if a rate limiter was tripped
1262 function pingLimiter( $action = 'edit' ) {
1263 # Call the 'PingLimiter' hook
1264 $result = false;
1265 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1266 return $result;
1269 global $wgRateLimits;
1270 if( !isset( $wgRateLimits[$action] ) ) {
1271 return false;
1274 # Some groups shouldn't trigger the ping limiter, ever
1275 if( !$this->isPingLimitable() )
1276 return false;
1278 global $wgMemc, $wgRateLimitLog;
1279 wfProfileIn( __METHOD__ );
1281 $limits = $wgRateLimits[$action];
1282 $keys = array();
1283 $id = $this->getId();
1284 $ip = wfGetIP();
1285 $userLimit = false;
1287 if( isset( $limits['anon'] ) && $id == 0 ) {
1288 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1291 if( isset( $limits['user'] ) && $id != 0 ) {
1292 $userLimit = $limits['user'];
1294 if( $this->isNewbie() ) {
1295 if( isset( $limits['newbie'] ) && $id != 0 ) {
1296 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1298 if( isset( $limits['ip'] ) ) {
1299 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1301 $matches = array();
1302 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1303 $subnet = $matches[1];
1304 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1307 // Check for group-specific permissions
1308 // If more than one group applies, use the group with the highest limit
1309 foreach ( $this->getGroups() as $group ) {
1310 if ( isset( $limits[$group] ) ) {
1311 if ( $userLimit === false || $limits[$group] > $userLimit ) {
1312 $userLimit = $limits[$group];
1316 // Set the user limit key
1317 if ( $userLimit !== false ) {
1318 wfDebug( __METHOD__ . ": effective user limit: $userLimit\n" );
1319 $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1322 $triggered = false;
1323 foreach( $keys as $key => $limit ) {
1324 list( $max, $period ) = $limit;
1325 $summary = "(limit $max in {$period}s)";
1326 $count = $wgMemc->get( $key );
1327 // Already pinged?
1328 if( $count ) {
1329 if( $count > $max ) {
1330 wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
1331 if( $wgRateLimitLog ) {
1332 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1334 $triggered = true;
1335 } else {
1336 wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
1338 } else {
1339 wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
1340 $wgMemc->add( $key, 0, intval( $period ) ); // first ping
1342 $wgMemc->incr( $key );
1345 wfProfileOut( __METHOD__ );
1346 return $triggered;
1350 * Check if user is blocked
1352 * @param $bFromSlave Bool Whether to check the slave database instead of the master
1353 * @return Bool True if blocked, false otherwise
1355 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1356 $this->getBlockedStatus( $bFromSlave );
1357 return $this->mBlockedby !== 0;
1361 * Check if user is blocked from editing a particular article
1363 * @param $title Title to check
1364 * @param $bFromSlave Bool whether to check the slave database instead of the master
1365 * @return Bool
1367 function isBlockedFrom( $title, $bFromSlave = false ) {
1368 global $wgBlockAllowsUTEdit;
1369 wfProfileIn( __METHOD__ );
1371 $blocked = $this->isBlocked( $bFromSlave );
1372 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
1373 # If a user's name is suppressed, they cannot make edits anywhere
1374 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1375 $title->getNamespace() == NS_USER_TALK ) {
1376 $blocked = false;
1377 wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
1380 wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
1382 wfProfileOut( __METHOD__ );
1383 return $blocked;
1387 * If user is blocked, return the name of the user who placed the block
1388 * @return String name of blocker
1390 function blockedBy() {
1391 $this->getBlockedStatus();
1392 return $this->mBlockedby;
1396 * If user is blocked, return the specified reason for the block
1397 * @return String Blocking reason
1399 function blockedFor() {
1400 $this->getBlockedStatus();
1401 return $this->mBlockreason;
1405 * If user is blocked, return the ID for the block
1406 * @return Int Block ID
1408 function getBlockId() {
1409 $this->getBlockedStatus();
1410 return ( $this->mBlock ? $this->mBlock->mId : false );
1414 * Check if user is blocked on all wikis.
1415 * Do not use for actual edit permission checks!
1416 * This is intented for quick UI checks.
1418 * @param $ip String IP address, uses current client if none given
1419 * @return Bool True if blocked, false otherwise
1421 function isBlockedGlobally( $ip = '' ) {
1422 if( $this->mBlockedGlobally !== null ) {
1423 return $this->mBlockedGlobally;
1425 // User is already an IP?
1426 if( IP::isIPAddress( $this->getName() ) ) {
1427 $ip = $this->getName();
1428 } else if( !$ip ) {
1429 $ip = wfGetIP();
1431 $blocked = false;
1432 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1433 $this->mBlockedGlobally = (bool)$blocked;
1434 return $this->mBlockedGlobally;
1438 * Check if user account is locked
1440 * @return Bool True if locked, false otherwise
1442 function isLocked() {
1443 if( $this->mLocked !== null ) {
1444 return $this->mLocked;
1446 global $wgAuth;
1447 $authUser = $wgAuth->getUserInstance( $this );
1448 $this->mLocked = (bool)$authUser->isLocked();
1449 return $this->mLocked;
1453 * Check if user account is hidden
1455 * @return Bool True if hidden, false otherwise
1457 function isHidden() {
1458 if( $this->mHideName !== null ) {
1459 return $this->mHideName;
1461 $this->getBlockedStatus();
1462 if( !$this->mHideName ) {
1463 global $wgAuth;
1464 $authUser = $wgAuth->getUserInstance( $this );
1465 $this->mHideName = (bool)$authUser->isHidden();
1467 return $this->mHideName;
1471 * Get the user's ID.
1472 * @return Int The user's ID; 0 if the user is anonymous or nonexistent
1474 function getId() {
1475 if( $this->mId === null and $this->mName !== null
1476 and User::isIP( $this->mName ) ) {
1477 // Special case, we know the user is anonymous
1478 return 0;
1479 } elseif( $this->mId === null ) {
1480 // Don't load if this was initialized from an ID
1481 $this->load();
1483 return $this->mId;
1487 * Set the user and reload all fields according to a given ID
1488 * @param $v Int User ID to reload
1490 function setId( $v ) {
1491 $this->mId = $v;
1492 $this->clearInstanceCache( 'id' );
1496 * Get the user name, or the IP of an anonymous user
1497 * @return String User's name or IP address
1499 function getName() {
1500 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1501 # Special case optimisation
1502 return $this->mName;
1503 } else {
1504 $this->load();
1505 if ( $this->mName === false ) {
1506 # Clean up IPs
1507 $this->mName = IP::sanitizeIP( wfGetIP() );
1509 return $this->mName;
1514 * Set the user name.
1516 * This does not reload fields from the database according to the given
1517 * name. Rather, it is used to create a temporary "nonexistent user" for
1518 * later addition to the database. It can also be used to set the IP
1519 * address for an anonymous user to something other than the current
1520 * remote IP.
1522 * @note User::newFromName() has rougly the same function, when the named user
1523 * does not exist.
1524 * @param $str String New user name to set
1526 function setName( $str ) {
1527 $this->load();
1528 $this->mName = $str;
1532 * Get the user's name escaped by underscores.
1533 * @return String Username escaped by underscores.
1535 function getTitleKey() {
1536 return str_replace( ' ', '_', $this->getName() );
1540 * Check if the user has new messages.
1541 * @return Bool True if the user has new messages
1543 function getNewtalk() {
1544 $this->load();
1546 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1547 if( $this->mNewtalk === -1 ) {
1548 $this->mNewtalk = false; # reset talk page status
1550 # Check memcached separately for anons, who have no
1551 # entire User object stored in there.
1552 if( !$this->mId ) {
1553 global $wgMemc;
1554 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1555 $newtalk = $wgMemc->get( $key );
1556 if( strval( $newtalk ) !== '' ) {
1557 $this->mNewtalk = (bool)$newtalk;
1558 } else {
1559 // Since we are caching this, make sure it is up to date by getting it
1560 // from the master
1561 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1562 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1564 } else {
1565 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1569 return (bool)$this->mNewtalk;
1573 * Return the talk page(s) this user has new messages on.
1574 * @return Array of String page URLs
1576 function getNewMessageLinks() {
1577 $talks = array();
1578 if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) )
1579 return $talks;
1581 if( !$this->getNewtalk() )
1582 return array();
1583 $up = $this->getUserPage();
1584 $utp = $up->getTalkPage();
1585 return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL() ) );
1589 * Internal uncached check for new messages
1591 * @see getNewtalk()
1592 * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
1593 * @param $id String|Int User's IP address for anonymous users, User ID otherwise
1594 * @param $fromMaster Bool true to fetch from the master, false for a slave
1595 * @return Bool True if the user has new messages
1596 * @private
1598 function checkNewtalk( $field, $id, $fromMaster = false ) {
1599 if ( $fromMaster ) {
1600 $db = wfGetDB( DB_MASTER );
1601 } else {
1602 $db = wfGetDB( DB_SLAVE );
1604 $ok = $db->selectField( 'user_newtalk', $field,
1605 array( $field => $id ), __METHOD__ );
1606 return $ok !== false;
1610 * Add or update the new messages flag
1611 * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
1612 * @param $id String|Int User's IP address for anonymous users, User ID otherwise
1613 * @return Bool True if successful, false otherwise
1614 * @private
1616 function updateNewtalk( $field, $id ) {
1617 $dbw = wfGetDB( DB_MASTER );
1618 $dbw->insert( 'user_newtalk',
1619 array( $field => $id ),
1620 __METHOD__,
1621 'IGNORE' );
1622 if ( $dbw->affectedRows() ) {
1623 wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
1624 return true;
1625 } else {
1626 wfDebug( __METHOD__ . " already set ($field, $id)\n" );
1627 return false;
1632 * Clear the new messages flag for the given user
1633 * @param $field String 'user_ip' for anonymous users, 'user_id' otherwise
1634 * @param $id String|Int User's IP address for anonymous users, User ID otherwise
1635 * @return Bool True if successful, false otherwise
1636 * @private
1638 function deleteNewtalk( $field, $id ) {
1639 $dbw = wfGetDB( DB_MASTER );
1640 $dbw->delete( 'user_newtalk',
1641 array( $field => $id ),
1642 __METHOD__ );
1643 if ( $dbw->affectedRows() ) {
1644 wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
1645 return true;
1646 } else {
1647 wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
1648 return false;
1653 * Update the 'You have new messages!' status.
1654 * @param $val Bool Whether the user has new messages
1656 function setNewtalk( $val ) {
1657 if( wfReadOnly() ) {
1658 return;
1661 $this->load();
1662 $this->mNewtalk = $val;
1664 if( $this->isAnon() ) {
1665 $field = 'user_ip';
1666 $id = $this->getName();
1667 } else {
1668 $field = 'user_id';
1669 $id = $this->getId();
1671 global $wgMemc;
1673 if( $val ) {
1674 $changed = $this->updateNewtalk( $field, $id );
1675 } else {
1676 $changed = $this->deleteNewtalk( $field, $id );
1679 if( $this->isAnon() ) {
1680 // Anons have a separate memcached space, since
1681 // user records aren't kept for them.
1682 $key = wfMemcKey( 'newtalk', 'ip', $id );
1683 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1685 if ( $changed ) {
1686 $this->invalidateCache();
1691 * Generate a current or new-future timestamp to be stored in the
1692 * user_touched field when we update things.
1693 * @return String Timestamp in TS_MW format
1695 private static function newTouchedTimestamp() {
1696 global $wgClockSkewFudge;
1697 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1701 * Clear user data from memcached.
1702 * Use after applying fun updates to the database; caller's
1703 * responsibility to update user_touched if appropriate.
1705 * Called implicitly from invalidateCache() and saveSettings().
1707 private function clearSharedCache() {
1708 $this->load();
1709 if( $this->mId ) {
1710 global $wgMemc;
1711 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1716 * Immediately touch the user data cache for this account.
1717 * Updates user_touched field, and removes account data from memcached
1718 * for reload on the next hit.
1720 function invalidateCache() {
1721 if( wfReadOnly() ) {
1722 return;
1724 $this->load();
1725 if( $this->mId ) {
1726 $this->mTouched = self::newTouchedTimestamp();
1728 $dbw = wfGetDB( DB_MASTER );
1729 $dbw->update( 'user',
1730 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1731 array( 'user_id' => $this->mId ),
1732 __METHOD__ );
1734 $this->clearSharedCache();
1739 * Validate the cache for this account.
1740 * @param $timestamp String A timestamp in TS_MW format
1742 function validateCache( $timestamp ) {
1743 $this->load();
1744 return ( $timestamp >= $this->mTouched );
1748 * Get the user touched timestamp
1749 * @return String timestamp
1751 function getTouched() {
1752 $this->load();
1753 return $this->mTouched;
1757 * Set the password and reset the random token.
1758 * Calls through to authentication plugin if necessary;
1759 * will have no effect if the auth plugin refuses to
1760 * pass the change through or if the legal password
1761 * checks fail.
1763 * As a special case, setting the password to null
1764 * wipes it, so the account cannot be logged in until
1765 * a new password is set, for instance via e-mail.
1767 * @param $str String New password to set
1768 * @throws PasswordError on failure
1770 function setPassword( $str ) {
1771 global $wgAuth;
1773 if( $str !== null ) {
1774 if( !$wgAuth->allowPasswordChange() ) {
1775 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1778 if( !$this->isValidPassword( $str ) ) {
1779 global $wgMinimalPasswordLength;
1780 $valid = $this->getPasswordValidity( $str );
1781 if ( is_array( $valid ) ) {
1782 $message = array_shift( $valid );
1783 $params = $valid;
1784 } else {
1785 $message = $valid;
1786 $params = array( $wgMinimalPasswordLength );
1788 throw new PasswordError( wfMsgExt( $message, array( 'parsemag' ), $params ) );
1792 if( !$wgAuth->setPassword( $this, $str ) ) {
1793 throw new PasswordError( wfMsg( 'externaldberror' ) );
1796 $this->setInternalPassword( $str );
1798 return true;
1802 * Set the password and reset the random token unconditionally.
1804 * @param $str String New password to set
1806 function setInternalPassword( $str ) {
1807 $this->load();
1808 $this->setToken();
1810 if( $str === null ) {
1811 // Save an invalid hash...
1812 $this->mPassword = '';
1813 } else {
1814 $this->mPassword = self::crypt( $str );
1816 $this->mNewpassword = '';
1817 $this->mNewpassTime = null;
1821 * Get the user's current token.
1822 * @return String Token
1824 function getToken() {
1825 $this->load();
1826 return $this->mToken;
1830 * Set the random token (used for persistent authentication)
1831 * Called from loadDefaults() among other places.
1833 * @param $token String If specified, set the token to this value
1834 * @private
1836 function setToken( $token = false ) {
1837 global $wgSecretKey, $wgProxyKey;
1838 $this->load();
1839 if ( !$token ) {
1840 if ( $wgSecretKey ) {
1841 $key = $wgSecretKey;
1842 } elseif ( $wgProxyKey ) {
1843 $key = $wgProxyKey;
1844 } else {
1845 $key = microtime();
1847 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1848 } else {
1849 $this->mToken = $token;
1854 * Set the cookie password
1856 * @param $str String New cookie password
1857 * @private
1859 function setCookiePassword( $str ) {
1860 $this->load();
1861 $this->mCookiePassword = md5( $str );
1865 * Set the password for a password reminder or new account email
1867 * @param $str String New password to set
1868 * @param $throttle Bool If true, reset the throttle timestamp to the present
1870 function setNewpassword( $str, $throttle = true ) {
1871 $this->load();
1872 $this->mNewpassword = self::crypt( $str );
1873 if ( $throttle ) {
1874 $this->mNewpassTime = wfTimestampNow();
1879 * Has password reminder email been sent within the last
1880 * $wgPasswordReminderResendTime hours?
1881 * @return Bool
1883 function isPasswordReminderThrottled() {
1884 global $wgPasswordReminderResendTime;
1885 $this->load();
1886 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1887 return false;
1889 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1890 return time() < $expiry;
1894 * Get the user's e-mail address
1895 * @return String User's email address
1897 function getEmail() {
1898 $this->load();
1899 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1900 return $this->mEmail;
1904 * Get the timestamp of the user's e-mail authentication
1905 * @return String TS_MW timestamp
1907 function getEmailAuthenticationTimestamp() {
1908 $this->load();
1909 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1910 return $this->mEmailAuthenticated;
1914 * Set the user's e-mail address
1915 * @param $str String New e-mail address
1917 function setEmail( $str ) {
1918 $this->load();
1919 $this->mEmail = $str;
1920 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1924 * Get the user's real name
1925 * @return String User's real name
1927 function getRealName() {
1928 $this->load();
1929 return $this->mRealName;
1933 * Set the user's real name
1934 * @param $str String New real name
1936 function setRealName( $str ) {
1937 $this->load();
1938 $this->mRealName = $str;
1942 * Get the user's current setting for a given option.
1944 * @param $oname String The option to check
1945 * @param $defaultOverride String A default value returned if the option does not exist
1946 * @return String User's current value for the option
1947 * @see getBoolOption()
1948 * @see getIntOption()
1950 function getOption( $oname, $defaultOverride = null ) {
1951 $this->loadOptions();
1953 if ( is_null( $this->mOptions ) ) {
1954 if($defaultOverride != '') {
1955 return $defaultOverride;
1957 $this->mOptions = User::getDefaultOptions();
1960 if ( array_key_exists( $oname, $this->mOptions ) ) {
1961 return $this->mOptions[$oname];
1962 } else {
1963 return $defaultOverride;
1968 * Get all user's options
1970 * @return array
1972 public function getOptions() {
1973 $this->loadOptions();
1974 return $this->mOptions;
1978 * Get the user's current setting for a given option, as a boolean value.
1980 * @param $oname String The option to check
1981 * @return Bool User's current value for the option
1982 * @see getOption()
1984 function getBoolOption( $oname ) {
1985 return (bool)$this->getOption( $oname );
1990 * Get the user's current setting for a given option, as a boolean value.
1992 * @param $oname String The option to check
1993 * @param $defaultOverride Int A default value returned if the option does not exist
1994 * @return Int User's current value for the option
1995 * @see getOption()
1997 function getIntOption( $oname, $defaultOverride=0 ) {
1998 $val = $this->getOption( $oname );
1999 if( $val == '' ) {
2000 $val = $defaultOverride;
2002 return intval( $val );
2006 * Set the given option for a user.
2008 * @param $oname String The option to set
2009 * @param $val mixed New value to set
2011 function setOption( $oname, $val ) {
2012 $this->load();
2013 $this->loadOptions();
2015 if ( $oname == 'skin' ) {
2016 # Clear cached skin, so the new one displays immediately in Special:Preferences
2017 $this->mSkin = null;
2020 // Explicitly NULL values should refer to defaults
2021 global $wgDefaultUserOptions;
2022 if( is_null( $val ) && isset( $wgDefaultUserOptions[$oname] ) ) {
2023 $val = $wgDefaultUserOptions[$oname];
2026 $this->mOptions[$oname] = $val;
2030 * Reset all options to the site defaults
2032 function resetOptions() {
2033 $this->mOptions = User::getDefaultOptions();
2037 * Get the user's preferred date format.
2038 * @return String User's preferred date format
2040 function getDatePreference() {
2041 // Important migration for old data rows
2042 if ( is_null( $this->mDatePreference ) ) {
2043 global $wgLang;
2044 $value = $this->getOption( 'date' );
2045 $map = $wgLang->getDatePreferenceMigrationMap();
2046 if ( isset( $map[$value] ) ) {
2047 $value = $map[$value];
2049 $this->mDatePreference = $value;
2051 return $this->mDatePreference;
2055 * Get the user preferred stub threshold
2057 function getStubThreshold() {
2058 global $wgMaxArticleSize; # Maximum article size, in Kb
2059 $threshold = intval( $this->getOption( 'stubthreshold' ) );
2060 if ( $threshold > $wgMaxArticleSize * 1024 ) {
2061 # If they have set an impossible value, disable the preference
2062 # so we can use the parser cache again.
2063 $threshold = 0;
2065 return $threshold;
2069 * Get the permissions this user has.
2070 * @return Array of String permission names
2072 function getRights() {
2073 if ( is_null( $this->mRights ) ) {
2074 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
2075 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
2076 // Force reindexation of rights when a hook has unset one of them
2077 $this->mRights = array_values( $this->mRights );
2079 return $this->mRights;
2083 * Get the list of explicit group memberships this user has.
2084 * The implicit * and user groups are not included.
2085 * @return Array of String internal group names
2087 function getGroups() {
2088 $this->load();
2089 return $this->mGroups;
2093 * Get the list of implicit group memberships this user has.
2094 * This includes all explicit groups, plus 'user' if logged in,
2095 * '*' for all accounts, and autopromoted groups
2096 * @param $recache Bool Whether to avoid the cache
2097 * @return Array of String internal group names
2099 function getEffectiveGroups( $recache = false ) {
2100 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2101 wfProfileIn( __METHOD__ );
2102 $this->mEffectiveGroups = $this->getGroups();
2103 $this->mEffectiveGroups[] = '*';
2104 if( $this->getId() ) {
2105 $this->mEffectiveGroups[] = 'user';
2107 $this->mEffectiveGroups = array_unique( array_merge(
2108 $this->mEffectiveGroups,
2109 Autopromote::getAutopromoteGroups( $this )
2110 ) );
2112 # Hook for additional groups
2113 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2115 wfProfileOut( __METHOD__ );
2117 return $this->mEffectiveGroups;
2121 * Get the user's edit count.
2122 * @return Int
2124 function getEditCount() {
2125 if( $this->getId() ) {
2126 if ( !isset( $this->mEditCount ) ) {
2127 /* Populate the count, if it has not been populated yet */
2128 $this->mEditCount = User::edits( $this->mId );
2130 return $this->mEditCount;
2131 } else {
2132 /* nil */
2133 return null;
2138 * Add the user to the given group.
2139 * This takes immediate effect.
2140 * @param $group String Name of the group to add
2142 function addGroup( $group ) {
2143 $dbw = wfGetDB( DB_MASTER );
2144 if( $this->getId() ) {
2145 $dbw->insert( 'user_groups',
2146 array(
2147 'ug_user' => $this->getID(),
2148 'ug_group' => $group,
2150 __METHOD__,
2151 array( 'IGNORE' ) );
2154 $this->loadGroups();
2155 $this->mGroups[] = $group;
2156 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2158 $this->invalidateCache();
2162 * Remove the user from the given group.
2163 * This takes immediate effect.
2164 * @param $group String Name of the group to remove
2166 function removeGroup( $group ) {
2167 $this->load();
2168 $dbw = wfGetDB( DB_MASTER );
2169 $dbw->delete( 'user_groups',
2170 array(
2171 'ug_user' => $this->getID(),
2172 'ug_group' => $group,
2173 ), __METHOD__ );
2175 $this->loadGroups();
2176 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2177 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2179 $this->invalidateCache();
2183 * Get whether the user is logged in
2184 * @return Bool
2186 function isLoggedIn() {
2187 return $this->getID() != 0;
2191 * Get whether the user is anonymous
2192 * @return Bool
2194 function isAnon() {
2195 return !$this->isLoggedIn();
2199 * Check if user is allowed to access a feature / make an action
2200 * @param $action String action to be checked
2201 * @return Boolean: True if action is allowed, else false
2203 function isAllowed( $action = '' ) {
2204 if ( $action === '' ) {
2205 return true; // In the spirit of DWIM
2207 # Patrolling may not be enabled
2208 if( $action === 'patrol' || $action === 'autopatrol' ) {
2209 global $wgUseRCPatrol, $wgUseNPPatrol;
2210 if( !$wgUseRCPatrol && !$wgUseNPPatrol )
2211 return false;
2213 # Use strict parameter to avoid matching numeric 0 accidentally inserted
2214 # by misconfiguration: 0 == 'foo'
2215 return in_array( $action, $this->getRights(), true );
2219 * Check whether to enable recent changes patrol features for this user
2220 * @return Boolean: True or false
2222 public function useRCPatrol() {
2223 global $wgUseRCPatrol;
2224 return( $wgUseRCPatrol && ( $this->isAllowed( 'patrol' ) || $this->isAllowed( 'patrolmarks' ) ) );
2228 * Check whether to enable new pages patrol features for this user
2229 * @return Bool True or false
2231 public function useNPPatrol() {
2232 global $wgUseRCPatrol, $wgUseNPPatrol;
2233 return( ( $wgUseRCPatrol || $wgUseNPPatrol ) && ( $this->isAllowed( 'patrol' ) || $this->isAllowed( 'patrolmarks' ) ) );
2237 * Get the current skin, loading it if required, and setting a title
2238 * @param $t Title: the title to use in the skin
2239 * @return Skin The current skin
2240 * @todo: FIXME : need to check the old failback system [AV]
2242 function getSkin( $t = null ) {
2243 if ( $t ) {
2244 $skin = $this->createSkinObject();
2245 $skin->setTitle( $t );
2246 return $skin;
2247 } else {
2248 if ( !$this->mSkin ) {
2249 $this->mSkin = $this->createSkinObject();
2252 if ( !$this->mSkin->getTitle() ) {
2253 global $wgOut;
2254 $t = $wgOut->getTitle();
2255 $this->mSkin->setTitle($t);
2258 return $this->mSkin;
2262 // Creates a Skin object, for getSkin()
2263 private function createSkinObject() {
2264 wfProfileIn( __METHOD__ );
2266 global $wgHiddenPrefs;
2267 if( !in_array( 'skin', $wgHiddenPrefs ) ) {
2268 global $wgRequest;
2269 # get the user skin
2270 $userSkin = $this->getOption( 'skin' );
2271 $userSkin = $wgRequest->getVal( 'useskin', $userSkin );
2272 } else {
2273 # if we're not allowing users to override, then use the default
2274 global $wgDefaultSkin;
2275 $userSkin = $wgDefaultSkin;
2278 $skin = Skin::newFromKey( $userSkin );
2279 wfProfileOut( __METHOD__ );
2281 return $skin;
2285 * Check the watched status of an article.
2286 * @param $title Title of the article to look at
2287 * @return Bool
2289 function isWatched( $title ) {
2290 $wl = WatchedItem::fromUserTitle( $this, $title );
2291 return $wl->isWatched();
2295 * Watch an article.
2296 * @param $title Title of the article to look at
2298 function addWatch( $title ) {
2299 $wl = WatchedItem::fromUserTitle( $this, $title );
2300 $wl->addWatch();
2301 $this->invalidateCache();
2305 * Stop watching an article.
2306 * @param $title Title of the article to look at
2308 function removeWatch( $title ) {
2309 $wl = WatchedItem::fromUserTitle( $this, $title );
2310 $wl->removeWatch();
2311 $this->invalidateCache();
2315 * Clear the user's notification timestamp for the given title.
2316 * If e-notif e-mails are on, they will receive notification mails on
2317 * the next change of the page if it's watched etc.
2318 * @param $title Title of the article to look at
2320 function clearNotification( &$title ) {
2321 global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2323 # Do nothing if the database is locked to writes
2324 if( wfReadOnly() ) {
2325 return;
2328 if( $title->getNamespace() == NS_USER_TALK &&
2329 $title->getText() == $this->getName() ) {
2330 if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) )
2331 return;
2332 $this->setNewtalk( false );
2335 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2336 return;
2339 if( $this->isAnon() ) {
2340 // Nothing else to do...
2341 return;
2344 // Only update the timestamp if the page is being watched.
2345 // The query to find out if it is watched is cached both in memcached and per-invocation,
2346 // and when it does have to be executed, it can be on a slave
2347 // If this is the user's newtalk page, we always update the timestamp
2348 if( $title->getNamespace() == NS_USER_TALK &&
2349 $title->getText() == $wgUser->getName() )
2351 $watched = true;
2352 } elseif ( $this->getId() == $wgUser->getId() ) {
2353 $watched = $title->userIsWatching();
2354 } else {
2355 $watched = true;
2358 // If the page is watched by the user (or may be watched), update the timestamp on any
2359 // any matching rows
2360 if ( $watched ) {
2361 $dbw = wfGetDB( DB_MASTER );
2362 $dbw->update( 'watchlist',
2363 array( /* SET */
2364 'wl_notificationtimestamp' => null
2365 ), array( /* WHERE */
2366 'wl_title' => $title->getDBkey(),
2367 'wl_namespace' => $title->getNamespace(),
2368 'wl_user' => $this->getID()
2369 ), __METHOD__
2375 * Resets all of the given user's page-change notification timestamps.
2376 * If e-notif e-mails are on, they will receive notification mails on
2377 * the next change of any watched page.
2379 * @param $currentUser Int User ID
2381 function clearAllNotifications( $currentUser ) {
2382 global $wgUseEnotif, $wgShowUpdatedMarker;
2383 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2384 $this->setNewtalk( false );
2385 return;
2387 if( $currentUser != 0 ) {
2388 $dbw = wfGetDB( DB_MASTER );
2389 $dbw->update( 'watchlist',
2390 array( /* SET */
2391 'wl_notificationtimestamp' => null
2392 ), array( /* WHERE */
2393 'wl_user' => $currentUser
2394 ), __METHOD__
2396 # We also need to clear here the "you have new message" notification for the own user_talk page
2397 # This is cleared one page view later in Article::viewUpdates();
2402 * Set this user's options from an encoded string
2403 * @param $str String Encoded options to import
2404 * @private
2406 function decodeOptions( $str ) {
2407 if( !$str )
2408 return;
2410 $this->mOptionsLoaded = true;
2411 $this->mOptionOverrides = array();
2413 // If an option is not set in $str, use the default value
2414 $this->mOptions = self::getDefaultOptions();
2416 $a = explode( "\n", $str );
2417 foreach ( $a as $s ) {
2418 $m = array();
2419 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2420 $this->mOptions[$m[1]] = $m[2];
2421 $this->mOptionOverrides[$m[1]] = $m[2];
2427 * Set a cookie on the user's client. Wrapper for
2428 * WebResponse::setCookie
2429 * @param $name String Name of the cookie to set
2430 * @param $value String Value to set
2431 * @param $exp Int Expiration time, as a UNIX time value;
2432 * if 0 or not specified, use the default $wgCookieExpiration
2434 protected function setCookie( $name, $value, $exp = 0 ) {
2435 global $wgRequest;
2436 $wgRequest->response()->setcookie( $name, $value, $exp );
2440 * Clear a cookie on the user's client
2441 * @param $name String Name of the cookie to clear
2443 protected function clearCookie( $name ) {
2444 $this->setCookie( $name, '', time() - 86400 );
2448 * Set the default cookies for this session on the user's client.
2450 function setCookies() {
2451 $this->load();
2452 if ( 0 == $this->mId ) return;
2453 $session = array(
2454 'wsUserID' => $this->mId,
2455 'wsToken' => $this->mToken,
2456 'wsUserName' => $this->getName()
2458 $cookies = array(
2459 'UserID' => $this->mId,
2460 'UserName' => $this->getName(),
2462 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2463 $cookies['Token'] = $this->mToken;
2464 } else {
2465 $cookies['Token'] = false;
2468 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2469 #check for null, since the hook could cause a null value
2470 if ( !is_null( $session ) && isset( $_SESSION ) ){
2471 $_SESSION = $session + $_SESSION;
2473 foreach ( $cookies as $name => $value ) {
2474 if ( $value === false ) {
2475 $this->clearCookie( $name );
2476 } else {
2477 $this->setCookie( $name, $value );
2483 * Log this user out.
2485 function logout() {
2486 if( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
2487 $this->doLogout();
2492 * Clear the user's cookies and session, and reset the instance cache.
2493 * @private
2494 * @see logout()
2496 function doLogout() {
2497 $this->clearInstanceCache( 'defaults' );
2499 $_SESSION['wsUserID'] = 0;
2501 $this->clearCookie( 'UserID' );
2502 $this->clearCookie( 'Token' );
2504 # Remember when user logged out, to prevent seeing cached pages
2505 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2509 * Save this user's settings into the database.
2510 * @todo Only rarely do all these fields need to be set!
2512 function saveSettings() {
2513 $this->load();
2514 if ( wfReadOnly() ) { return; }
2515 if ( 0 == $this->mId ) { return; }
2517 $this->mTouched = self::newTouchedTimestamp();
2519 $dbw = wfGetDB( DB_MASTER );
2520 $dbw->update( 'user',
2521 array( /* SET */
2522 'user_name' => $this->mName,
2523 'user_password' => $this->mPassword,
2524 'user_newpassword' => $this->mNewpassword,
2525 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2526 'user_real_name' => $this->mRealName,
2527 'user_email' => $this->mEmail,
2528 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2529 'user_options' => '',
2530 'user_touched' => $dbw->timestamp( $this->mTouched ),
2531 'user_token' => $this->mToken,
2532 'user_email_token' => $this->mEmailToken,
2533 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2534 ), array( /* WHERE */
2535 'user_id' => $this->mId
2536 ), __METHOD__
2539 $this->saveOptions();
2541 wfRunHooks( 'UserSaveSettings', array( $this ) );
2542 $this->clearSharedCache();
2543 $this->getUserPage()->invalidateCache();
2547 * If only this user's username is known, and it exists, return the user ID.
2548 * @return Int
2550 function idForName() {
2551 $s = trim( $this->getName() );
2552 if ( $s === '' ) return 0;
2554 $dbr = wfGetDB( DB_SLAVE );
2555 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2556 if ( $id === false ) {
2557 $id = 0;
2559 return $id;
2563 * Add a user to the database, return the user object
2565 * @param $name String Username to add
2566 * @param $params Array of Strings Non-default parameters to save to the database:
2567 * - password The user's password. Password logins will be disabled if this is omitted.
2568 * - newpassword A temporary password mailed to the user
2569 * - email The user's email address
2570 * - email_authenticated The email authentication timestamp
2571 * - real_name The user's real name
2572 * - options An associative array of non-default options
2573 * - token Random authentication token. Do not set.
2574 * - registration Registration timestamp. Do not set.
2576 * @return User object, or null if the username already exists
2578 static function createNew( $name, $params = array() ) {
2579 $user = new User;
2580 $user->load();
2581 if ( isset( $params['options'] ) ) {
2582 $user->mOptions = $params['options'] + (array)$user->mOptions;
2583 unset( $params['options'] );
2585 $dbw = wfGetDB( DB_MASTER );
2586 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2588 $fields = array(
2589 'user_id' => $seqVal,
2590 'user_name' => $name,
2591 'user_password' => $user->mPassword,
2592 'user_newpassword' => $user->mNewpassword,
2593 'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
2594 'user_email' => $user->mEmail,
2595 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2596 'user_real_name' => $user->mRealName,
2597 'user_options' => '',
2598 'user_token' => $user->mToken,
2599 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2600 'user_editcount' => 0,
2602 foreach ( $params as $name => $value ) {
2603 $fields["user_$name"] = $value;
2605 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2606 if ( $dbw->affectedRows() ) {
2607 $newUser = User::newFromId( $dbw->insertId() );
2608 } else {
2609 $newUser = null;
2611 return $newUser;
2615 * Add this existing user object to the database
2617 function addToDatabase() {
2618 $this->load();
2619 $dbw = wfGetDB( DB_MASTER );
2620 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2621 $dbw->insert( 'user',
2622 array(
2623 'user_id' => $seqVal,
2624 'user_name' => $this->mName,
2625 'user_password' => $this->mPassword,
2626 'user_newpassword' => $this->mNewpassword,
2627 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2628 'user_email' => $this->mEmail,
2629 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2630 'user_real_name' => $this->mRealName,
2631 'user_options' => '',
2632 'user_token' => $this->mToken,
2633 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2634 'user_editcount' => 0,
2635 ), __METHOD__
2637 $this->mId = $dbw->insertId();
2639 // Clear instance cache other than user table data, which is already accurate
2640 $this->clearInstanceCache();
2642 $this->saveOptions();
2646 * If this (non-anonymous) user is blocked, block any IP address
2647 * they've successfully logged in from.
2649 function spreadBlock() {
2650 wfDebug( __METHOD__ . "()\n" );
2651 $this->load();
2652 if ( $this->mId == 0 ) {
2653 return;
2656 $userblock = Block::newFromDB( '', $this->mId );
2657 if ( !$userblock ) {
2658 return;
2661 $userblock->doAutoblock( wfGetIP() );
2665 * Generate a string which will be different for any combination of
2666 * user options which would produce different parser output.
2667 * This will be used as part of the hash key for the parser cache,
2668 * so users with the same options can share the same cached data
2669 * safely.
2671 * Extensions which require it should install 'PageRenderingHash' hook,
2672 * which will give them a chance to modify this key based on their own
2673 * settings.
2675 * @deprecated @since 1.17 use the ParserOptions object to get the relevant options
2676 * @return String Page rendering hash
2678 function getPageRenderingHash() {
2679 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2680 if( $this->mHash ){
2681 return $this->mHash;
2683 wfDeprecated( __METHOD__ );
2685 // stubthreshold is only included below for completeness,
2686 // since it disables the parser cache, its value will always
2687 // be 0 when this function is called by parsercache.
2689 $confstr = $this->getOption( 'math' );
2690 $confstr .= '!' . $this->getStubThreshold();
2691 if ( $wgUseDynamicDates ) { # This is wrong (bug 24714)
2692 $confstr .= '!' . $this->getDatePreference();
2694 $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' );
2695 $confstr .= '!' . $wgLang->getCode();
2696 $confstr .= '!' . $this->getOption( 'thumbsize' );
2697 // add in language specific options, if any
2698 $extra = $wgContLang->getExtraHashOptions();
2699 $confstr .= $extra;
2701 // Since the skin could be overloading link(), it should be
2702 // included here but in practice, none of our skins do that.
2704 $confstr .= $wgRenderHashAppend;
2706 // Give a chance for extensions to modify the hash, if they have
2707 // extra options or other effects on the parser cache.
2708 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2710 // Make it a valid memcached key fragment
2711 $confstr = str_replace( ' ', '_', $confstr );
2712 $this->mHash = $confstr;
2713 return $confstr;
2717 * Get whether the user is explicitly blocked from account creation.
2718 * @return Bool
2720 function isBlockedFromCreateAccount() {
2721 $this->getBlockedStatus();
2722 return $this->mBlock && $this->mBlock->mCreateAccount;
2726 * Get whether the user is blocked from using Special:Emailuser.
2727 * @return Bool
2729 function isBlockedFromEmailuser() {
2730 $this->getBlockedStatus();
2731 return $this->mBlock && $this->mBlock->mBlockEmail;
2735 * Get whether the user is allowed to create an account.
2736 * @return Bool
2738 function isAllowedToCreateAccount() {
2739 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2743 * Get this user's personal page title.
2745 * @return Title: User's personal page title
2747 function getUserPage() {
2748 return Title::makeTitle( NS_USER, $this->getName() );
2752 * Get this user's talk page title.
2754 * @return Title: User's talk page title
2756 function getTalkPage() {
2757 $title = $this->getUserPage();
2758 return $title->getTalkPage();
2762 * Get the maximum valid user ID.
2763 * @return Integer: User ID
2764 * @static
2766 function getMaxID() {
2767 static $res; // cache
2769 if ( isset( $res ) ) {
2770 return $res;
2771 } else {
2772 $dbr = wfGetDB( DB_SLAVE );
2773 return $res = $dbr->selectField( 'user', 'max(user_id)', false, __METHOD__ );
2778 * Determine whether the user is a newbie. Newbies are either
2779 * anonymous IPs, or the most recently created accounts.
2780 * @return Bool
2782 function isNewbie() {
2783 return !$this->isAllowed( 'autoconfirmed' );
2787 * Check to see if the given clear-text password is one of the accepted passwords
2788 * @param $password String: user password.
2789 * @return Boolean: True if the given password is correct, otherwise False.
2791 function checkPassword( $password ) {
2792 global $wgAuth;
2793 $this->load();
2795 // Even though we stop people from creating passwords that
2796 // are shorter than this, doesn't mean people wont be able
2797 // to. Certain authentication plugins do NOT want to save
2798 // domain passwords in a mysql database, so we should
2799 // check this (in case $wgAuth->strict() is false).
2800 if( !$this->isValidPassword( $password ) ) {
2801 return false;
2804 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2805 return true;
2806 } elseif( $wgAuth->strict() ) {
2807 /* Auth plugin doesn't allow local authentication */
2808 return false;
2809 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2810 /* Auth plugin doesn't allow local authentication for this user name */
2811 return false;
2813 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2814 return true;
2815 } elseif ( function_exists( 'iconv' ) ) {
2816 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2817 # Check for this with iconv
2818 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2819 if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2820 return true;
2823 return false;
2827 * Check if the given clear-text password matches the temporary password
2828 * sent by e-mail for password reset operations.
2829 * @return Boolean: True if matches, false otherwise
2831 function checkTemporaryPassword( $plaintext ) {
2832 global $wgNewPasswordExpiry;
2833 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
2834 if ( is_null( $this->mNewpassTime ) ) {
2835 return true;
2837 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
2838 return ( time() < $expiry );
2839 } else {
2840 return false;
2845 * Initialize (if necessary) and return a session token value
2846 * which can be used in edit forms to show that the user's
2847 * login credentials aren't being hijacked with a foreign form
2848 * submission.
2850 * @param $salt String|Array of Strings Optional function-specific data for hashing
2851 * @return String The new edit token
2853 function editToken( $salt = '' ) {
2854 if ( $this->isAnon() ) {
2855 return EDIT_TOKEN_SUFFIX;
2856 } else {
2857 if( !isset( $_SESSION['wsEditToken'] ) ) {
2858 $token = self::generateToken();
2859 $_SESSION['wsEditToken'] = $token;
2860 } else {
2861 $token = $_SESSION['wsEditToken'];
2863 if( is_array( $salt ) ) {
2864 $salt = implode( '|', $salt );
2866 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2871 * Generate a looking random token for various uses.
2873 * @param $salt String Optional salt value
2874 * @return String The new random token
2876 public static function generateToken( $salt = '' ) {
2877 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2878 return md5( $token . $salt );
2882 * Check given value against the token value stored in the session.
2883 * A match should confirm that the form was submitted from the
2884 * user's own login session, not a form submission from a third-party
2885 * site.
2887 * @param $val String Input value to compare
2888 * @param $salt String Optional function-specific data for hashing
2889 * @return Boolean: Whether the token matches
2891 function matchEditToken( $val, $salt = '' ) {
2892 $sessionToken = $this->editToken( $salt );
2893 if ( $val != $sessionToken ) {
2894 wfDebug( "User::matchEditToken: broken session data\n" );
2896 return $val == $sessionToken;
2900 * Check given value against the token value stored in the session,
2901 * ignoring the suffix.
2903 * @param $val String Input value to compare
2904 * @param $salt String Optional function-specific data for hashing
2905 * @return Boolean: Whether the token matches
2907 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2908 $sessionToken = $this->editToken( $salt );
2909 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2913 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2914 * mail to the user's given address.
2916 * @param $changed Boolean: whether the adress changed
2917 * @return Status object
2919 function sendConfirmationMail( $changed = false ) {
2920 global $wgLang;
2921 $expiration = null; // gets passed-by-ref and defined in next line.
2922 $token = $this->confirmationToken( $expiration );
2923 $url = $this->confirmationTokenUrl( $token );
2924 $invalidateURL = $this->invalidationTokenUrl( $token );
2925 $this->saveSettings();
2927 $message = $changed ? 'confirmemail_body_changed' : 'confirmemail_body';
2928 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2929 wfMsg( $message,
2930 wfGetIP(),
2931 $this->getName(),
2932 $url,
2933 $wgLang->timeanddate( $expiration, false ),
2934 $invalidateURL,
2935 $wgLang->date( $expiration, false ),
2936 $wgLang->time( $expiration, false ) ) );
2940 * Send an e-mail to this user's account. Does not check for
2941 * confirmed status or validity.
2943 * @param $subject String Message subject
2944 * @param $body String Message body
2945 * @param $from String Optional From address; if unspecified, default $wgPasswordSender will be used
2946 * @param $replyto String Reply-To address
2947 * @return Status
2949 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2950 if( is_null( $from ) ) {
2951 global $wgPasswordSender, $wgPasswordSenderName;
2952 $sender = new MailAddress( $wgPasswordSender, $wgPasswordSenderName );
2953 } else {
2954 $sender = new MailAddress( $from );
2957 $to = new MailAddress( $this );
2958 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2962 * Generate, store, and return a new e-mail confirmation code.
2963 * A hash (unsalted, since it's used as a key) is stored.
2965 * @note Call saveSettings() after calling this function to commit
2966 * this change to the database.
2968 * @param[out] &$expiration \mixed Accepts the expiration time
2969 * @return String New token
2970 * @private
2972 function confirmationToken( &$expiration ) {
2973 $now = time();
2974 $expires = $now + 7 * 24 * 60 * 60;
2975 $expiration = wfTimestamp( TS_MW, $expires );
2976 $token = self::generateToken( $this->mId . $this->mEmail . $expires );
2977 $hash = md5( $token );
2978 $this->load();
2979 $this->mEmailToken = $hash;
2980 $this->mEmailTokenExpires = $expiration;
2981 return $token;
2985 * Return a URL the user can use to confirm their email address.
2986 * @param $token String Accepts the email confirmation token
2987 * @return String New token URL
2988 * @private
2990 function confirmationTokenUrl( $token ) {
2991 return $this->getTokenUrl( 'ConfirmEmail', $token );
2995 * Return a URL the user can use to invalidate their email address.
2996 * @param $token String Accepts the email confirmation token
2997 * @return String New token URL
2998 * @private
3000 function invalidationTokenUrl( $token ) {
3001 return $this->getTokenUrl( 'Invalidateemail', $token );
3005 * Internal function to format the e-mail validation/invalidation URLs.
3006 * This uses $wgArticlePath directly as a quickie hack to use the
3007 * hardcoded English names of the Special: pages, for ASCII safety.
3009 * @note Since these URLs get dropped directly into emails, using the
3010 * short English names avoids insanely long URL-encoded links, which
3011 * also sometimes can get corrupted in some browsers/mailers
3012 * (bug 6957 with Gmail and Internet Explorer).
3014 * @param $page String Special page
3015 * @param $token String Token
3016 * @return String Formatted URL
3018 protected function getTokenUrl( $page, $token ) {
3019 global $wgArticlePath;
3020 return wfExpandUrl(
3021 str_replace(
3022 '$1',
3023 "Special:$page/$token",
3024 $wgArticlePath ) );
3028 * Mark the e-mail address confirmed.
3030 * @note Call saveSettings() after calling this function to commit the change.
3032 function confirmEmail() {
3033 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
3034 wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
3035 return true;
3039 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
3040 * address if it was already confirmed.
3042 * @note Call saveSettings() after calling this function to commit the change.
3044 function invalidateEmail() {
3045 $this->load();
3046 $this->mEmailToken = null;
3047 $this->mEmailTokenExpires = null;
3048 $this->setEmailAuthenticationTimestamp( null );
3049 wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
3050 return true;
3054 * Set the e-mail authentication timestamp.
3055 * @param $timestamp String TS_MW timestamp
3057 function setEmailAuthenticationTimestamp( $timestamp ) {
3058 $this->load();
3059 $this->mEmailAuthenticated = $timestamp;
3060 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
3064 * Is this user allowed to send e-mails within limits of current
3065 * site configuration?
3066 * @return Bool
3068 function canSendEmail() {
3069 global $wgEnableEmail, $wgEnableUserEmail;
3070 if( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
3071 return false;
3073 $canSend = $this->isEmailConfirmed();
3074 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
3075 return $canSend;
3079 * Is this user allowed to receive e-mails within limits of current
3080 * site configuration?
3081 * @return Bool
3083 function canReceiveEmail() {
3084 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
3088 * Is this user's e-mail address valid-looking and confirmed within
3089 * limits of the current site configuration?
3091 * @note If $wgEmailAuthentication is on, this may require the user to have
3092 * confirmed their address by returning a code or using a password
3093 * sent to the address from the wiki.
3095 * @return Bool
3097 function isEmailConfirmed() {
3098 global $wgEmailAuthentication;
3099 $this->load();
3100 $confirmed = true;
3101 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
3102 if( $this->isAnon() )
3103 return false;
3104 if( !self::isValidEmailAddr( $this->mEmail ) )
3105 return false;
3106 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
3107 return false;
3108 return true;
3109 } else {
3110 return $confirmed;
3115 * Check whether there is an outstanding request for e-mail confirmation.
3116 * @return Bool
3118 function isEmailConfirmationPending() {
3119 global $wgEmailAuthentication;
3120 return $wgEmailAuthentication &&
3121 !$this->isEmailConfirmed() &&
3122 $this->mEmailToken &&
3123 $this->mEmailTokenExpires > wfTimestamp();
3127 * Get the timestamp of account creation.
3129 * @return String|Bool Timestamp of account creation, or false for
3130 * non-existent/anonymous user accounts.
3132 public function getRegistration() {
3133 return $this->getId() > 0
3134 ? $this->mRegistration
3135 : false;
3139 * Get the timestamp of the first edit
3141 * @return String|Bool Timestamp of first edit, or false for
3142 * non-existent/anonymous user accounts.
3144 public function getFirstEditTimestamp() {
3145 if( $this->getId() == 0 ) {
3146 return false; // anons
3148 $dbr = wfGetDB( DB_SLAVE );
3149 $time = $dbr->selectField( 'revision', 'rev_timestamp',
3150 array( 'rev_user' => $this->getId() ),
3151 __METHOD__,
3152 array( 'ORDER BY' => 'rev_timestamp ASC' )
3154 if( !$time ) {
3155 return false; // no edits
3157 return wfTimestamp( TS_MW, $time );
3161 * Get the permissions associated with a given list of groups
3163 * @param $groups Array of Strings List of internal group names
3164 * @return Array of Strings List of permission key names for given groups combined
3166 static function getGroupPermissions( $groups ) {
3167 global $wgGroupPermissions, $wgRevokePermissions;
3168 $rights = array();
3169 // grant every granted permission first
3170 foreach( $groups as $group ) {
3171 if( isset( $wgGroupPermissions[$group] ) ) {
3172 $rights = array_merge( $rights,
3173 // array_filter removes empty items
3174 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
3177 // now revoke the revoked permissions
3178 foreach( $groups as $group ) {
3179 if( isset( $wgRevokePermissions[$group] ) ) {
3180 $rights = array_diff( $rights,
3181 array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
3184 return array_unique( $rights );
3188 * Get all the groups who have a given permission
3190 * @param $role String Role to check
3191 * @return Array of Strings List of internal group names with the given permission
3193 static function getGroupsWithPermission( $role ) {
3194 global $wgGroupPermissions;
3195 $allowedGroups = array();
3196 foreach ( $wgGroupPermissions as $group => $rights ) {
3197 if ( isset( $rights[$role] ) && $rights[$role] ) {
3198 $allowedGroups[] = $group;
3201 return $allowedGroups;
3205 * Get the localized descriptive name for a group, if it exists
3207 * @param $group String Internal group name
3208 * @return String Localized descriptive group name
3210 static function getGroupName( $group ) {
3211 $msg = wfMessage( "group-$group" );
3212 return $msg->isBlank() ? $group : $msg->text();
3216 * Get the localized descriptive name for a member of a group, if it exists
3218 * @param $group String Internal group name
3219 * @return String Localized name for group member
3221 static function getGroupMember( $group ) {
3222 $msg = wfMessage( "group-$group-member" );
3223 return $msg->isBlank() ? $group : $msg->text();
3227 * Return the set of defined explicit groups.
3228 * The implicit groups (by default *, 'user' and 'autoconfirmed')
3229 * are not included, as they are defined automatically, not in the database.
3230 * @return Array of internal group names
3232 static function getAllGroups() {
3233 global $wgGroupPermissions, $wgRevokePermissions;
3234 return array_diff(
3235 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
3236 self::getImplicitGroups()
3241 * Get a list of all available permissions.
3242 * @return Array of permission names
3244 static function getAllRights() {
3245 if ( self::$mAllRights === false ) {
3246 global $wgAvailableRights;
3247 if ( count( $wgAvailableRights ) ) {
3248 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3249 } else {
3250 self::$mAllRights = self::$mCoreRights;
3252 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3254 return self::$mAllRights;
3258 * Get a list of implicit groups
3259 * @return Array of Strings Array of internal group names
3261 public static function getImplicitGroups() {
3262 global $wgImplicitGroups;
3263 $groups = $wgImplicitGroups;
3264 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
3265 return $groups;
3269 * Get the title of a page describing a particular group
3271 * @param $group String Internal group name
3272 * @return Title|Bool Title of the page if it exists, false otherwise
3274 static function getGroupPage( $group ) {
3275 $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
3276 if( $msg->exists() ) {
3277 $title = Title::newFromText( $msg->text() );
3278 if( is_object( $title ) )
3279 return $title;
3281 return false;
3285 * Create a link to the group in HTML, if available;
3286 * else return the group name.
3288 * @param $group String Internal name of the group
3289 * @param $text String The text of the link
3290 * @return String HTML link to the group
3292 static function makeGroupLinkHTML( $group, $text = '' ) {
3293 if( $text == '' ) {
3294 $text = self::getGroupName( $group );
3296 $title = self::getGroupPage( $group );
3297 if( $title ) {
3298 global $wgUser;
3299 $sk = $wgUser->getSkin();
3300 return $sk->link( $title, htmlspecialchars( $text ) );
3301 } else {
3302 return $text;
3307 * Create a link to the group in Wikitext, if available;
3308 * else return the group name.
3310 * @param $group String Internal name of the group
3311 * @param $text String The text of the link
3312 * @return String Wikilink to the group
3314 static function makeGroupLinkWiki( $group, $text = '' ) {
3315 if( $text == '' ) {
3316 $text = self::getGroupName( $group );
3318 $title = self::getGroupPage( $group );
3319 if( $title ) {
3320 $page = $title->getPrefixedText();
3321 return "[[$page|$text]]";
3322 } else {
3323 return $text;
3328 * Returns an array of the groups that a particular group can add/remove.
3330 * @param $group String: the group to check for whether it can add/remove
3331 * @return Array array( 'add' => array( addablegroups ),
3332 * 'remove' => array( removablegroups ),
3333 * 'add-self' => array( addablegroups to self),
3334 * 'remove-self' => array( removable groups from self) )
3336 static function changeableByGroup( $group ) {
3337 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
3339 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
3340 if( empty( $wgAddGroups[$group] ) ) {
3341 // Don't add anything to $groups
3342 } elseif( $wgAddGroups[$group] === true ) {
3343 // You get everything
3344 $groups['add'] = self::getAllGroups();
3345 } elseif( is_array( $wgAddGroups[$group] ) ) {
3346 $groups['add'] = $wgAddGroups[$group];
3349 // Same thing for remove
3350 if( empty( $wgRemoveGroups[$group] ) ) {
3351 } elseif( $wgRemoveGroups[$group] === true ) {
3352 $groups['remove'] = self::getAllGroups();
3353 } elseif( is_array( $wgRemoveGroups[$group] ) ) {
3354 $groups['remove'] = $wgRemoveGroups[$group];
3357 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
3358 if( empty( $wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
3359 foreach( $wgGroupsAddToSelf as $key => $value ) {
3360 if( is_int( $key ) ) {
3361 $wgGroupsAddToSelf['user'][] = $value;
3366 if( empty( $wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
3367 foreach( $wgGroupsRemoveFromSelf as $key => $value ) {
3368 if( is_int( $key ) ) {
3369 $wgGroupsRemoveFromSelf['user'][] = $value;
3374 // Now figure out what groups the user can add to him/herself
3375 if( empty( $wgGroupsAddToSelf[$group] ) ) {
3376 } elseif( $wgGroupsAddToSelf[$group] === true ) {
3377 // No idea WHY this would be used, but it's there
3378 $groups['add-self'] = User::getAllGroups();
3379 } elseif( is_array( $wgGroupsAddToSelf[$group] ) ) {
3380 $groups['add-self'] = $wgGroupsAddToSelf[$group];
3383 if( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
3384 } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
3385 $groups['remove-self'] = User::getAllGroups();
3386 } elseif( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
3387 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
3390 return $groups;
3394 * Returns an array of groups that this user can add and remove
3395 * @return Array array( 'add' => array( addablegroups ),
3396 * 'remove' => array( removablegroups ),
3397 * 'add-self' => array( addablegroups to self),
3398 * 'remove-self' => array( removable groups from self) )
3400 function changeableGroups() {
3401 if( $this->isAllowed( 'userrights' ) ) {
3402 // This group gives the right to modify everything (reverse-
3403 // compatibility with old "userrights lets you change
3404 // everything")
3405 // Using array_merge to make the groups reindexed
3406 $all = array_merge( User::getAllGroups() );
3407 return array(
3408 'add' => $all,
3409 'remove' => $all,
3410 'add-self' => array(),
3411 'remove-self' => array()
3415 // Okay, it's not so simple, we will have to go through the arrays
3416 $groups = array(
3417 'add' => array(),
3418 'remove' => array(),
3419 'add-self' => array(),
3420 'remove-self' => array()
3422 $addergroups = $this->getEffectiveGroups();
3424 foreach( $addergroups as $addergroup ) {
3425 $groups = array_merge_recursive(
3426 $groups, $this->changeableByGroup( $addergroup )
3428 $groups['add'] = array_unique( $groups['add'] );
3429 $groups['remove'] = array_unique( $groups['remove'] );
3430 $groups['add-self'] = array_unique( $groups['add-self'] );
3431 $groups['remove-self'] = array_unique( $groups['remove-self'] );
3433 return $groups;
3437 * Increment the user's edit-count field.
3438 * Will have no effect for anonymous users.
3440 function incEditCount() {
3441 if( !$this->isAnon() ) {
3442 $dbw = wfGetDB( DB_MASTER );
3443 $dbw->update( 'user',
3444 array( 'user_editcount=user_editcount+1' ),
3445 array( 'user_id' => $this->getId() ),
3446 __METHOD__ );
3448 // Lazy initialization check...
3449 if( $dbw->affectedRows() == 0 ) {
3450 // Pull from a slave to be less cruel to servers
3451 // Accuracy isn't the point anyway here
3452 $dbr = wfGetDB( DB_SLAVE );
3453 $count = $dbr->selectField( 'revision',
3454 'COUNT(rev_user)',
3455 array( 'rev_user' => $this->getId() ),
3456 __METHOD__ );
3458 // Now here's a goddamn hack...
3459 if( $dbr !== $dbw ) {
3460 // If we actually have a slave server, the count is
3461 // at least one behind because the current transaction
3462 // has not been committed and replicated.
3463 $count++;
3464 } else {
3465 // But if DB_SLAVE is selecting the master, then the
3466 // count we just read includes the revision that was
3467 // just added in the working transaction.
3470 $dbw->update( 'user',
3471 array( 'user_editcount' => $count ),
3472 array( 'user_id' => $this->getId() ),
3473 __METHOD__ );
3476 // edit count in user cache too
3477 $this->invalidateCache();
3481 * Get the description of a given right
3483 * @param $right String Right to query
3484 * @return String Localized description of the right
3486 static function getRightDescription( $right ) {
3487 $key = "right-$right";
3488 $name = wfMsg( $key );
3489 return $name == '' || wfEmptyMsg( $key, $name )
3490 ? $right
3491 : $name;
3495 * Make an old-style password hash
3497 * @param $password String Plain-text password
3498 * @param $userId String User ID
3499 * @return String Password hash
3501 static function oldCrypt( $password, $userId ) {
3502 global $wgPasswordSalt;
3503 if ( $wgPasswordSalt ) {
3504 return md5( $userId . '-' . md5( $password ) );
3505 } else {
3506 return md5( $password );
3511 * Make a new-style password hash
3513 * @param $password String Plain-text password
3514 * @param $salt String Optional salt, may be random or the user ID.
3515 * If unspecified or false, will generate one automatically
3516 * @return String Password hash
3518 static function crypt( $password, $salt = false ) {
3519 global $wgPasswordSalt;
3521 $hash = '';
3522 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
3523 return $hash;
3526 if( $wgPasswordSalt ) {
3527 if ( $salt === false ) {
3528 $salt = substr( wfGenerateToken(), 0, 8 );
3530 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3531 } else {
3532 return ':A:' . md5( $password );
3537 * Compare a password hash with a plain-text password. Requires the user
3538 * ID if there's a chance that the hash is an old-style hash.
3540 * @param $hash String Password hash
3541 * @param $password String Plain-text password to compare
3542 * @param $userId String User ID for old-style password salt
3543 * @return Boolean:
3545 static function comparePasswords( $hash, $password, $userId = false ) {
3546 $type = substr( $hash, 0, 3 );
3548 $result = false;
3549 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
3550 return $result;
3553 if ( $type == ':A:' ) {
3554 # Unsalted
3555 return md5( $password ) === substr( $hash, 3 );
3556 } elseif ( $type == ':B:' ) {
3557 # Salted
3558 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3559 return md5( $salt.'-'.md5( $password ) ) == $realHash;
3560 } else {
3561 # Old-style
3562 return self::oldCrypt( $password, $userId ) === $hash;
3567 * Add a newuser log entry for this user
3569 * @param $byEmail Boolean: account made by email?
3570 * @param $reason String: user supplied reason
3572 public function addNewUserLogEntry( $byEmail = false, $reason = '' ) {
3573 global $wgUser, $wgContLang, $wgNewUserLog;
3574 if( empty( $wgNewUserLog ) ) {
3575 return true; // disabled
3578 if( $this->getName() == $wgUser->getName() ) {
3579 $action = 'create';
3580 } else {
3581 $action = 'create2';
3582 if ( $byEmail ) {
3583 if ( $reason === '' ) {
3584 $reason = wfMsgForContent( 'newuserlog-byemail' );
3585 } else {
3586 $reason = $wgContLang->commaList( array(
3587 $reason, wfMsgForContent( 'newuserlog-byemail' ) ) );
3591 $log = new LogPage( 'newusers' );
3592 $log->addEntry(
3593 $action,
3594 $this->getUserPage(),
3595 $reason,
3596 array( $this->getId() )
3598 return true;
3602 * Add an autocreate newuser log entry for this user
3603 * Used by things like CentralAuth and perhaps other authplugins.
3605 public function addNewUserLogEntryAutoCreate() {
3606 global $wgNewUserLog, $wgLogAutocreatedAccounts;
3607 if( !$wgNewUserLog || !$wgLogAutocreatedAccounts ) {
3608 return true; // disabled
3610 $log = new LogPage( 'newusers', false );
3611 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3612 return true;
3615 protected function loadOptions() {
3616 $this->load();
3617 if ( $this->mOptionsLoaded || !$this->getId() )
3618 return;
3620 $this->mOptions = self::getDefaultOptions();
3622 // Maybe load from the object
3623 if ( !is_null( $this->mOptionOverrides ) ) {
3624 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
3625 foreach( $this->mOptionOverrides as $key => $value ) {
3626 $this->mOptions[$key] = $value;
3628 } else {
3629 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
3630 // Load from database
3631 $dbr = wfGetDB( DB_SLAVE );
3633 $res = $dbr->select(
3634 'user_properties',
3635 '*',
3636 array( 'up_user' => $this->getId() ),
3637 __METHOD__
3640 foreach ( $res as $row ) {
3641 $this->mOptionOverrides[$row->up_property] = $row->up_value;
3642 $this->mOptions[$row->up_property] = $row->up_value;
3646 $this->mOptionsLoaded = true;
3648 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
3651 protected function saveOptions() {
3652 global $wgAllowPrefChange;
3654 $extuser = ExternalUser::newFromUser( $this );
3656 $this->loadOptions();
3657 $dbw = wfGetDB( DB_MASTER );
3659 $insert_rows = array();
3661 $saveOptions = $this->mOptions;
3663 // Allow hooks to abort, for instance to save to a global profile.
3664 // Reset options to default state before saving.
3665 if( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) )
3666 return;
3668 foreach( $saveOptions as $key => $value ) {
3669 # Don't bother storing default values
3670 if ( ( is_null( self::getDefaultOption( $key ) ) &&
3671 !( $value === false || is_null($value) ) ) ||
3672 $value != self::getDefaultOption( $key ) ) {
3673 $insert_rows[] = array(
3674 'up_user' => $this->getId(),
3675 'up_property' => $key,
3676 'up_value' => $value,
3679 if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
3680 switch ( $wgAllowPrefChange[$key] ) {
3681 case 'local':
3682 case 'message':
3683 break;
3684 case 'semiglobal':
3685 case 'global':
3686 $extuser->setPref( $key, $value );
3691 $dbw->begin();
3692 $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ );
3693 $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
3694 $dbw->commit();
3698 * Provide an array of HTML5 attributes to put on an input element
3699 * intended for the user to enter a new password. This may include
3700 * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
3702 * Do *not* use this when asking the user to enter his current password!
3703 * Regardless of configuration, users may have invalid passwords for whatever
3704 * reason (e.g., they were set before requirements were tightened up).
3705 * Only use it when asking for a new password, like on account creation or
3706 * ResetPass.
3708 * Obviously, you still need to do server-side checking.
3710 * NOTE: A combination of bugs in various browsers means that this function
3711 * actually just returns array() unconditionally at the moment. May as
3712 * well keep it around for when the browser bugs get fixed, though.
3714 * FIXME : This does not belong here; put it in Html or Linker or somewhere
3716 * @return array Array of HTML attributes suitable for feeding to
3717 * Html::element(), directly or indirectly. (Don't feed to Xml::*()!
3718 * That will potentially output invalid XHTML 1.0 Transitional, and will
3719 * get confused by the boolean attribute syntax used.)
3721 public static function passwordChangeInputAttribs() {
3722 global $wgMinimalPasswordLength;
3724 if ( $wgMinimalPasswordLength == 0 ) {
3725 return array();
3728 # Note that the pattern requirement will always be satisfied if the
3729 # input is empty, so we need required in all cases.
3731 # FIXME (bug 23769): This needs to not claim the password is required
3732 # if e-mail confirmation is being used. Since HTML5 input validation
3733 # is b0rked anyway in some browsers, just return nothing. When it's
3734 # re-enabled, fix this code to not output required for e-mail
3735 # registration.
3736 #$ret = array( 'required' );
3737 $ret = array();
3739 # We can't actually do this right now, because Opera 9.6 will print out
3740 # the entered password visibly in its error message! When other
3741 # browsers add support for this attribute, or Opera fixes its support,
3742 # we can add support with a version check to avoid doing this on Opera
3743 # versions where it will be a problem. Reported to Opera as
3744 # DSK-262266, but they don't have a public bug tracker for us to follow.
3746 if ( $wgMinimalPasswordLength > 1 ) {
3747 $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
3748 $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag',
3749 $wgMinimalPasswordLength );
3753 return $ret;
3757 * Format the user message using a hook, a template, or, failing these, a static format.
3758 * @param $subject String the subject of the message
3759 * @param $text String the content of the message
3760 * @param $signature String the signature, if provided.
3762 static protected function formatUserMessage( $subject, $text, $signature ) {
3763 if ( wfRunHooks( 'FormatUserMessage',
3764 array( $subject, &$text, $signature ) ) ) {
3766 $signature = empty($signature) ? "~~~~~" : "{$signature} ~~~~~";
3768 $template = Title::newFromText( wfMsgForContent( 'usermessage-template' ) );
3769 if ( !$template
3770 || $template->getNamespace() !== NS_TEMPLATE
3771 || !$template->exists() ) {
3772 $text = "\n== $subject ==\n\n$text\n\n-- $signature";
3773 } else {
3774 $text = '{{'. $template->getText()
3775 . " | subject=$subject | body=$text | signature=$signature }}";
3779 return $text;
3783 * Leave a user a message
3784 * @param $subject String the subject of the message
3785 * @param $text String the message to leave
3786 * @param $signature String Text to leave in the signature
3787 * @param $summary String the summary for this change, defaults to
3788 * "Leave system message."
3789 * @param $editor User The user leaving the message, defaults to
3790 * "{{MediaWiki:usermessage-editor}}"
3791 * @param $flags Int default edit flags
3793 * @return boolean true if it was successful
3795 public function leaveUserMessage( $subject, $text, $signature = "",
3796 $summary = null, $editor = null, $flags = 0 ) {
3797 if ( !isset( $summary ) ) {
3798 $summary = wfMsgForContent( 'usermessage-summary' );
3801 if ( !isset( $editor ) ) {
3802 $editor = User::newFromName( wfMsgForContent( 'usermessage-editor' ) );
3803 if ( !$editor->isLoggedIn() ) {
3804 $editor->addToDatabase();
3808 $article = new Article( $this->getTalkPage() );
3809 wfRunHooks( 'SetupUserMessageArticle',
3810 array( $this, &$article, $subject, $text, $signature, $summary, $editor ) );
3813 $text = self::formatUserMessage( $subject, $text, $signature );
3814 $flags = $article->checkFlags( $flags );
3816 if ( $flags & EDIT_UPDATE ) {
3817 $text = $article->getContent() . $text;
3820 $dbw = wfGetDB( DB_MASTER );
3821 $dbw->begin();
3823 try {
3824 $status = $article->doEdit( $text, $summary, $flags, false, $editor );
3825 } catch ( DBQueryError $e ) {
3826 $status = Status::newFatal("DB Error");
3829 if ( $status->isGood() ) {
3830 // Set newtalk with the right user ID
3831 $this->setNewtalk( true );
3832 wfRunHooks( 'AfterUserMessage',
3833 array( $this, $article, $summary, $text, $signature, $summary, $editor ) );
3834 $dbw->commit();
3835 } else {
3836 // The article was concurrently created
3837 wfDebug( __METHOD__ . ": Error ".$status->getWikiText() );
3838 $dbw->rollback();
3841 return $status->isGood();