Correction to r31475 -- works with PHP 5.1 now.
[mediawiki.git] / includes / User.php
blob08940a5ca20fe2ba91d299185406f37be590723d
1 <?php
2 /**
3 * See user.txt
5 */
7 # Number of characters in user_token field
8 define( 'USER_TOKEN_LENGTH', 32 );
10 # Serialized record version
11 define( 'MW_USER_VERSION', 5 );
13 # Some punctuation to prevent editing from broken text-mangling proxies.
14 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
16 /**
17 * Thrown by User::setPassword() on error
18 * @addtogroup Exception
20 class PasswordError extends MWException {
21 // NOP
24 /**
25 * The User object encapsulates all of the user-specific settings (user_id,
26 * name, rights, password, email address, options, last login time). Client
27 * classes use the getXXX() functions to access these fields. These functions
28 * do all the work of determining whether the user is logged in,
29 * whether the requested option can be satisfied from cookies or
30 * whether a database query is needed. Most of the settings needed
31 * for rendering normal pages are set in the cookie to minimize use
32 * of the database.
34 class User {
36 /**
37 * A list of default user toggles, i.e. boolean user preferences that are
38 * displayed by Special:Preferences as checkboxes. This list can be
39 * extended via the UserToggles hook or $wgContLang->getExtraUserToggles().
41 static public $mToggles = array(
42 'highlightbroken',
43 'justify',
44 'hideminor',
45 'extendwatchlist',
46 'usenewrc',
47 'numberheadings',
48 'showtoolbar',
49 'editondblclick',
50 'editsection',
51 'editsectiononrightclick',
52 'showtoc',
53 'rememberpassword',
54 'editwidth',
55 'watchcreations',
56 'watchdefault',
57 'watchmoves',
58 'watchdeletion',
59 'minordefault',
60 'previewontop',
61 'previewonfirst',
62 'nocache',
63 'enotifwatchlistpages',
64 'enotifusertalkpages',
65 'enotifminoredits',
66 'enotifrevealaddr',
67 'shownumberswatching',
68 'fancysig',
69 'externaleditor',
70 'externaldiff',
71 'showjumplinks',
72 'uselivepreview',
73 'forceeditsummary',
74 'watchlisthideown',
75 'watchlisthidebots',
76 'watchlisthideminor',
77 'ccmeonemails',
78 'diffonly',
79 'showhiddencats',
82 /**
83 * List of member variables which are saved to the shared cache (memcached).
84 * Any operation which changes the corresponding database fields must
85 * call a cache-clearing function.
87 static $mCacheVars = array(
88 # user table
89 'mId',
90 'mName',
91 'mRealName',
92 'mPassword',
93 'mNewpassword',
94 'mNewpassTime',
95 'mEmail',
96 'mOptions',
97 'mTouched',
98 'mToken',
99 'mEmailAuthenticated',
100 'mEmailToken',
101 'mEmailTokenExpires',
102 'mRegistration',
103 'mEditCount',
104 # user_group table
105 'mGroups',
109 * The cache variable declarations
111 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
112 $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
113 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
116 * Whether the cache variables have been loaded
118 var $mDataLoaded;
121 * Initialisation data source if mDataLoaded==false. May be one of:
122 * defaults anonymous user initialised from class defaults
123 * name initialise from mName
124 * id initialise from mId
125 * session log in from cookies or session if possible
127 * Use the User::newFrom*() family of functions to set this.
129 var $mFrom;
132 * Lazy-initialised variables, invalidated with clearInstanceCache
134 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
135 $mBlockreason, $mBlock, $mEffectiveGroups;
137 /**
138 * Lightweight constructor for anonymous user
139 * Use the User::newFrom* factory functions for other kinds of users
141 function User() {
142 $this->clearInstanceCache( 'defaults' );
146 * Load the user table data for this object from the source given by mFrom
148 function load() {
149 if ( $this->mDataLoaded ) {
150 return;
152 wfProfileIn( __METHOD__ );
154 # Set it now to avoid infinite recursion in accessors
155 $this->mDataLoaded = true;
157 switch ( $this->mFrom ) {
158 case 'defaults':
159 $this->loadDefaults();
160 break;
161 case 'name':
162 $this->mId = self::idFromName( $this->mName );
163 if ( !$this->mId ) {
164 # Nonexistent user placeholder object
165 $this->loadDefaults( $this->mName );
166 } else {
167 $this->loadFromId();
169 break;
170 case 'id':
171 $this->loadFromId();
172 break;
173 case 'session':
174 $this->loadFromSession();
175 break;
176 default:
177 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
179 wfProfileOut( __METHOD__ );
183 * Load user table data given mId
184 * @return false if the ID does not exist, true otherwise
185 * @private
187 function loadFromId() {
188 global $wgMemc;
189 if ( $this->mId == 0 ) {
190 $this->loadDefaults();
191 return false;
194 # Try cache
195 $key = wfMemcKey( 'user', 'id', $this->mId );
196 $data = $wgMemc->get( $key );
197 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
198 # Object is expired, load from DB
199 $data = false;
202 if ( !$data ) {
203 wfDebug( "Cache miss for user {$this->mId}\n" );
204 # Load from DB
205 if ( !$this->loadFromDatabase() ) {
206 # Can't load from ID, user is anonymous
207 return false;
210 $this->saveToCache();
211 } else {
212 wfDebug( "Got user {$this->mId} from cache\n" );
213 # Restore from cache
214 foreach ( self::$mCacheVars as $name ) {
215 $this->$name = $data[$name];
218 return true;
222 * Save user data to the shared cache
224 function saveToCache() {
225 $this->load();
226 if ( $this->isAnon() ) {
227 // Anonymous users are uncached
228 return;
230 $data = array();
231 foreach ( self::$mCacheVars as $name ) {
232 $data[$name] = $this->$name;
234 $data['mVersion'] = MW_USER_VERSION;
235 $key = wfMemcKey( 'user', 'id', $this->mId );
236 global $wgMemc;
237 $wgMemc->set( $key, $data );
241 * Static factory method for creation from username.
243 * This is slightly less efficient than newFromId(), so use newFromId() if
244 * you have both an ID and a name handy.
246 * @param string $name Username, validated by Title:newFromText()
247 * @param mixed $validate Validate username. Takes the same parameters as
248 * User::getCanonicalName(), except that true is accepted as an alias
249 * for 'valid', for BC.
251 * @return User object, or null if the username is invalid. If the username
252 * is not present in the database, the result will be a user object with
253 * a name, zero user ID and default settings.
254 * @static
256 static function newFromName( $name, $validate = 'valid' ) {
257 if ( $validate === true ) {
258 $validate = 'valid';
260 $name = self::getCanonicalName( $name, $validate );
261 if ( $name === false ) {
262 return null;
263 } else {
264 # Create unloaded user object
265 $u = new User;
266 $u->mName = $name;
267 $u->mFrom = 'name';
268 return $u;
272 static function newFromId( $id ) {
273 $u = new User;
274 $u->mId = $id;
275 $u->mFrom = 'id';
276 return $u;
280 * Factory method to fetch whichever user has a given email confirmation code.
281 * This code is generated when an account is created or its e-mail address
282 * has changed.
284 * If the code is invalid or has expired, returns NULL.
286 * @param string $code
287 * @return User
288 * @static
290 static function newFromConfirmationCode( $code ) {
291 $dbr = wfGetDB( DB_SLAVE );
292 $id = $dbr->selectField( 'user', 'user_id', array(
293 'user_email_token' => md5( $code ),
294 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
295 ) );
296 if( $id !== false ) {
297 return User::newFromId( $id );
298 } else {
299 return null;
304 * Create a new user object using data from session or cookies. If the
305 * login credentials are invalid, the result is an anonymous user.
307 * @return User
308 * @static
310 static function newFromSession() {
311 $user = new User;
312 $user->mFrom = 'session';
313 return $user;
317 * Get username given an id.
318 * @param integer $id Database user id
319 * @return string Nickname of a user
320 * @static
322 static function whoIs( $id ) {
323 $dbr = wfGetDB( DB_SLAVE );
324 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
328 * Get the real name of a user given their identifier
330 * @param int $id Database user id
331 * @return string Real name of a user
333 static function whoIsReal( $id ) {
334 $dbr = wfGetDB( DB_SLAVE );
335 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
339 * Get database id given a user name
340 * @param string $name Nickname of a user
341 * @return integer|null Database user id (null: if non existent
342 * @static
344 static function idFromName( $name ) {
345 $nt = Title::newFromText( $name );
346 if( is_null( $nt ) ) {
347 # Illegal name
348 return null;
350 $dbr = wfGetDB( DB_SLAVE );
351 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
353 if ( $s === false ) {
354 return 0;
355 } else {
356 return $s->user_id;
361 * Does the string match an anonymous IPv4 address?
363 * This function exists for username validation, in order to reject
364 * usernames which are similar in form to IP addresses. Strings such
365 * as 300.300.300.300 will return true because it looks like an IP
366 * address, despite not being strictly valid.
368 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
369 * address because the usemod software would "cloak" anonymous IP
370 * addresses like this, if we allowed accounts like this to be created
371 * new users could get the old edits of these anonymous users.
373 * @static
374 * @param string $name Nickname of a user
375 * @return bool
377 static function isIP( $name ) {
378 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || User::isIPv6($name);
379 /*return preg_match("/^
380 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
381 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
382 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
383 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
384 $/x", $name);*/
388 * Check if $name is an IPv6 IP.
390 static function isIPv6($name) {
392 * if it has any non-valid characters, it can't be a valid IPv6
393 * address.
395 if (preg_match("/[^:a-fA-F0-9]/", $name))
396 return false;
398 $parts = explode(":", $name);
399 if (count($parts) < 3)
400 return false;
401 foreach ($parts as $part) {
402 if (!preg_match("/^[0-9a-fA-F]{0,4}$/", $part))
403 return false;
405 return true;
409 * Is the input a valid username?
411 * Checks if the input is a valid username, we don't want an empty string,
412 * an IP address, anything that containins slashes (would mess up subpages),
413 * is longer than the maximum allowed username size or doesn't begin with
414 * a capital letter.
416 * @param string $name
417 * @return bool
418 * @static
420 static function isValidUserName( $name ) {
421 global $wgContLang, $wgMaxNameChars;
423 if ( $name == ''
424 || User::isIP( $name )
425 || strpos( $name, '/' ) !== false
426 || strlen( $name ) > $wgMaxNameChars
427 || $name != $wgContLang->ucfirst( $name ) )
428 return false;
430 // Ensure that the name can't be misresolved as a different title,
431 // such as with extra namespace keys at the start.
432 $parsed = Title::newFromText( $name );
433 if( is_null( $parsed )
434 || $parsed->getNamespace()
435 || strcmp( $name, $parsed->getPrefixedText() ) )
436 return false;
438 // Check an additional blacklist of troublemaker characters.
439 // Should these be merged into the title char list?
440 $unicodeBlacklist = '/[' .
441 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
442 '\x{00a0}' . # non-breaking space
443 '\x{2000}-\x{200f}' . # various whitespace
444 '\x{2028}-\x{202f}' . # breaks and control chars
445 '\x{3000}' . # ideographic space
446 '\x{e000}-\x{f8ff}' . # private use
447 ']/u';
448 if( preg_match( $unicodeBlacklist, $name ) ) {
449 return false;
452 return true;
456 * Usernames which fail to pass this function will be blocked
457 * from user login and new account registrations, but may be used
458 * internally by batch processes.
460 * If an account already exists in this form, login will be blocked
461 * by a failure to pass this function.
463 * @param string $name
464 * @return bool
466 static function isUsableName( $name ) {
467 global $wgReservedUsernames;
468 return
469 // Must be a valid username, obviously ;)
470 self::isValidUserName( $name ) &&
472 // Certain names may be reserved for batch processes.
473 !in_array( $name, $wgReservedUsernames );
477 * Usernames which fail to pass this function will be blocked
478 * from new account registrations, but may be used internally
479 * either by batch processes or by user accounts which have
480 * already been created.
482 * Additional character blacklisting may be added here
483 * rather than in isValidUserName() to avoid disrupting
484 * existing accounts.
486 * @param string $name
487 * @return bool
489 static function isCreatableName( $name ) {
490 return
491 self::isUsableName( $name ) &&
493 // Registration-time character blacklisting...
494 strpos( $name, '@' ) === false;
498 * Is the input a valid password for this user?
500 * @param string $password Desired password
501 * @return bool
503 function isValidPassword( $password ) {
504 global $wgMinimalPasswordLength, $wgContLang;
506 $result = null;
507 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
508 return $result;
509 if( $result === false )
510 return false;
512 // Password needs to be long enough, and can't be the same as the username
513 return strlen( $password ) >= $wgMinimalPasswordLength
514 && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
518 * Does a string look like an email address?
520 * There used to be a regular expression here, it got removed because it
521 * rejected valid addresses. Actually just check if there is '@' somewhere
522 * in the given address.
524 * @todo Check for RFC 2822 compilance (bug 959)
526 * @param string $addr email address
527 * @return bool
529 public static function isValidEmailAddr( $addr ) {
530 $result = null;
531 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
532 return $result;
535 return strpos( $addr, '@' ) !== false;
539 * Given unvalidated user input, return a canonical username, or false if
540 * the username is invalid.
541 * @param string $name
542 * @param mixed $validate Type of validation to use:
543 * false No validation
544 * 'valid' Valid for batch processes
545 * 'usable' Valid for batch processes and login
546 * 'creatable' Valid for batch processes, login and account creation
548 static function getCanonicalName( $name, $validate = 'valid' ) {
549 # Force usernames to capital
550 global $wgContLang;
551 $name = $wgContLang->ucfirst( $name );
553 # Reject names containing '#'; these will be cleaned up
554 # with title normalisation, but then it's too late to
555 # check elsewhere
556 if( strpos( $name, '#' ) !== false )
557 return false;
559 # Clean up name according to title rules
560 $t = Title::newFromText( $name );
561 if( is_null( $t ) ) {
562 return false;
565 # Reject various classes of invalid names
566 $name = $t->getText();
567 global $wgAuth;
568 $name = $wgAuth->getCanonicalName( $t->getText() );
570 switch ( $validate ) {
571 case false:
572 break;
573 case 'valid':
574 if ( !User::isValidUserName( $name ) ) {
575 $name = false;
577 break;
578 case 'usable':
579 if ( !User::isUsableName( $name ) ) {
580 $name = false;
582 break;
583 case 'creatable':
584 if ( !User::isCreatableName( $name ) ) {
585 $name = false;
587 break;
588 default:
589 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
591 return $name;
595 * Count the number of edits of a user
597 * It should not be static and some day should be merged as proper member function / deprecated -- domas
599 * @param int $uid The user ID to check
600 * @return int
601 * @static
603 static function edits( $uid ) {
604 wfProfileIn( __METHOD__ );
605 $dbr = wfGetDB( DB_SLAVE );
606 // check if the user_editcount field has been initialized
607 $field = $dbr->selectField(
608 'user', 'user_editcount',
609 array( 'user_id' => $uid ),
610 __METHOD__
613 if( $field === null ) { // it has not been initialized. do so.
614 $dbw = wfGetDB( DB_MASTER );
615 $count = $dbr->selectField(
616 'revision', 'count(*)',
617 array( 'rev_user' => $uid ),
618 __METHOD__
620 $dbw->update(
621 'user',
622 array( 'user_editcount' => $count ),
623 array( 'user_id' => $uid ),
624 __METHOD__
626 } else {
627 $count = $field;
629 wfProfileOut( __METHOD__ );
630 return $count;
634 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
635 * @todo hash random numbers to improve security, like generateToken()
637 * @return string
638 * @static
640 static function randomPassword() {
641 global $wgMinimalPasswordLength;
642 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
643 $l = strlen( $pwchars ) - 1;
645 $pwlength = max( 7, $wgMinimalPasswordLength );
646 $digit = mt_rand(0, $pwlength - 1);
647 $np = '';
648 for ( $i = 0; $i < $pwlength; $i++ ) {
649 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
651 return $np;
655 * Set cached properties to default. Note: this no longer clears
656 * uncached lazy-initialised properties. The constructor does that instead.
658 * @private
660 function loadDefaults( $name = false ) {
661 wfProfileIn( __METHOD__ );
663 global $wgCookiePrefix;
665 $this->mId = 0;
666 $this->mName = $name;
667 $this->mRealName = '';
668 $this->mPassword = $this->mNewpassword = '';
669 $this->mNewpassTime = null;
670 $this->mEmail = '';
671 $this->mOptions = null; # Defer init
673 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
674 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
675 } else {
676 $this->mTouched = '0'; # Allow any pages to be cached
679 $this->setToken(); # Random
680 $this->mEmailAuthenticated = null;
681 $this->mEmailToken = '';
682 $this->mEmailTokenExpires = null;
683 $this->mRegistration = wfTimestamp( TS_MW );
684 $this->mGroups = array();
686 wfProfileOut( __METHOD__ );
690 * Initialise php session
691 * @deprecated use wfSetupSession()
693 function SetupSession() {
694 wfSetupSession();
698 * Load user data from the session or login cookie. If there are no valid
699 * credentials, initialises the user as an anon.
700 * @return true if the user is logged in, false otherwise
702 private function loadFromSession() {
703 global $wgMemc, $wgCookiePrefix;
705 if ( isset( $_SESSION['wsUserID'] ) ) {
706 if ( 0 != $_SESSION['wsUserID'] ) {
707 $sId = $_SESSION['wsUserID'];
708 } else {
709 $this->loadDefaults();
710 return false;
712 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
713 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
714 $_SESSION['wsUserID'] = $sId;
715 } else {
716 $this->loadDefaults();
717 return false;
719 if ( isset( $_SESSION['wsUserName'] ) ) {
720 $sName = $_SESSION['wsUserName'];
721 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
722 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
723 $_SESSION['wsUserName'] = $sName;
724 } else {
725 $this->loadDefaults();
726 return false;
729 $passwordCorrect = FALSE;
730 $this->mId = $sId;
731 if ( !$this->loadFromId() ) {
732 # Not a valid ID, loadFromId has switched the object to anon for us
733 return false;
736 if ( isset( $_SESSION['wsToken'] ) ) {
737 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
738 $from = 'session';
739 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
740 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
741 $from = 'cookie';
742 } else {
743 # No session or persistent login cookie
744 $this->loadDefaults();
745 return false;
748 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
749 $_SESSION['wsToken'] = $this->mToken;
750 wfDebug( "Logged in from $from\n" );
751 return true;
752 } else {
753 # Invalid credentials
754 wfDebug( "Can't log in from $from, invalid credentials\n" );
755 $this->loadDefaults();
756 return false;
761 * Load user and user_group data from the database
762 * $this->mId must be set, this is how the user is identified.
764 * @return true if the user exists, false if the user is anonymous
765 * @private
767 function loadFromDatabase() {
768 # Paranoia
769 $this->mId = intval( $this->mId );
771 /** Anonymous user */
772 if( !$this->mId ) {
773 $this->loadDefaults();
774 return false;
777 $dbr = wfGetDB( DB_MASTER );
778 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
780 if ( $s !== false ) {
781 # Initialise user table data
782 $this->mName = $s->user_name;
783 $this->mRealName = $s->user_real_name;
784 $this->mPassword = $s->user_password;
785 $this->mNewpassword = $s->user_newpassword;
786 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $s->user_newpass_time );
787 $this->mEmail = $s->user_email;
788 $this->decodeOptions( $s->user_options );
789 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
790 $this->mToken = $s->user_token;
791 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
792 $this->mEmailToken = $s->user_email_token;
793 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires );
794 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
795 $this->mEditCount = $s->user_editcount;
796 $this->getEditCount(); // revalidation for nulls
798 # Load group data
799 $res = $dbr->select( 'user_groups',
800 array( 'ug_group' ),
801 array( 'ug_user' => $this->mId ),
802 __METHOD__ );
803 $this->mGroups = array();
804 while( $row = $dbr->fetchObject( $res ) ) {
805 $this->mGroups[] = $row->ug_group;
807 return true;
808 } else {
809 # Invalid user_id
810 $this->mId = 0;
811 $this->loadDefaults();
812 return false;
817 * Clear various cached data stored in this object.
818 * @param string $reloadFrom Reload user and user_groups table data from a
819 * given source. May be "name", "id", "defaults", "session" or false for
820 * no reload.
822 function clearInstanceCache( $reloadFrom = false ) {
823 $this->mNewtalk = -1;
824 $this->mDatePreference = null;
825 $this->mBlockedby = -1; # Unset
826 $this->mHash = false;
827 $this->mSkin = null;
828 $this->mRights = null;
829 $this->mEffectiveGroups = null;
831 if ( $reloadFrom ) {
832 $this->mDataLoaded = false;
833 $this->mFrom = $reloadFrom;
838 * Combine the language default options with any site-specific options
839 * and add the default language variants.
840 * Not really private cause it's called by Language class
841 * @return array
842 * @static
843 * @private
845 static function getDefaultOptions() {
846 global $wgNamespacesToBeSearchedDefault;
848 * Site defaults will override the global/language defaults
850 global $wgDefaultUserOptions, $wgContLang;
851 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
854 * default language setting
856 $variant = $wgContLang->getPreferredVariant( false );
857 $defOpt['variant'] = $variant;
858 $defOpt['language'] = $variant;
860 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
861 $defOpt['searchNs'.$nsnum] = $val;
863 return $defOpt;
867 * Get a given default option value.
869 * @param string $opt
870 * @return string
871 * @static
872 * @public
874 function getDefaultOption( $opt ) {
875 $defOpts = User::getDefaultOptions();
876 if( isset( $defOpts[$opt] ) ) {
877 return $defOpts[$opt];
878 } else {
879 return '';
884 * Get a list of user toggle names
885 * @return array
887 static function getToggles() {
888 global $wgContLang;
889 $extraToggles = array();
890 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
891 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
896 * Get blocking information
897 * @private
898 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
899 * non-critical checks are done against slaves. Check when actually saving should be done against
900 * master.
902 function getBlockedStatus( $bFromSlave = true ) {
903 global $wgEnableSorbs, $wgProxyWhitelist;
905 if ( -1 != $this->mBlockedby ) {
906 wfDebug( "User::getBlockedStatus: already loaded.\n" );
907 return;
910 wfProfileIn( __METHOD__ );
911 wfDebug( __METHOD__.": checking...\n" );
913 $this->mBlockedby = 0;
914 $this->mHideName = 0;
915 $ip = wfGetIP();
917 if ($this->isAllowed( 'ipblock-exempt' ) ) {
918 # Exempt from all types of IP-block
919 $ip = '';
922 # User/IP blocking
923 $this->mBlock = new Block();
924 $this->mBlock->fromMaster( !$bFromSlave );
925 if ( $this->mBlock->load( $ip , $this->mId ) ) {
926 wfDebug( __METHOD__.": Found block.\n" );
927 $this->mBlockedby = $this->mBlock->mBy;
928 $this->mBlockreason = $this->mBlock->mReason;
929 $this->mHideName = $this->mBlock->mHideName;
930 if ( $this->isLoggedIn() ) {
931 $this->spreadBlock();
933 } else {
934 $this->mBlock = null;
935 wfDebug( __METHOD__.": No block.\n" );
938 # Proxy blocking
939 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
941 # Local list
942 if ( wfIsLocallyBlockedProxy( $ip ) ) {
943 $this->mBlockedby = wfMsg( 'proxyblocker' );
944 $this->mBlockreason = wfMsg( 'proxyblockreason' );
947 # DNSBL
948 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
949 if ( $this->inSorbsBlacklist( $ip ) ) {
950 $this->mBlockedby = wfMsg( 'sorbs' );
951 $this->mBlockreason = wfMsg( 'sorbsreason' );
956 # Extensions
957 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
959 wfProfileOut( __METHOD__ );
962 function inSorbsBlacklist( $ip ) {
963 global $wgEnableSorbs, $wgSorbsUrl;
965 return $wgEnableSorbs &&
966 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
969 function inDnsBlacklist( $ip, $base ) {
970 wfProfileIn( __METHOD__ );
972 $found = false;
973 $host = '';
975 $m = array();
976 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
977 # Make hostname
978 for ( $i=4; $i>=1; $i-- ) {
979 $host .= $m[$i] . '.';
981 $host .= $base;
983 # Send query
984 $ipList = gethostbynamel( $host );
986 if ( $ipList ) {
987 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
988 $found = true;
989 } else {
990 wfDebug( "Requested $host, not found in $base.\n" );
994 wfProfileOut( __METHOD__ );
995 return $found;
999 * Is this user subject to rate limiting?
1001 * @return bool
1003 public function isPingLimitable() {
1004 global $wgRateLimitsExcludedGroups;
1005 return array_intersect($this->getEffectiveGroups(), $wgRateLimitsExcludedGroups) == array();
1009 * Primitive rate limits: enforce maximum actions per time period
1010 * to put a brake on flooding.
1012 * Note: when using a shared cache like memcached, IP-address
1013 * last-hit counters will be shared across wikis.
1015 * @return bool true if a rate limiter was tripped
1016 * @public
1018 function pingLimiter( $action='edit' ) {
1020 # Call the 'PingLimiter' hook
1021 $result = false;
1022 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1023 return $result;
1026 global $wgRateLimits;
1027 if( !isset( $wgRateLimits[$action] ) ) {
1028 return false;
1031 # Some groups shouldn't trigger the ping limiter, ever
1032 if( !$this->isPingLimitable() )
1033 return false;
1035 global $wgMemc, $wgRateLimitLog;
1036 wfProfileIn( __METHOD__ );
1038 $limits = $wgRateLimits[$action];
1039 $keys = array();
1040 $id = $this->getId();
1041 $ip = wfGetIP();
1043 if( isset( $limits['anon'] ) && $id == 0 ) {
1044 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1047 if( isset( $limits['user'] ) && $id != 0 ) {
1048 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['user'];
1050 if( $this->isNewbie() ) {
1051 if( isset( $limits['newbie'] ) && $id != 0 ) {
1052 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1054 if( isset( $limits['ip'] ) ) {
1055 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1057 $matches = array();
1058 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1059 $subnet = $matches[1];
1060 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1064 $triggered = false;
1065 foreach( $keys as $key => $limit ) {
1066 list( $max, $period ) = $limit;
1067 $summary = "(limit $max in {$period}s)";
1068 $count = $wgMemc->get( $key );
1069 if( $count ) {
1070 if( $count > $max ) {
1071 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1072 if( $wgRateLimitLog ) {
1073 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1075 $triggered = true;
1076 } else {
1077 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1079 } else {
1080 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1081 $wgMemc->add( $key, 1, intval( $period ) );
1083 $wgMemc->incr( $key );
1086 wfProfileOut( __METHOD__ );
1087 return $triggered;
1091 * Check if user is blocked
1092 * @return bool True if blocked, false otherwise
1094 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1095 wfDebug( "User::isBlocked: enter\n" );
1096 $this->getBlockedStatus( $bFromSlave );
1097 return $this->mBlockedby !== 0;
1101 * Check if user is blocked from editing a particular article
1103 function isBlockedFrom( $title, $bFromSlave = false ) {
1104 global $wgBlockAllowsUTEdit;
1105 wfProfileIn( __METHOD__ );
1106 wfDebug( __METHOD__.": enter\n" );
1108 wfDebug( __METHOD__.": asking isBlocked()\n" );
1109 $blocked = $this->isBlocked( $bFromSlave );
1110 # If a user's name is suppressed, they cannot make edits anywhere
1111 if ( !$this->mHideName && $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
1112 $title->getNamespace() == NS_USER_TALK ) {
1113 $blocked = false;
1114 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1116 wfProfileOut( __METHOD__ );
1117 return $blocked;
1121 * Get name of blocker
1122 * @return string name of blocker
1124 function blockedBy() {
1125 $this->getBlockedStatus();
1126 return $this->mBlockedby;
1130 * Get blocking reason
1131 * @return string Blocking reason
1133 function blockedFor() {
1134 $this->getBlockedStatus();
1135 return $this->mBlockreason;
1139 * Get the user ID. Returns 0 if the user is anonymous or nonexistent.
1141 function getID() {
1142 if( $this->mId === null and $this->mName !== null
1143 and User::isIP( $this->mName ) ) {
1144 // Special case, we know the user is anonymous
1145 return 0;
1146 } elseif( $this->mId === null ) {
1147 // Don't load if this was initialized from an ID
1148 $this->load();
1150 return $this->mId;
1154 * Set the user and reload all fields according to that ID
1155 * @deprecated use User::newFromId()
1157 function setID( $v ) {
1158 $this->mId = $v;
1159 $this->clearInstanceCache( 'id' );
1163 * Get the user name, or the IP for anons
1165 function getName() {
1166 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1167 # Special case optimisation
1168 return $this->mName;
1169 } else {
1170 $this->load();
1171 if ( $this->mName === false ) {
1172 # Clean up IPs
1173 $this->mName = IP::sanitizeIP( wfGetIP() );
1175 return $this->mName;
1180 * Set the user name.
1182 * This does not reload fields from the database according to the given
1183 * name. Rather, it is used to create a temporary "nonexistent user" for
1184 * later addition to the database. It can also be used to set the IP
1185 * address for an anonymous user to something other than the current
1186 * remote IP.
1188 * User::newFromName() has rougly the same function, when the named user
1189 * does not exist.
1191 function setName( $str ) {
1192 $this->load();
1193 $this->mName = $str;
1197 * Return the title dbkey form of the name, for eg user pages.
1198 * @return string
1199 * @public
1201 function getTitleKey() {
1202 return str_replace( ' ', '_', $this->getName() );
1205 function getNewtalk() {
1206 $this->load();
1208 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1209 if( $this->mNewtalk === -1 ) {
1210 $this->mNewtalk = false; # reset talk page status
1212 # Check memcached separately for anons, who have no
1213 # entire User object stored in there.
1214 if( !$this->mId ) {
1215 global $wgMemc;
1216 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1217 $newtalk = $wgMemc->get( $key );
1218 if( strval( $newtalk ) !== '' ) {
1219 $this->mNewtalk = (bool)$newtalk;
1220 } else {
1221 // Since we are caching this, make sure it is up to date by getting it
1222 // from the master
1223 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1224 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1226 } else {
1227 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1231 return (bool)$this->mNewtalk;
1235 * Return the talk page(s) this user has new messages on.
1237 function getNewMessageLinks() {
1238 $talks = array();
1239 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1240 return $talks;
1242 if (!$this->getNewtalk())
1243 return array();
1244 $up = $this->getUserPage();
1245 $utp = $up->getTalkPage();
1246 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1251 * Perform a user_newtalk check, uncached.
1252 * Use getNewtalk for a cached check.
1254 * @param string $field
1255 * @param mixed $id
1256 * @param bool $fromMaster True to fetch from the master, false for a slave
1257 * @return bool
1258 * @private
1260 function checkNewtalk( $field, $id, $fromMaster = false ) {
1261 if ( $fromMaster ) {
1262 $db = wfGetDB( DB_MASTER );
1263 } else {
1264 $db = wfGetDB( DB_SLAVE );
1266 $ok = $db->selectField( 'user_newtalk', $field,
1267 array( $field => $id ), __METHOD__ );
1268 return $ok !== false;
1272 * Add or update the
1273 * @param string $field
1274 * @param mixed $id
1275 * @private
1277 function updateNewtalk( $field, $id ) {
1278 $dbw = wfGetDB( DB_MASTER );
1279 $dbw->insert( 'user_newtalk',
1280 array( $field => $id ),
1281 __METHOD__,
1282 'IGNORE' );
1283 if ( $dbw->affectedRows() ) {
1284 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1285 return true;
1286 } else {
1287 wfDebug( __METHOD__." already set ($field, $id)\n" );
1288 return false;
1293 * Clear the new messages flag for the given user
1294 * @param string $field
1295 * @param mixed $id
1296 * @private
1298 function deleteNewtalk( $field, $id ) {
1299 $dbw = wfGetDB( DB_MASTER );
1300 $dbw->delete( 'user_newtalk',
1301 array( $field => $id ),
1302 __METHOD__ );
1303 if ( $dbw->affectedRows() ) {
1304 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1305 return true;
1306 } else {
1307 wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1308 return false;
1313 * Update the 'You have new messages!' status.
1314 * @param bool $val
1316 function setNewtalk( $val ) {
1317 if( wfReadOnly() ) {
1318 return;
1321 $this->load();
1322 $this->mNewtalk = $val;
1324 if( $this->isAnon() ) {
1325 $field = 'user_ip';
1326 $id = $this->getName();
1327 } else {
1328 $field = 'user_id';
1329 $id = $this->getId();
1331 global $wgMemc;
1333 if( $val ) {
1334 $changed = $this->updateNewtalk( $field, $id );
1335 } else {
1336 $changed = $this->deleteNewtalk( $field, $id );
1339 if( $this->isAnon() ) {
1340 // Anons have a separate memcached space, since
1341 // user records aren't kept for them.
1342 $key = wfMemcKey( 'newtalk', 'ip', $id );
1343 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1345 if ( $changed ) {
1346 $this->invalidateCache();
1351 * Generate a current or new-future timestamp to be stored in the
1352 * user_touched field when we update things.
1354 private static function newTouchedTimestamp() {
1355 global $wgClockSkewFudge;
1356 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1360 * Clear user data from memcached.
1361 * Use after applying fun updates to the database; caller's
1362 * responsibility to update user_touched if appropriate.
1364 * Called implicitly from invalidateCache() and saveSettings().
1366 private function clearSharedCache() {
1367 if( $this->mId ) {
1368 global $wgMemc;
1369 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1374 * Immediately touch the user data cache for this account.
1375 * Updates user_touched field, and removes account data from memcached
1376 * for reload on the next hit.
1378 function invalidateCache() {
1379 $this->load();
1380 if( $this->mId ) {
1381 $this->mTouched = self::newTouchedTimestamp();
1383 $dbw = wfGetDB( DB_MASTER );
1384 $dbw->update( 'user',
1385 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1386 array( 'user_id' => $this->mId ),
1387 __METHOD__ );
1389 $this->clearSharedCache();
1393 function validateCache( $timestamp ) {
1394 $this->load();
1395 return ($timestamp >= $this->mTouched);
1399 * Encrypt a password.
1400 * It can eventually salt a password.
1401 * @see User::addSalt()
1402 * @param string $p clear Password.
1403 * @return string Encrypted password.
1405 function encryptPassword( $p ) {
1406 $this->load();
1407 return wfEncryptPassword( $this->mId, $p );
1411 * Set the password and reset the random token
1412 * Calls through to authentication plugin if necessary;
1413 * will have no effect if the auth plugin refuses to
1414 * pass the change through or if the legal password
1415 * checks fail.
1417 * As a special case, setting the password to null
1418 * wipes it, so the account cannot be logged in until
1419 * a new password is set, for instance via e-mail.
1421 * @param string $str
1422 * @throws PasswordError on failure
1424 function setPassword( $str ) {
1425 global $wgAuth;
1427 if( $str !== null ) {
1428 if( !$wgAuth->allowPasswordChange() ) {
1429 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1432 if( !$this->isValidPassword( $str ) ) {
1433 global $wgMinimalPasswordLength;
1434 throw new PasswordError( wfMsg( 'passwordtooshort',
1435 $wgMinimalPasswordLength ) );
1439 if( !$wgAuth->setPassword( $this, $str ) ) {
1440 throw new PasswordError( wfMsg( 'externaldberror' ) );
1443 $this->setInternalPassword( $str );
1445 return true;
1449 * Set the password and reset the random token no matter
1450 * what.
1452 * @param string $str
1454 function setInternalPassword( $str ) {
1455 $this->load();
1456 $this->setToken();
1458 if( $str === null ) {
1459 // Save an invalid hash...
1460 $this->mPassword = '';
1461 } else {
1462 $this->mPassword = $this->encryptPassword( $str );
1464 $this->mNewpassword = '';
1465 $this->mNewpassTime = null;
1468 * Set the random token (used for persistent authentication)
1469 * Called from loadDefaults() among other places.
1470 * @private
1472 function setToken( $token = false ) {
1473 global $wgSecretKey, $wgProxyKey;
1474 $this->load();
1475 if ( !$token ) {
1476 if ( $wgSecretKey ) {
1477 $key = $wgSecretKey;
1478 } elseif ( $wgProxyKey ) {
1479 $key = $wgProxyKey;
1480 } else {
1481 $key = microtime();
1483 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1484 } else {
1485 $this->mToken = $token;
1489 function setCookiePassword( $str ) {
1490 $this->load();
1491 $this->mCookiePassword = md5( $str );
1495 * Set the password for a password reminder or new account email
1496 * Sets the user_newpass_time field if $throttle is true
1498 function setNewpassword( $str, $throttle = true ) {
1499 $this->load();
1500 $this->mNewpassword = $this->encryptPassword( $str );
1501 if ( $throttle ) {
1502 $this->mNewpassTime = wfTimestampNow();
1507 * Returns true if a password reminder email has already been sent within
1508 * the last $wgPasswordReminderResendTime hours
1510 function isPasswordReminderThrottled() {
1511 global $wgPasswordReminderResendTime;
1512 $this->load();
1513 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1514 return false;
1516 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1517 return time() < $expiry;
1520 function getEmail() {
1521 $this->load();
1522 return $this->mEmail;
1525 function getEmailAuthenticationTimestamp() {
1526 $this->load();
1527 return $this->mEmailAuthenticated;
1530 function setEmail( $str ) {
1531 $this->load();
1532 $this->mEmail = $str;
1535 function getRealName() {
1536 $this->load();
1537 return $this->mRealName;
1540 function setRealName( $str ) {
1541 $this->load();
1542 $this->mRealName = $str;
1546 * @param string $oname The option to check
1547 * @param string $defaultOverride A default value returned if the option does not exist
1548 * @return string
1550 function getOption( $oname, $defaultOverride = '' ) {
1551 $this->load();
1553 if ( is_null( $this->mOptions ) ) {
1554 if($defaultOverride != '') {
1555 return $defaultOverride;
1557 $this->mOptions = User::getDefaultOptions();
1560 if ( array_key_exists( $oname, $this->mOptions ) ) {
1561 return trim( $this->mOptions[$oname] );
1562 } else {
1563 return $defaultOverride;
1568 * Get the user's date preference, including some important migration for
1569 * old user rows.
1571 function getDatePreference() {
1572 if ( is_null( $this->mDatePreference ) ) {
1573 global $wgLang;
1574 $value = $this->getOption( 'date' );
1575 $map = $wgLang->getDatePreferenceMigrationMap();
1576 if ( isset( $map[$value] ) ) {
1577 $value = $map[$value];
1579 $this->mDatePreference = $value;
1581 return $this->mDatePreference;
1585 * @param string $oname The option to check
1586 * @return bool False if the option is not selected, true if it is
1588 function getBoolOption( $oname ) {
1589 return (bool)$this->getOption( $oname );
1593 * Get an option as an integer value from the source string.
1594 * @param string $oname The option to check
1595 * @param int $default Optional value to return if option is unset/blank.
1596 * @return int
1598 function getIntOption( $oname, $default=0 ) {
1599 $val = $this->getOption( $oname );
1600 if( $val == '' ) {
1601 $val = $default;
1603 return intval( $val );
1606 function setOption( $oname, $val ) {
1607 $this->load();
1608 if ( is_null( $this->mOptions ) ) {
1609 $this->mOptions = User::getDefaultOptions();
1611 if ( $oname == 'skin' ) {
1612 # Clear cached skin, so the new one displays immediately in Special:Preferences
1613 unset( $this->mSkin );
1615 // Filter out any newlines that may have passed through input validation.
1616 // Newlines are used to separate items in the options blob.
1617 $val = str_replace( "\r\n", "\n", $val );
1618 $val = str_replace( "\r", "\n", $val );
1619 $val = str_replace( "\n", " ", $val );
1620 $this->mOptions[$oname] = $val;
1623 function getRights() {
1624 if ( is_null( $this->mRights ) ) {
1625 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1626 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
1628 return $this->mRights;
1632 * Get the list of explicit group memberships this user has.
1633 * The implicit * and user groups are not included.
1634 * @return array of strings
1636 function getGroups() {
1637 $this->load();
1638 return $this->mGroups;
1642 * Get the list of implicit group memberships this user has.
1643 * This includes all explicit groups, plus 'user' if logged in,
1644 * '*' for all accounts and autopromoted groups
1645 * @param boolean $recache Don't use the cache
1646 * @return array of strings
1648 function getEffectiveGroups( $recache = false ) {
1649 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
1650 $this->load();
1651 $this->mEffectiveGroups = $this->mGroups;
1652 $this->mEffectiveGroups[] = '*';
1653 if( $this->mId ) {
1654 $this->mEffectiveGroups[] = 'user';
1656 $this->mEffectiveGroups = array_unique( array_merge(
1657 $this->mEffectiveGroups,
1658 Autopromote::getAutopromoteGroups( $this )
1659 ) );
1661 # Hook for additional groups
1662 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
1665 return $this->mEffectiveGroups;
1668 /* Return the edit count for the user. This is where User::edits should have been */
1669 function getEditCount() {
1670 if ($this->mId) {
1671 if ( !isset( $this->mEditCount ) ) {
1672 /* Populate the count, if it has not been populated yet */
1673 $this->mEditCount = User::edits($this->mId);
1675 return $this->mEditCount;
1676 } else {
1677 /* nil */
1678 return null;
1683 * Add the user to the given group.
1684 * This takes immediate effect.
1685 * @param string $group
1687 function addGroup( $group ) {
1688 $this->load();
1689 $dbw = wfGetDB( DB_MASTER );
1690 if( $this->getId() ) {
1691 $dbw->insert( 'user_groups',
1692 array(
1693 'ug_user' => $this->getID(),
1694 'ug_group' => $group,
1696 'User::addGroup',
1697 array( 'IGNORE' ) );
1700 $this->mGroups[] = $group;
1701 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1703 $this->invalidateCache();
1707 * Remove the user from the given group.
1708 * This takes immediate effect.
1709 * @param string $group
1711 function removeGroup( $group ) {
1712 $this->load();
1713 $dbw = wfGetDB( DB_MASTER );
1714 $dbw->delete( 'user_groups',
1715 array(
1716 'ug_user' => $this->getID(),
1717 'ug_group' => $group,
1719 'User::removeGroup' );
1721 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1722 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1724 $this->invalidateCache();
1729 * A more legible check for non-anonymousness.
1730 * Returns true if the user is not an anonymous visitor.
1732 * @return bool
1734 function isLoggedIn() {
1735 return $this->getID() != 0;
1739 * A more legible check for anonymousness.
1740 * Returns true if the user is an anonymous visitor.
1742 * @return bool
1744 function isAnon() {
1745 return !$this->isLoggedIn();
1749 * Whether the user is a bot
1750 * @deprecated
1752 function isBot() {
1753 return $this->isAllowed( 'bot' );
1757 * Check if user is allowed to access a feature / make an action
1758 * @param string $action Action to be checked
1759 * @return boolean True: action is allowed, False: action should not be allowed
1761 function isAllowed($action='') {
1762 if ( $action === '' )
1763 // In the spirit of DWIM
1764 return true;
1766 return in_array( $action, $this->getRights() );
1770 * Load a skin if it doesn't exist or return it
1771 * @todo FIXME : need to check the old failback system [AV]
1773 function &getSkin() {
1774 global $wgRequest;
1775 if ( ! isset( $this->mSkin ) ) {
1776 wfProfileIn( __METHOD__ );
1778 # get the user skin
1779 $userSkin = $this->getOption( 'skin' );
1780 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1782 $this->mSkin =& Skin::newFromKey( $userSkin );
1783 wfProfileOut( __METHOD__ );
1785 return $this->mSkin;
1788 /**#@+
1789 * @param string $title Article title to look at
1793 * Check watched status of an article
1794 * @return bool True if article is watched
1796 function isWatched( $title ) {
1797 $wl = WatchedItem::fromUserTitle( $this, $title );
1798 return $wl->isWatched();
1802 * Watch an article
1804 function addWatch( $title ) {
1805 $wl = WatchedItem::fromUserTitle( $this, $title );
1806 $wl->addWatch();
1807 $this->invalidateCache();
1811 * Stop watching an article
1813 function removeWatch( $title ) {
1814 $wl = WatchedItem::fromUserTitle( $this, $title );
1815 $wl->removeWatch();
1816 $this->invalidateCache();
1820 * Clear the user's notification timestamp for the given title.
1821 * If e-notif e-mails are on, they will receive notification mails on
1822 * the next change of the page if it's watched etc.
1824 function clearNotification( &$title ) {
1825 global $wgUser, $wgUseEnotif;
1827 # Do nothing if the database is locked to writes
1828 if( wfReadOnly() ) {
1829 return;
1832 if ($title->getNamespace() == NS_USER_TALK &&
1833 $title->getText() == $this->getName() ) {
1834 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1835 return;
1836 $this->setNewtalk( false );
1839 if( !$wgUseEnotif ) {
1840 return;
1843 if( $this->isAnon() ) {
1844 // Nothing else to do...
1845 return;
1848 // Only update the timestamp if the page is being watched.
1849 // The query to find out if it is watched is cached both in memcached and per-invocation,
1850 // and when it does have to be executed, it can be on a slave
1851 // If this is the user's newtalk page, we always update the timestamp
1852 if ($title->getNamespace() == NS_USER_TALK &&
1853 $title->getText() == $wgUser->getName())
1855 $watched = true;
1856 } elseif ( $this->getID() == $wgUser->getID() ) {
1857 $watched = $title->userIsWatching();
1858 } else {
1859 $watched = true;
1862 // If the page is watched by the user (or may be watched), update the timestamp on any
1863 // any matching rows
1864 if ( $watched ) {
1865 $dbw = wfGetDB( DB_MASTER );
1866 $dbw->update( 'watchlist',
1867 array( /* SET */
1868 'wl_notificationtimestamp' => NULL
1869 ), array( /* WHERE */
1870 'wl_title' => $title->getDBkey(),
1871 'wl_namespace' => $title->getNamespace(),
1872 'wl_user' => $this->getID()
1873 ), 'User::clearLastVisited'
1878 /**#@-*/
1881 * Resets all of the given user's page-change notification timestamps.
1882 * If e-notif e-mails are on, they will receive notification mails on
1883 * the next change of any watched page.
1885 * @param int $currentUser user ID number
1886 * @public
1888 function clearAllNotifications( $currentUser ) {
1889 global $wgUseEnotif;
1890 if ( !$wgUseEnotif ) {
1891 $this->setNewtalk( false );
1892 return;
1894 if( $currentUser != 0 ) {
1896 $dbw = wfGetDB( DB_MASTER );
1897 $dbw->update( 'watchlist',
1898 array( /* SET */
1899 'wl_notificationtimestamp' => NULL
1900 ), array( /* WHERE */
1901 'wl_user' => $currentUser
1902 ), __METHOD__
1905 # we also need to clear here the "you have new message" notification for the own user_talk page
1906 # This is cleared one page view later in Article::viewUpdates();
1911 * @private
1912 * @return string Encoding options
1914 function encodeOptions() {
1915 $this->load();
1916 if ( is_null( $this->mOptions ) ) {
1917 $this->mOptions = User::getDefaultOptions();
1919 $a = array();
1920 foreach ( $this->mOptions as $oname => $oval ) {
1921 array_push( $a, $oname.'='.$oval );
1923 $s = implode( "\n", $a );
1924 return $s;
1928 * @private
1930 function decodeOptions( $str ) {
1931 $this->mOptions = array();
1932 $a = explode( "\n", $str );
1933 foreach ( $a as $s ) {
1934 $m = array();
1935 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1936 $this->mOptions[$m[1]] = $m[2];
1941 function setCookies() {
1942 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1943 $this->load();
1944 if ( 0 == $this->mId ) return;
1945 $exp = time() + $wgCookieExpiration;
1947 $_SESSION['wsUserID'] = $this->mId;
1948 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1950 $_SESSION['wsUserName'] = $this->getName();
1951 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1953 $_SESSION['wsToken'] = $this->mToken;
1954 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1955 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1956 } else {
1957 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1962 * Logout user.
1964 function logout() {
1965 global $wgUser;
1966 if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
1967 $this->doLogout();
1968 wfRunHooks( 'UserLogoutComplete', array(&$wgUser) );
1973 * Really logout user
1974 * Clears the cookies and session, resets the instance cache
1976 function doLogout() {
1977 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1978 $this->clearInstanceCache( 'defaults' );
1980 $_SESSION['wsUserID'] = 0;
1982 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1983 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1985 # Remember when user logged out, to prevent seeing cached pages
1986 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1990 * Save object settings into database
1991 * @todo Only rarely do all these fields need to be set!
1993 function saveSettings() {
1994 $this->load();
1995 if ( wfReadOnly() ) { return; }
1996 if ( 0 == $this->mId ) { return; }
1998 $this->mTouched = self::newTouchedTimestamp();
2000 $dbw = wfGetDB( DB_MASTER );
2001 $dbw->update( 'user',
2002 array( /* SET */
2003 'user_name' => $this->mName,
2004 'user_password' => $this->mPassword,
2005 'user_newpassword' => $this->mNewpassword,
2006 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2007 'user_real_name' => $this->mRealName,
2008 'user_email' => $this->mEmail,
2009 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2010 'user_options' => $this->encodeOptions(),
2011 'user_touched' => $dbw->timestamp($this->mTouched),
2012 'user_token' => $this->mToken
2013 ), array( /* WHERE */
2014 'user_id' => $this->mId
2015 ), __METHOD__
2017 $this->clearSharedCache();
2022 * Checks if a user with the given name exists, returns the ID.
2024 function idForName() {
2025 $s = trim( $this->getName() );
2026 if ( $s === '' ) return 0;
2028 $dbr = wfGetDB( DB_SLAVE );
2029 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2030 if ( $id === false ) {
2031 $id = 0;
2033 return $id;
2037 * Add a user to the database, return the user object
2039 * @param string $name The user's name
2040 * @param array $params Associative array of non-default parameters to save to the database:
2041 * password The user's password. Password logins will be disabled if this is omitted.
2042 * newpassword A temporary password mailed to the user
2043 * email The user's email address
2044 * email_authenticated The email authentication timestamp
2045 * real_name The user's real name
2046 * options An associative array of non-default options
2047 * token Random authentication token. Do not set.
2048 * registration Registration timestamp. Do not set.
2050 * @return User object, or null if the username already exists
2052 static function createNew( $name, $params = array() ) {
2053 $user = new User;
2054 $user->load();
2055 if ( isset( $params['options'] ) ) {
2056 $user->mOptions = $params['options'] + $user->mOptions;
2057 unset( $params['options'] );
2059 $dbw = wfGetDB( DB_MASTER );
2060 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2061 $fields = array(
2062 'user_id' => $seqVal,
2063 'user_name' => $name,
2064 'user_password' => $user->mPassword,
2065 'user_newpassword' => $user->mNewpassword,
2066 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2067 'user_email' => $user->mEmail,
2068 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2069 'user_real_name' => $user->mRealName,
2070 'user_options' => $user->encodeOptions(),
2071 'user_token' => $user->mToken,
2072 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2073 'user_editcount' => 0,
2075 foreach ( $params as $name => $value ) {
2076 $fields["user_$name"] = $value;
2078 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2079 if ( $dbw->affectedRows() ) {
2080 $newUser = User::newFromId( $dbw->insertId() );
2081 } else {
2082 $newUser = null;
2084 return $newUser;
2088 * Add an existing user object to the database
2090 function addToDatabase() {
2091 $this->load();
2092 $dbw = wfGetDB( DB_MASTER );
2093 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2094 $dbw->insert( 'user',
2095 array(
2096 'user_id' => $seqVal,
2097 'user_name' => $this->mName,
2098 'user_password' => $this->mPassword,
2099 'user_newpassword' => $this->mNewpassword,
2100 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2101 'user_email' => $this->mEmail,
2102 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2103 'user_real_name' => $this->mRealName,
2104 'user_options' => $this->encodeOptions(),
2105 'user_token' => $this->mToken,
2106 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2107 'user_editcount' => 0,
2108 ), __METHOD__
2110 $this->mId = $dbw->insertId();
2112 # Clear instance cache other than user table data, which is already accurate
2113 $this->clearInstanceCache();
2117 * If the (non-anonymous) user is blocked, this function will block any IP address
2118 * that they successfully log on from.
2120 function spreadBlock() {
2121 wfDebug( __METHOD__."()\n" );
2122 $this->load();
2123 if ( $this->mId == 0 ) {
2124 return;
2127 $userblock = Block::newFromDB( '', $this->mId );
2128 if ( !$userblock ) {
2129 return;
2132 $userblock->doAutoblock( wfGetIp() );
2137 * Generate a string which will be different for any combination of
2138 * user options which would produce different parser output.
2139 * This will be used as part of the hash key for the parser cache,
2140 * so users will the same options can share the same cached data
2141 * safely.
2143 * Extensions which require it should install 'PageRenderingHash' hook,
2144 * which will give them a chance to modify this key based on their own
2145 * settings.
2147 * @return string
2149 function getPageRenderingHash() {
2150 global $wgContLang, $wgUseDynamicDates, $wgLang;
2151 if( $this->mHash ){
2152 return $this->mHash;
2155 // stubthreshold is only included below for completeness,
2156 // it will always be 0 when this function is called by parsercache.
2158 $confstr = $this->getOption( 'math' );
2159 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2160 if ( $wgUseDynamicDates ) {
2161 $confstr .= '!' . $this->getDatePreference();
2163 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2164 $confstr .= '!' . $wgLang->getCode();
2165 $confstr .= '!' . $this->getOption( 'thumbsize' );
2166 // add in language specific options, if any
2167 $extra = $wgContLang->getExtraHashOptions();
2168 $confstr .= $extra;
2170 // Give a chance for extensions to modify the hash, if they have
2171 // extra options or other effects on the parser cache.
2172 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2174 // Make it a valid memcached key fragment
2175 $confstr = str_replace( ' ', '_', $confstr );
2176 $this->mHash = $confstr;
2177 return $confstr;
2180 function isBlockedFromCreateAccount() {
2181 $this->getBlockedStatus();
2182 return $this->mBlock && $this->mBlock->mCreateAccount;
2186 * Determine if the user is blocked from using Special:Emailuser.
2188 * @public
2189 * @return boolean
2191 function isBlockedFromEmailuser() {
2192 $this->getBlockedStatus();
2193 return $this->mBlock && $this->mBlock->mBlockEmail;
2196 function isAllowedToCreateAccount() {
2197 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2201 * @deprecated
2203 function setLoaded( $loaded ) {}
2206 * Get this user's personal page title.
2208 * @return Title
2209 * @public
2211 function getUserPage() {
2212 return Title::makeTitle( NS_USER, $this->getName() );
2216 * Get this user's talk page title.
2218 * @return Title
2219 * @public
2221 function getTalkPage() {
2222 $title = $this->getUserPage();
2223 return $title->getTalkPage();
2227 * @static
2229 function getMaxID() {
2230 static $res; // cache
2232 if ( isset( $res ) )
2233 return $res;
2234 else {
2235 $dbr = wfGetDB( DB_SLAVE );
2236 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2241 * Determine whether the user is a newbie. Newbies are either
2242 * anonymous IPs, or the most recently created accounts.
2243 * @return bool True if it is a newbie.
2245 function isNewbie() {
2246 return !$this->isAllowed( 'autoconfirmed' );
2250 * Check to see if the given clear-text password is one of the accepted passwords
2251 * @param string $password User password.
2252 * @return bool True if the given password is correct otherwise False.
2254 function checkPassword( $password ) {
2255 global $wgAuth;
2256 $this->load();
2258 // Even though we stop people from creating passwords that
2259 // are shorter than this, doesn't mean people wont be able
2260 // to. Certain authentication plugins do NOT want to save
2261 // domain passwords in a mysql database, so we should
2262 // check this (incase $wgAuth->strict() is false).
2263 if( !$this->isValidPassword( $password ) ) {
2264 return false;
2267 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2268 return true;
2269 } elseif( $wgAuth->strict() ) {
2270 /* Auth plugin doesn't allow local authentication */
2271 return false;
2272 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2273 /* Auth plugin doesn't allow local authentication for this user name */
2274 return false;
2276 $ep = $this->encryptPassword( $password );
2277 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
2278 return true;
2279 } elseif ( function_exists( 'iconv' ) ) {
2280 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2281 # Check for this with iconv
2282 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ) );
2283 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
2284 return true;
2287 return false;
2291 * Check if the given clear-text password matches the temporary password
2292 * sent by e-mail for password reset operations.
2293 * @return bool
2295 function checkTemporaryPassword( $plaintext ) {
2296 $hash = $this->encryptPassword( $plaintext );
2297 return $hash === $this->mNewpassword;
2301 * Initialize (if necessary) and return a session token value
2302 * which can be used in edit forms to show that the user's
2303 * login credentials aren't being hijacked with a foreign form
2304 * submission.
2306 * @param mixed $salt - Optional function-specific data for hash.
2307 * Use a string or an array of strings.
2308 * @return string
2309 * @public
2311 function editToken( $salt = '' ) {
2312 if ( $this->isAnon() ) {
2313 return EDIT_TOKEN_SUFFIX;
2314 } else {
2315 if( !isset( $_SESSION['wsEditToken'] ) ) {
2316 $token = $this->generateToken();
2317 $_SESSION['wsEditToken'] = $token;
2318 } else {
2319 $token = $_SESSION['wsEditToken'];
2321 if( is_array( $salt ) ) {
2322 $salt = implode( '|', $salt );
2324 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2329 * Generate a hex-y looking random token for various uses.
2330 * Could be made more cryptographically sure if someone cares.
2331 * @return string
2333 function generateToken( $salt = '' ) {
2334 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2335 return md5( $token . $salt );
2339 * Check given value against the token value stored in the session.
2340 * A match should confirm that the form was submitted from the
2341 * user's own login session, not a form submission from a third-party
2342 * site.
2344 * @param string $val - the input value to compare
2345 * @param string $salt - Optional function-specific data for hash
2346 * @return bool
2347 * @public
2349 function matchEditToken( $val, $salt = '' ) {
2350 $sessionToken = $this->editToken( $salt );
2351 if ( $val != $sessionToken ) {
2352 wfDebug( "User::matchEditToken: broken session data\n" );
2354 return $val == $sessionToken;
2358 * Check whether the edit token is fine except for the suffix
2360 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2361 $sessionToken = $this->editToken( $salt );
2362 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2366 * Generate a new e-mail confirmation token and send a confirmation
2367 * mail to the user's given address.
2369 * @return mixed True on success, a WikiError object on failure.
2371 function sendConfirmationMail() {
2372 global $wgContLang;
2373 $expiration = null; // gets passed-by-ref and defined in next line.
2374 $url = $this->confirmationTokenUrl( $expiration );
2375 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2376 wfMsg( 'confirmemail_body',
2377 wfGetIP(),
2378 $this->getName(),
2379 $url,
2380 $wgContLang->timeanddate( $expiration, false ) ) );
2384 * Send an e-mail to this user's account. Does not check for
2385 * confirmed status or validity.
2387 * @param string $subject
2388 * @param string $body
2389 * @param string $from Optional from address; default $wgPasswordSender will be used otherwise.
2390 * @return mixed True on success, a WikiError object on failure.
2392 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2393 if( is_null( $from ) ) {
2394 global $wgPasswordSender;
2395 $from = $wgPasswordSender;
2398 $to = new MailAddress( $this );
2399 $sender = new MailAddress( $from );
2400 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2404 * Generate, store, and return a new e-mail confirmation code.
2405 * A hash (unsalted since it's used as a key) is stored.
2406 * @param &$expiration mixed output: accepts the expiration time
2407 * @return string
2408 * @private
2410 function confirmationToken( &$expiration ) {
2411 $now = time();
2412 $expires = $now + 7 * 24 * 60 * 60;
2413 $expiration = wfTimestamp( TS_MW, $expires );
2415 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2416 $hash = md5( $token );
2418 $dbw = wfGetDB( DB_MASTER );
2419 $dbw->update( 'user',
2420 array( 'user_email_token' => $hash,
2421 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
2422 array( 'user_id' => $this->mId ),
2423 __METHOD__ );
2425 return $token;
2429 * Generate and store a new e-mail confirmation token, and return
2430 * the URL the user can use to confirm.
2431 * @param &$expiration mixed output: accepts the expiration time
2432 * @return string
2433 * @private
2435 function confirmationTokenUrl( &$expiration ) {
2436 $token = $this->confirmationToken( $expiration );
2437 $title = SpecialPage::getTitleFor( 'Confirmemail', $token );
2438 return $title->getFullUrl();
2442 * Mark the e-mail address confirmed and save.
2444 function confirmEmail() {
2445 $this->load();
2446 $this->mEmailAuthenticated = wfTimestampNow();
2447 $this->saveSettings();
2448 return true;
2452 * Is this user allowed to send e-mails within limits of current
2453 * site configuration?
2454 * @return bool
2456 function canSendEmail() {
2457 $canSend = $this->isEmailConfirmed();
2458 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2459 return $canSend;
2463 * Is this user allowed to receive e-mails within limits of current
2464 * site configuration?
2465 * @return bool
2467 function canReceiveEmail() {
2468 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
2472 * Is this user's e-mail address valid-looking and confirmed within
2473 * limits of the current site configuration?
2475 * If $wgEmailAuthentication is on, this may require the user to have
2476 * confirmed their address by returning a code or using a password
2477 * sent to the address from the wiki.
2479 * @return bool
2481 function isEmailConfirmed() {
2482 global $wgEmailAuthentication;
2483 $this->load();
2484 $confirmed = true;
2485 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2486 if( $this->isAnon() )
2487 return false;
2488 if( !self::isValidEmailAddr( $this->mEmail ) )
2489 return false;
2490 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2491 return false;
2492 return true;
2493 } else {
2494 return $confirmed;
2499 * Return true if there is an outstanding request for e-mail confirmation.
2500 * @return bool
2502 function isEmailConfirmationPending() {
2503 global $wgEmailAuthentication;
2504 return $wgEmailAuthentication &&
2505 !$this->isEmailConfirmed() &&
2506 $this->mEmailToken &&
2507 $this->mEmailTokenExpires > wfTimestamp();
2511 * Get the timestamp of account creation, or false for
2512 * non-existent/anonymous user accounts
2514 * @return mixed
2516 public function getRegistration() {
2517 return $this->mId > 0
2518 ? $this->mRegistration
2519 : false;
2523 * @param array $groups list of groups
2524 * @return array list of permission key names for given groups combined
2525 * @static
2527 static function getGroupPermissions( $groups ) {
2528 global $wgGroupPermissions;
2529 $rights = array();
2530 foreach( $groups as $group ) {
2531 if( isset( $wgGroupPermissions[$group] ) ) {
2532 $rights = array_merge( $rights,
2533 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2536 return $rights;
2540 * @param string $group key name
2541 * @return string localized descriptive name for group, if provided
2542 * @static
2544 static function getGroupName( $group ) {
2545 global $wgMessageCache;
2546 $wgMessageCache->loadAllMessages();
2547 $key = "group-$group";
2548 $name = wfMsg( $key );
2549 return $name == '' || wfEmptyMsg( $key, $name )
2550 ? $group
2551 : $name;
2555 * @param string $group key name
2556 * @return string localized descriptive name for member of a group, if provided
2557 * @static
2559 static function getGroupMember( $group ) {
2560 global $wgMessageCache;
2561 $wgMessageCache->loadAllMessages();
2562 $key = "group-$group-member";
2563 $name = wfMsg( $key );
2564 return $name == '' || wfEmptyMsg( $key, $name )
2565 ? $group
2566 : $name;
2570 * Return the set of defined explicit groups.
2571 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2572 * groups are not included, as they are defined
2573 * automatically, not in the database.
2574 * @return array
2575 * @static
2577 static function getAllGroups() {
2578 global $wgGroupPermissions;
2579 return array_diff(
2580 array_keys( $wgGroupPermissions ),
2581 self::getImplicitGroups()
2586 * Get a list of implicit groups
2588 * @return array
2590 public static function getImplicitGroups() {
2591 global $wgImplicitGroups;
2592 $groups = $wgImplicitGroups;
2593 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
2594 return $groups;
2598 * Get the title of a page describing a particular group
2600 * @param $group Name of the group
2601 * @return mixed
2603 static function getGroupPage( $group ) {
2604 global $wgMessageCache;
2605 $wgMessageCache->loadAllMessages();
2606 $page = wfMsgForContent( 'grouppage-' . $group );
2607 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2608 $title = Title::newFromText( $page );
2609 if( is_object( $title ) )
2610 return $title;
2612 return false;
2616 * Create a link to the group in HTML, if available
2618 * @param $group Name of the group
2619 * @param $text The text of the link
2620 * @return mixed
2622 static function makeGroupLinkHTML( $group, $text = '' ) {
2623 if( $text == '' ) {
2624 $text = self::getGroupName( $group );
2626 $title = self::getGroupPage( $group );
2627 if( $title ) {
2628 global $wgUser;
2629 $sk = $wgUser->getSkin();
2630 return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
2631 } else {
2632 return $text;
2637 * Create a link to the group in Wikitext, if available
2639 * @param $group Name of the group
2640 * @param $text The text of the link (by default, the name of the group)
2641 * @return mixed
2643 static function makeGroupLinkWiki( $group, $text = '' ) {
2644 if( $text == '' ) {
2645 $text = self::getGroupName( $group );
2647 $title = self::getGroupPage( $group );
2648 if( $title ) {
2649 $page = $title->getPrefixedText();
2650 return "[[$page|$text]]";
2651 } else {
2652 return $text;
2657 * Increment the user's edit-count field.
2658 * Will have no effect for anonymous users.
2660 function incEditCount() {
2661 if( !$this->isAnon() ) {
2662 $dbw = wfGetDB( DB_MASTER );
2663 $dbw->update( 'user',
2664 array( 'user_editcount=user_editcount+1' ),
2665 array( 'user_id' => $this->getId() ),
2666 __METHOD__ );
2668 // Lazy initialization check...
2669 if( $dbw->affectedRows() == 0 ) {
2670 // Pull from a slave to be less cruel to servers
2671 // Accuracy isn't the point anyway here
2672 $dbr = wfGetDB( DB_SLAVE );
2673 $count = $dbr->selectField( 'revision',
2674 'COUNT(rev_user)',
2675 array( 'rev_user' => $this->getId() ),
2676 __METHOD__ );
2678 // Now here's a goddamn hack...
2679 if( $dbr !== $dbw ) {
2680 // If we actually have a slave server, the count is
2681 // at least one behind because the current transaction
2682 // has not been committed and replicated.
2683 $count++;
2684 } else {
2685 // But if DB_SLAVE is selecting the master, then the
2686 // count we just read includes the revision that was
2687 // just added in the working transaction.
2690 $dbw->update( 'user',
2691 array( 'user_editcount' => $count ),
2692 array( 'user_id' => $this->getId() ),
2693 __METHOD__ );
2696 // edit count in user cache too
2697 $this->invalidateCache();