Create User::getGroupsInRoles(). Given an array of roles (edit, read, delete), return...
[mediawiki.git] / includes / User.php
blob09326cc1f549cc439b492a0aef91740a7156b027
1 <?php
2 /**
3 * See user.txt
4 * @file
5 */
7 # Number of characters in user_token field
8 define( 'USER_TOKEN_LENGTH', 32 );
10 # Serialized record version
11 define( 'MW_USER_VERSION', 6 );
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 * @ingroup 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 * Core rights
110 * Each of these should have a corresponding message of the form "right-$right"
112 static $mCoreRights = array(
113 'apihighlimits',
114 'autoconfirmed',
115 'autopatrol',
116 'bigdelete',
117 'block',
118 'blockemail',
119 'bot',
120 'browsearchive',
121 'createaccount',
122 'createpage',
123 'createtalk',
124 'delete',
125 'deletedhistory',
126 'edit',
127 'editinterface',
128 'editusercssjs',
129 'import',
130 'importupload',
131 'ipblock-exempt',
132 'markbotedits',
133 'minoredit',
134 'move',
135 'nominornewtalk',
136 'noratelimit',
137 'patrol',
138 'protect',
139 'proxyunbannable',
140 'purge',
141 'read',
142 'reupload',
143 'reupload-shared',
144 'rollback',
145 'suppressredirect',
146 'trackback',
147 'undelete',
148 'unwatchedpages',
149 'upload',
150 'upload_by_url',
151 'userrights',
153 static $mAllRights = false;
156 * The cache variable declarations
158 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
159 $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
160 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
163 * Whether the cache variables have been loaded
165 var $mDataLoaded;
168 * Initialisation data source if mDataLoaded==false. May be one of:
169 * defaults anonymous user initialised from class defaults
170 * name initialise from mName
171 * id initialise from mId
172 * session log in from cookies or session if possible
174 * Use the User::newFrom*() family of functions to set this.
176 var $mFrom;
179 * Lazy-initialised variables, invalidated with clearInstanceCache
181 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
182 $mBlockreason, $mBlock, $mEffectiveGroups;
185 * Lightweight constructor for anonymous user
186 * Use the User::newFrom* factory functions for other kinds of users
188 function User() {
189 $this->clearInstanceCache( 'defaults' );
193 * Load the user table data for this object from the source given by mFrom
195 function load() {
196 if ( $this->mDataLoaded ) {
197 return;
199 wfProfileIn( __METHOD__ );
201 # Set it now to avoid infinite recursion in accessors
202 $this->mDataLoaded = true;
204 switch ( $this->mFrom ) {
205 case 'defaults':
206 $this->loadDefaults();
207 break;
208 case 'name':
209 $this->mId = self::idFromName( $this->mName );
210 if ( !$this->mId ) {
211 # Nonexistent user placeholder object
212 $this->loadDefaults( $this->mName );
213 } else {
214 $this->loadFromId();
216 break;
217 case 'id':
218 $this->loadFromId();
219 break;
220 case 'session':
221 $this->loadFromSession();
222 break;
223 default:
224 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
226 wfProfileOut( __METHOD__ );
230 * Load user table data given mId
231 * @return false if the ID does not exist, true otherwise
232 * @private
234 function loadFromId() {
235 global $wgMemc;
236 if ( $this->mId == 0 ) {
237 $this->loadDefaults();
238 return false;
241 # Try cache
242 $key = wfMemcKey( 'user', 'id', $this->mId );
243 $data = $wgMemc->get( $key );
244 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
245 # Object is expired, load from DB
246 $data = false;
249 if ( !$data ) {
250 wfDebug( "Cache miss for user {$this->mId}\n" );
251 # Load from DB
252 if ( !$this->loadFromDatabase() ) {
253 # Can't load from ID, user is anonymous
254 return false;
256 $this->saveToCache();
257 } else {
258 wfDebug( "Got user {$this->mId} from cache\n" );
259 # Restore from cache
260 foreach ( self::$mCacheVars as $name ) {
261 $this->$name = $data[$name];
264 return true;
268 * Save user data to the shared cache
270 function saveToCache() {
271 $this->load();
272 $this->loadGroups();
273 if ( $this->isAnon() ) {
274 // Anonymous users are uncached
275 return;
277 $data = array();
278 foreach ( self::$mCacheVars as $name ) {
279 $data[$name] = $this->$name;
281 $data['mVersion'] = MW_USER_VERSION;
282 $key = wfMemcKey( 'user', 'id', $this->mId );
283 global $wgMemc;
284 $wgMemc->set( $key, $data );
288 * Static factory method for creation from username.
290 * This is slightly less efficient than newFromId(), so use newFromId() if
291 * you have both an ID and a name handy.
293 * @param $name String: username, validated by Title:newFromText()
294 * @param $validate Mixed: validate username. Takes the same parameters as
295 * User::getCanonicalName(), except that true is accepted as an alias
296 * for 'valid', for BC.
298 * @return User object, or null if the username is invalid. If the username
299 * is not present in the database, the result will be a user object with
300 * a name, zero user ID and default settings.
302 static function newFromName( $name, $validate = 'valid' ) {
303 if ( $validate === true ) {
304 $validate = 'valid';
306 $name = self::getCanonicalName( $name, $validate );
307 if ( $name === false ) {
308 return null;
309 } else {
310 # Create unloaded user object
311 $u = new User;
312 $u->mName = $name;
313 $u->mFrom = 'name';
314 return $u;
318 static function newFromId( $id ) {
319 $u = new User;
320 $u->mId = $id;
321 $u->mFrom = 'id';
322 return $u;
326 * Factory method to fetch whichever user has a given email confirmation code.
327 * This code is generated when an account is created or its e-mail address
328 * has changed.
330 * If the code is invalid or has expired, returns NULL.
332 * @param $code string
333 * @return User
335 static function newFromConfirmationCode( $code ) {
336 $dbr = wfGetDB( DB_SLAVE );
337 $id = $dbr->selectField( 'user', 'user_id', array(
338 'user_email_token' => md5( $code ),
339 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
340 ) );
341 if( $id !== false ) {
342 return User::newFromId( $id );
343 } else {
344 return null;
349 * Create a new user object using data from session or cookies. If the
350 * login credentials are invalid, the result is an anonymous user.
352 * @return User
354 static function newFromSession() {
355 $user = new User;
356 $user->mFrom = 'session';
357 return $user;
361 * Create a new user object from a user row.
362 * The row should have all fields from the user table in it.
364 static function newFromRow( $row ) {
365 $user = new User;
366 $user->loadFromRow( $row );
367 return $user;
371 * Get username given an id.
372 * @param $id Integer: database user id
373 * @return string Nickname of a user
375 static function whoIs( $id ) {
376 $dbr = wfGetDB( DB_SLAVE );
377 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
381 * Get the real name of a user given their identifier
383 * @param $id Int: database user id
384 * @return string Real name of a user
386 static function whoIsReal( $id ) {
387 $dbr = wfGetDB( DB_SLAVE );
388 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
392 * Get database id given a user name
393 * @param $name String: nickname of a user
394 * @return integer|null Database user id (null: if non existent
395 * @static
397 static function idFromName( $name ) {
398 $nt = Title::newFromText( $name );
399 if( is_null( $nt ) ) {
400 # Illegal name
401 return null;
403 $dbr = wfGetDB( DB_SLAVE );
404 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
406 if ( $s === false ) {
407 return 0;
408 } else {
409 return $s->user_id;
414 * Does the string match an anonymous IPv4 address?
416 * This function exists for username validation, in order to reject
417 * usernames which are similar in form to IP addresses. Strings such
418 * as 300.300.300.300 will return true because it looks like an IP
419 * address, despite not being strictly valid.
421 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
422 * address because the usemod software would "cloak" anonymous IP
423 * addresses like this, if we allowed accounts like this to be created
424 * new users could get the old edits of these anonymous users.
426 * @param $name String: nickname of a user
427 * @return bool
429 static function isIP( $name ) {
430 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
434 * Is the input a valid username?
436 * Checks if the input is a valid username, we don't want an empty string,
437 * an IP address, anything that containins slashes (would mess up subpages),
438 * is longer than the maximum allowed username size or doesn't begin with
439 * a capital letter.
441 * @param $name string
442 * @return bool
444 static function isValidUserName( $name ) {
445 global $wgContLang, $wgMaxNameChars;
447 if ( $name == ''
448 || User::isIP( $name )
449 || strpos( $name, '/' ) !== false
450 || strlen( $name ) > $wgMaxNameChars
451 || $name != $wgContLang->ucfirst( $name ) ) {
452 wfDebugLog( 'username', __METHOD__ .
453 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
454 return false;
457 // Ensure that the name can't be misresolved as a different title,
458 // such as with extra namespace keys at the start.
459 $parsed = Title::newFromText( $name );
460 if( is_null( $parsed )
461 || $parsed->getNamespace()
462 || strcmp( $name, $parsed->getPrefixedText() ) ) {
463 wfDebugLog( 'username', __METHOD__ .
464 ": '$name' invalid due to ambiguous prefixes" );
465 return false;
468 // Check an additional blacklist of troublemaker characters.
469 // Should these be merged into the title char list?
470 $unicodeBlacklist = '/[' .
471 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
472 '\x{00a0}' . # non-breaking space
473 '\x{2000}-\x{200f}' . # various whitespace
474 '\x{2028}-\x{202f}' . # breaks and control chars
475 '\x{3000}' . # ideographic space
476 '\x{e000}-\x{f8ff}' . # private use
477 ']/u';
478 if( preg_match( $unicodeBlacklist, $name ) ) {
479 wfDebugLog( 'username', __METHOD__ .
480 ": '$name' invalid due to blacklisted characters" );
481 return false;
484 return true;
488 * Usernames which fail to pass this function will be blocked
489 * from user login and new account registrations, but may be used
490 * internally by batch processes.
492 * If an account already exists in this form, login will be blocked
493 * by a failure to pass this function.
495 * @param $name string
496 * @return bool
498 static function isUsableName( $name ) {
499 global $wgReservedUsernames;
500 // Must be a valid username, obviously ;)
501 if ( !self::isValidUserName( $name ) ) {
502 return false;
505 // Certain names may be reserved for batch processes.
506 foreach ( $wgReservedUsernames as $reserved ) {
507 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
508 $reserved = wfMsgForContent( substr( $reserved, 4 ) );
510 if ( $reserved == $name ) {
511 return false;
514 return true;
518 * Usernames which fail to pass this function will be blocked
519 * from new account registrations, but may be used internally
520 * either by batch processes or by user accounts which have
521 * already been created.
523 * Additional character blacklisting may be added here
524 * rather than in isValidUserName() to avoid disrupting
525 * existing accounts.
527 * @param $name string
528 * @return bool
530 static function isCreatableName( $name ) {
531 return
532 self::isUsableName( $name ) &&
534 // Registration-time character blacklisting...
535 strpos( $name, '@' ) === false;
539 * Is the input a valid password for this user?
541 * @param $password String: desired password
542 * @return bool
544 function isValidPassword( $password ) {
545 global $wgMinimalPasswordLength, $wgContLang;
547 $result = null;
548 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
549 return $result;
550 if( $result === false )
551 return false;
553 // Password needs to be long enough, and can't be the same as the username
554 return strlen( $password ) >= $wgMinimalPasswordLength
555 && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
559 * Does a string look like an email address?
561 * There used to be a regular expression here, it got removed because it
562 * rejected valid addresses. Actually just check if there is '@' somewhere
563 * in the given address.
565 * @todo Check for RFC 2822 compilance (bug 959)
567 * @param $addr String: email address
568 * @return bool
570 public static function isValidEmailAddr( $addr ) {
571 $result = null;
572 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
573 return $result;
576 return strpos( $addr, '@' ) !== false;
580 * Given unvalidated user input, return a canonical username, or false if
581 * the username is invalid.
582 * @param $name string
583 * @param $validate Mixed: type of validation to use:
584 * false No validation
585 * 'valid' Valid for batch processes
586 * 'usable' Valid for batch processes and login
587 * 'creatable' Valid for batch processes, login and account creation
589 static function getCanonicalName( $name, $validate = 'valid' ) {
590 # Force usernames to capital
591 global $wgContLang;
592 $name = $wgContLang->ucfirst( $name );
594 # Reject names containing '#'; these will be cleaned up
595 # with title normalisation, but then it's too late to
596 # check elsewhere
597 if( strpos( $name, '#' ) !== false )
598 return false;
600 # Clean up name according to title rules
601 $t = Title::newFromText( $name );
602 if( is_null( $t ) ) {
603 return false;
606 # Reject various classes of invalid names
607 $name = $t->getText();
608 global $wgAuth;
609 $name = $wgAuth->getCanonicalName( $t->getText() );
611 switch ( $validate ) {
612 case false:
613 break;
614 case 'valid':
615 if ( !User::isValidUserName( $name ) ) {
616 $name = false;
618 break;
619 case 'usable':
620 if ( !User::isUsableName( $name ) ) {
621 $name = false;
623 break;
624 case 'creatable':
625 if ( !User::isCreatableName( $name ) ) {
626 $name = false;
628 break;
629 default:
630 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
632 return $name;
636 * Count the number of edits of a user
638 * It should not be static and some day should be merged as proper member function / deprecated -- domas
640 * @param $uid Int: the user ID to check
641 * @return int
643 static function edits( $uid ) {
644 wfProfileIn( __METHOD__ );
645 $dbr = wfGetDB( DB_SLAVE );
646 // check if the user_editcount field has been initialized
647 $field = $dbr->selectField(
648 'user', 'user_editcount',
649 array( 'user_id' => $uid ),
650 __METHOD__
653 if( $field === null ) { // it has not been initialized. do so.
654 $dbw = wfGetDB( DB_MASTER );
655 $count = $dbr->selectField(
656 'revision', 'count(*)',
657 array( 'rev_user' => $uid ),
658 __METHOD__
660 $dbw->update(
661 'user',
662 array( 'user_editcount' => $count ),
663 array( 'user_id' => $uid ),
664 __METHOD__
666 } else {
667 $count = $field;
669 wfProfileOut( __METHOD__ );
670 return $count;
674 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
675 * @todo hash random numbers to improve security, like generateToken()
677 * @return string
679 static function randomPassword() {
680 global $wgMinimalPasswordLength;
681 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
682 $l = strlen( $pwchars ) - 1;
684 $pwlength = max( 7, $wgMinimalPasswordLength );
685 $digit = mt_rand(0, $pwlength - 1);
686 $np = '';
687 for ( $i = 0; $i < $pwlength; $i++ ) {
688 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
690 return $np;
694 * Set cached properties to default. Note: this no longer clears
695 * uncached lazy-initialised properties. The constructor does that instead.
697 * @private
699 function loadDefaults( $name = false ) {
700 wfProfileIn( __METHOD__ );
702 global $wgCookiePrefix;
704 $this->mId = 0;
705 $this->mName = $name;
706 $this->mRealName = '';
707 $this->mPassword = $this->mNewpassword = '';
708 $this->mNewpassTime = null;
709 $this->mEmail = '';
710 $this->mOptions = null; # Defer init
712 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
713 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
714 } else {
715 $this->mTouched = '0'; # Allow any pages to be cached
718 $this->setToken(); # Random
719 $this->mEmailAuthenticated = null;
720 $this->mEmailToken = '';
721 $this->mEmailTokenExpires = null;
722 $this->mRegistration = wfTimestamp( TS_MW );
723 $this->mGroups = array();
725 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
727 wfProfileOut( __METHOD__ );
731 * Initialise php session
732 * @deprecated use wfSetupSession()
734 function SetupSession() {
735 wfDeprecated( __METHOD__ );
736 wfSetupSession();
740 * Load user data from the session or login cookie. If there are no valid
741 * credentials, initialises the user as an anon.
742 * @return true if the user is logged in, false otherwise
744 private function loadFromSession() {
745 global $wgMemc, $wgCookiePrefix;
747 $result = null;
748 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
749 if ( $result !== null ) {
750 return $result;
753 if ( isset( $_SESSION['wsUserID'] ) ) {
754 if ( 0 != $_SESSION['wsUserID'] ) {
755 $sId = $_SESSION['wsUserID'];
756 } else {
757 $this->loadDefaults();
758 return false;
760 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
761 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
762 $_SESSION['wsUserID'] = $sId;
763 } else {
764 $this->loadDefaults();
765 return false;
767 if ( isset( $_SESSION['wsUserName'] ) ) {
768 $sName = $_SESSION['wsUserName'];
769 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
770 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
771 $_SESSION['wsUserName'] = $sName;
772 } else {
773 $this->loadDefaults();
774 return false;
777 $passwordCorrect = FALSE;
778 $this->mId = $sId;
779 if ( !$this->loadFromId() ) {
780 # Not a valid ID, loadFromId has switched the object to anon for us
781 return false;
784 if ( isset( $_SESSION['wsToken'] ) ) {
785 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
786 $from = 'session';
787 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
788 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
789 $from = 'cookie';
790 } else {
791 # No session or persistent login cookie
792 $this->loadDefaults();
793 return false;
796 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
797 $_SESSION['wsToken'] = $this->mToken;
798 wfDebug( "Logged in from $from\n" );
799 return true;
800 } else {
801 # Invalid credentials
802 wfDebug( "Can't log in from $from, invalid credentials\n" );
803 $this->loadDefaults();
804 return false;
809 * Load user and user_group data from the database
810 * $this->mId must be set, this is how the user is identified.
812 * @return true if the user exists, false if the user is anonymous
813 * @private
815 function loadFromDatabase() {
816 # Paranoia
817 $this->mId = intval( $this->mId );
819 /** Anonymous user */
820 if( !$this->mId ) {
821 $this->loadDefaults();
822 return false;
825 $dbr = wfGetDB( DB_MASTER );
826 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
828 if ( $s !== false ) {
829 # Initialise user table data
830 $this->loadFromRow( $s );
831 $this->mGroups = null; // deferred
832 $this->getEditCount(); // revalidation for nulls
833 return true;
834 } else {
835 # Invalid user_id
836 $this->mId = 0;
837 $this->loadDefaults();
838 return false;
843 * Initialise the user object from a row from the user table
845 function loadFromRow( $row ) {
846 $this->mDataLoaded = true;
848 if ( isset( $row->user_id ) ) {
849 $this->mId = $row->user_id;
851 $this->mName = $row->user_name;
852 $this->mRealName = $row->user_real_name;
853 $this->mPassword = $row->user_password;
854 $this->mNewpassword = $row->user_newpassword;
855 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
856 $this->mEmail = $row->user_email;
857 $this->decodeOptions( $row->user_options );
858 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
859 $this->mToken = $row->user_token;
860 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
861 $this->mEmailToken = $row->user_email_token;
862 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
863 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
864 $this->mEditCount = $row->user_editcount;
868 * Load the groups from the database if they aren't already loaded
869 * @private
871 function loadGroups() {
872 if ( is_null( $this->mGroups ) ) {
873 $dbr = wfGetDB( DB_MASTER );
874 $res = $dbr->select( 'user_groups',
875 array( 'ug_group' ),
876 array( 'ug_user' => $this->mId ),
877 __METHOD__ );
878 $this->mGroups = array();
879 while( $row = $dbr->fetchObject( $res ) ) {
880 $this->mGroups[] = $row->ug_group;
886 * Clear various cached data stored in this object.
887 * @param $reloadFrom String: reload user and user_groups table data from a
888 * given source. May be "name", "id", "defaults", "session" or false for
889 * no reload.
891 function clearInstanceCache( $reloadFrom = false ) {
892 $this->mNewtalk = -1;
893 $this->mDatePreference = null;
894 $this->mBlockedby = -1; # Unset
895 $this->mHash = false;
896 $this->mSkin = null;
897 $this->mRights = null;
898 $this->mEffectiveGroups = null;
900 if ( $reloadFrom ) {
901 $this->mDataLoaded = false;
902 $this->mFrom = $reloadFrom;
907 * Combine the language default options with any site-specific options
908 * and add the default language variants.
909 * Not really private cause it's called by Language class
910 * @return array
911 * @private
913 static function getDefaultOptions() {
914 global $wgNamespacesToBeSearchedDefault;
916 * Site defaults will override the global/language defaults
918 global $wgDefaultUserOptions, $wgContLang;
919 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
922 * default language setting
924 $variant = $wgContLang->getPreferredVariant( false );
925 $defOpt['variant'] = $variant;
926 $defOpt['language'] = $variant;
928 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
929 $defOpt['searchNs'.$nsnum] = $val;
931 return $defOpt;
935 * Get a given default option value.
937 * @param $opt string
938 * @return string
940 public static function getDefaultOption( $opt ) {
941 $defOpts = self::getDefaultOptions();
942 if( isset( $defOpts[$opt] ) ) {
943 return $defOpts[$opt];
944 } else {
945 return '';
950 * Get a list of user toggle names
951 * @return array
953 static function getToggles() {
954 global $wgContLang;
955 $extraToggles = array();
956 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
957 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
962 * Get blocking information
963 * @private
964 * @param $bFromSlave Bool: specify whether to check slave or master. To
965 * improve performance, non-critical checks are done
966 * against slaves. Check when actually saving should be
967 * done against master.
969 function getBlockedStatus( $bFromSlave = true ) {
970 global $wgEnableSorbs, $wgProxyWhitelist;
972 if ( -1 != $this->mBlockedby ) {
973 wfDebug( "User::getBlockedStatus: already loaded.\n" );
974 return;
977 wfProfileIn( __METHOD__ );
978 wfDebug( __METHOD__.": checking...\n" );
980 // Initialize data...
981 // Otherwise something ends up stomping on $this->mBlockedby when
982 // things get lazy-loaded later, causing false positive block hits
983 // due to -1 !== 0. Probably session-related... Nothing should be
984 // overwriting mBlockedby, surely?
985 $this->load();
987 $this->mBlockedby = 0;
988 $this->mHideName = 0;
989 $ip = wfGetIP();
991 if ($this->isAllowed( 'ipblock-exempt' ) ) {
992 # Exempt from all types of IP-block
993 $ip = '';
996 # User/IP blocking
997 $this->mBlock = new Block();
998 $this->mBlock->fromMaster( !$bFromSlave );
999 if ( $this->mBlock->load( $ip , $this->mId ) ) {
1000 wfDebug( __METHOD__.": Found block.\n" );
1001 $this->mBlockedby = $this->mBlock->mBy;
1002 $this->mBlockreason = $this->mBlock->mReason;
1003 $this->mHideName = $this->mBlock->mHideName;
1004 if ( $this->isLoggedIn() ) {
1005 $this->spreadBlock();
1007 } else {
1008 $this->mBlock = null;
1009 wfDebug( __METHOD__.": No block.\n" );
1012 # Proxy blocking
1013 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
1014 # Local list
1015 if ( wfIsLocallyBlockedProxy( $ip ) ) {
1016 $this->mBlockedby = wfMsg( 'proxyblocker' );
1017 $this->mBlockreason = wfMsg( 'proxyblockreason' );
1020 # DNSBL
1021 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
1022 if ( $this->inSorbsBlacklist( $ip ) ) {
1023 $this->mBlockedby = wfMsg( 'sorbs' );
1024 $this->mBlockreason = wfMsg( 'sorbsreason' );
1029 # Extensions
1030 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1032 wfProfileOut( __METHOD__ );
1035 function inSorbsBlacklist( $ip ) {
1036 global $wgEnableSorbs, $wgSorbsUrl;
1038 return $wgEnableSorbs &&
1039 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
1042 function inDnsBlacklist( $ip, $base ) {
1043 wfProfileIn( __METHOD__ );
1045 $found = false;
1046 $host = '';
1047 // FIXME: IPv6 ???
1048 $m = array();
1049 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
1050 # Make hostname
1051 for ( $i=4; $i>=1; $i-- ) {
1052 $host .= $m[$i] . '.';
1054 $host .= $base;
1056 # Send query
1057 $ipList = gethostbynamel( $host );
1059 if ( $ipList ) {
1060 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1061 $found = true;
1062 } else {
1063 wfDebug( "Requested $host, not found in $base.\n" );
1067 wfProfileOut( __METHOD__ );
1068 return $found;
1072 * Is this user subject to rate limiting?
1074 * @return bool
1076 public function isPingLimitable() {
1077 global $wgRateLimitsExcludedGroups;
1078 if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1079 // Deprecated, but kept for backwards-compatibility config
1080 return false;
1082 return !$this->isAllowed('noratelimit');
1086 * Primitive rate limits: enforce maximum actions per time period
1087 * to put a brake on flooding.
1089 * Note: when using a shared cache like memcached, IP-address
1090 * last-hit counters will be shared across wikis.
1092 * @return bool true if a rate limiter was tripped
1094 function pingLimiter( $action='edit' ) {
1096 # Call the 'PingLimiter' hook
1097 $result = false;
1098 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1099 return $result;
1102 global $wgRateLimits;
1103 if( !isset( $wgRateLimits[$action] ) ) {
1104 return false;
1107 # Some groups shouldn't trigger the ping limiter, ever
1108 if( !$this->isPingLimitable() )
1109 return false;
1111 global $wgMemc, $wgRateLimitLog;
1112 wfProfileIn( __METHOD__ );
1114 $limits = $wgRateLimits[$action];
1115 $keys = array();
1116 $id = $this->getId();
1117 $ip = wfGetIP();
1118 $userLimit = false;
1120 if( isset( $limits['anon'] ) && $id == 0 ) {
1121 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1124 if( isset( $limits['user'] ) && $id != 0 ) {
1125 $userLimit = $limits['user'];
1127 if( $this->isNewbie() ) {
1128 if( isset( $limits['newbie'] ) && $id != 0 ) {
1129 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1131 if( isset( $limits['ip'] ) ) {
1132 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1134 $matches = array();
1135 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1136 $subnet = $matches[1];
1137 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1140 // Check for group-specific permissions
1141 // If more than one group applies, use the group with the highest limit
1142 foreach ( $this->getGroups() as $group ) {
1143 if ( isset( $limits[$group] ) ) {
1144 if ( $userLimit === false || $limits[$group] > $userLimit ) {
1145 $userLimit = $limits[$group];
1149 // Set the user limit key
1150 if ( $userLimit !== false ) {
1151 wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
1152 $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1155 $triggered = false;
1156 foreach( $keys as $key => $limit ) {
1157 list( $max, $period ) = $limit;
1158 $summary = "(limit $max in {$period}s)";
1159 $count = $wgMemc->get( $key );
1160 if( $count ) {
1161 if( $count > $max ) {
1162 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1163 if( $wgRateLimitLog ) {
1164 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1166 $triggered = true;
1167 } else {
1168 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1170 } else {
1171 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1172 $wgMemc->add( $key, 1, intval( $period ) );
1174 $wgMemc->incr( $key );
1177 wfProfileOut( __METHOD__ );
1178 return $triggered;
1182 * Check if user is blocked
1183 * @return bool True if blocked, false otherwise
1185 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1186 wfDebug( "User::isBlocked: enter\n" );
1187 $this->getBlockedStatus( $bFromSlave );
1188 return $this->mBlockedby !== 0;
1192 * Check if user is blocked from editing a particular article
1194 function isBlockedFrom( $title, $bFromSlave = false ) {
1195 global $wgBlockAllowsUTEdit;
1196 wfProfileIn( __METHOD__ );
1197 wfDebug( __METHOD__.": enter\n" );
1199 wfDebug( __METHOD__.": asking isBlocked()\n" );
1200 $blocked = $this->isBlocked( $bFromSlave );
1201 # If a user's name is suppressed, they cannot make edits anywhere
1202 if ( !$this->mHideName && $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
1203 $title->getNamespace() == NS_USER_TALK ) {
1204 $blocked = false;
1205 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1207 wfProfileOut( __METHOD__ );
1208 return $blocked;
1212 * Get name of blocker
1213 * @return string name of blocker
1215 function blockedBy() {
1216 $this->getBlockedStatus();
1217 return $this->mBlockedby;
1221 * Get blocking reason
1222 * @return string Blocking reason
1224 function blockedFor() {
1225 $this->getBlockedStatus();
1226 return $this->mBlockreason;
1230 * Get the user ID. Returns 0 if the user is anonymous or nonexistent.
1232 function getId() {
1233 if( $this->mId === null and $this->mName !== null
1234 and User::isIP( $this->mName ) ) {
1235 // Special case, we know the user is anonymous
1236 return 0;
1237 } elseif( $this->mId === null ) {
1238 // Don't load if this was initialized from an ID
1239 $this->load();
1241 return $this->mId;
1245 * Set the user and reload all fields according to that ID
1247 function setId( $v ) {
1248 $this->mId = $v;
1249 $this->clearInstanceCache( 'id' );
1253 * Get the user name, or the IP for anons
1255 function getName() {
1256 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1257 # Special case optimisation
1258 return $this->mName;
1259 } else {
1260 $this->load();
1261 if ( $this->mName === false ) {
1262 # Clean up IPs
1263 $this->mName = IP::sanitizeIP( wfGetIP() );
1265 return $this->mName;
1270 * Set the user name.
1272 * This does not reload fields from the database according to the given
1273 * name. Rather, it is used to create a temporary "nonexistent user" for
1274 * later addition to the database. It can also be used to set the IP
1275 * address for an anonymous user to something other than the current
1276 * remote IP.
1278 * User::newFromName() has rougly the same function, when the named user
1279 * does not exist.
1281 function setName( $str ) {
1282 $this->load();
1283 $this->mName = $str;
1287 * Return the title dbkey form of the name, for eg user pages.
1288 * @return string
1290 function getTitleKey() {
1291 return str_replace( ' ', '_', $this->getName() );
1294 function getNewtalk() {
1295 $this->load();
1297 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1298 if( $this->mNewtalk === -1 ) {
1299 $this->mNewtalk = false; # reset talk page status
1301 # Check memcached separately for anons, who have no
1302 # entire User object stored in there.
1303 if( !$this->mId ) {
1304 global $wgMemc;
1305 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1306 $newtalk = $wgMemc->get( $key );
1307 if( strval( $newtalk ) !== '' ) {
1308 $this->mNewtalk = (bool)$newtalk;
1309 } else {
1310 // Since we are caching this, make sure it is up to date by getting it
1311 // from the master
1312 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1313 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1315 } else {
1316 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1320 return (bool)$this->mNewtalk;
1324 * Return the talk page(s) this user has new messages on.
1326 function getNewMessageLinks() {
1327 $talks = array();
1328 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1329 return $talks;
1331 if (!$this->getNewtalk())
1332 return array();
1333 $up = $this->getUserPage();
1334 $utp = $up->getTalkPage();
1335 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1340 * Perform a user_newtalk check, uncached.
1341 * Use getNewtalk for a cached check.
1343 * @param $field string
1344 * @param $id mixed
1345 * @param $fromMaster Bool: true to fetch from the master, false for a slave
1346 * @return bool
1347 * @private
1349 function checkNewtalk( $field, $id, $fromMaster = false ) {
1350 if ( $fromMaster ) {
1351 $db = wfGetDB( DB_MASTER );
1352 } else {
1353 $db = wfGetDB( DB_SLAVE );
1355 $ok = $db->selectField( 'user_newtalk', $field,
1356 array( $field => $id ), __METHOD__ );
1357 return $ok !== false;
1361 * Add or update the
1362 * @param $field string
1363 * @param $id mixed
1364 * @private
1366 function updateNewtalk( $field, $id ) {
1367 $dbw = wfGetDB( DB_MASTER );
1368 $dbw->insert( 'user_newtalk',
1369 array( $field => $id ),
1370 __METHOD__,
1371 'IGNORE' );
1372 if ( $dbw->affectedRows() ) {
1373 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1374 return true;
1375 } else {
1376 wfDebug( __METHOD__." already set ($field, $id)\n" );
1377 return false;
1382 * Clear the new messages flag for the given user
1383 * @param $field string
1384 * @param $id mixed
1385 * @private
1387 function deleteNewtalk( $field, $id ) {
1388 $dbw = wfGetDB( DB_MASTER );
1389 $dbw->delete( 'user_newtalk',
1390 array( $field => $id ),
1391 __METHOD__ );
1392 if ( $dbw->affectedRows() ) {
1393 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1394 return true;
1395 } else {
1396 wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1397 return false;
1402 * Update the 'You have new messages!' status.
1403 * @param $val bool
1405 function setNewtalk( $val ) {
1406 if( wfReadOnly() ) {
1407 return;
1410 $this->load();
1411 $this->mNewtalk = $val;
1413 if( $this->isAnon() ) {
1414 $field = 'user_ip';
1415 $id = $this->getName();
1416 } else {
1417 $field = 'user_id';
1418 $id = $this->getId();
1420 global $wgMemc;
1422 if( $val ) {
1423 $changed = $this->updateNewtalk( $field, $id );
1424 } else {
1425 $changed = $this->deleteNewtalk( $field, $id );
1428 if( $this->isAnon() ) {
1429 // Anons have a separate memcached space, since
1430 // user records aren't kept for them.
1431 $key = wfMemcKey( 'newtalk', 'ip', $id );
1432 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1434 if ( $changed ) {
1435 $this->invalidateCache();
1440 * Generate a current or new-future timestamp to be stored in the
1441 * user_touched field when we update things.
1443 private static function newTouchedTimestamp() {
1444 global $wgClockSkewFudge;
1445 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1449 * Clear user data from memcached.
1450 * Use after applying fun updates to the database; caller's
1451 * responsibility to update user_touched if appropriate.
1453 * Called implicitly from invalidateCache() and saveSettings().
1455 private function clearSharedCache() {
1456 if( $this->mId ) {
1457 global $wgMemc;
1458 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1463 * Immediately touch the user data cache for this account.
1464 * Updates user_touched field, and removes account data from memcached
1465 * for reload on the next hit.
1467 function invalidateCache() {
1468 $this->load();
1469 if( $this->mId ) {
1470 $this->mTouched = self::newTouchedTimestamp();
1472 $dbw = wfGetDB( DB_MASTER );
1473 $dbw->update( 'user',
1474 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1475 array( 'user_id' => $this->mId ),
1476 __METHOD__ );
1478 $this->clearSharedCache();
1482 function validateCache( $timestamp ) {
1483 $this->load();
1484 return ($timestamp >= $this->mTouched);
1488 * Set the password and reset the random token
1489 * Calls through to authentication plugin if necessary;
1490 * will have no effect if the auth plugin refuses to
1491 * pass the change through or if the legal password
1492 * checks fail.
1494 * As a special case, setting the password to null
1495 * wipes it, so the account cannot be logged in until
1496 * a new password is set, for instance via e-mail.
1498 * @param $str string
1499 * @throws PasswordError on failure
1501 function setPassword( $str ) {
1502 global $wgAuth;
1504 if( $str !== null ) {
1505 if( !$wgAuth->allowPasswordChange() ) {
1506 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1509 if( !$this->isValidPassword( $str ) ) {
1510 global $wgMinimalPasswordLength;
1511 throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
1512 $wgMinimalPasswordLength ) );
1516 if( !$wgAuth->setPassword( $this, $str ) ) {
1517 throw new PasswordError( wfMsg( 'externaldberror' ) );
1520 $this->setInternalPassword( $str );
1522 return true;
1526 * Set the password and reset the random token no matter
1527 * what.
1529 * @param $str string
1531 function setInternalPassword( $str ) {
1532 $this->load();
1533 $this->setToken();
1535 if( $str === null ) {
1536 // Save an invalid hash...
1537 $this->mPassword = '';
1538 } else {
1539 $this->mPassword = self::crypt( $str );
1541 $this->mNewpassword = '';
1542 $this->mNewpassTime = null;
1545 function getToken() {
1546 $this->load();
1547 return $this->mToken;
1551 * Set the random token (used for persistent authentication)
1552 * Called from loadDefaults() among other places.
1553 * @private
1555 function setToken( $token = false ) {
1556 global $wgSecretKey, $wgProxyKey;
1557 $this->load();
1558 if ( !$token ) {
1559 if ( $wgSecretKey ) {
1560 $key = $wgSecretKey;
1561 } elseif ( $wgProxyKey ) {
1562 $key = $wgProxyKey;
1563 } else {
1564 $key = microtime();
1566 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1567 } else {
1568 $this->mToken = $token;
1572 function setCookiePassword( $str ) {
1573 $this->load();
1574 $this->mCookiePassword = md5( $str );
1578 * Set the password for a password reminder or new account email
1579 * Sets the user_newpass_time field if $throttle is true
1581 function setNewpassword( $str, $throttle = true ) {
1582 $this->load();
1583 $this->mNewpassword = self::crypt( $str );
1584 if ( $throttle ) {
1585 $this->mNewpassTime = wfTimestampNow();
1590 * Returns true if a password reminder email has already been sent within
1591 * the last $wgPasswordReminderResendTime hours
1593 function isPasswordReminderThrottled() {
1594 global $wgPasswordReminderResendTime;
1595 $this->load();
1596 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1597 return false;
1599 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1600 return time() < $expiry;
1603 function getEmail() {
1604 $this->load();
1605 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1606 return $this->mEmail;
1609 function getEmailAuthenticationTimestamp() {
1610 $this->load();
1611 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1612 return $this->mEmailAuthenticated;
1615 function setEmail( $str ) {
1616 $this->load();
1617 $this->mEmail = $str;
1618 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1621 function getRealName() {
1622 $this->load();
1623 return $this->mRealName;
1626 function setRealName( $str ) {
1627 $this->load();
1628 $this->mRealName = $str;
1632 * @param $oname String: the option to check
1633 * @param $defaultOverride String: A default value returned if the option does not exist
1634 * @return string
1636 function getOption( $oname, $defaultOverride = '' ) {
1637 $this->load();
1639 if ( is_null( $this->mOptions ) ) {
1640 if($defaultOverride != '') {
1641 return $defaultOverride;
1643 $this->mOptions = User::getDefaultOptions();
1646 if ( array_key_exists( $oname, $this->mOptions ) ) {
1647 return trim( $this->mOptions[$oname] );
1648 } else {
1649 return $defaultOverride;
1654 * Get the user's date preference, including some important migration for
1655 * old user rows.
1657 function getDatePreference() {
1658 if ( is_null( $this->mDatePreference ) ) {
1659 global $wgLang;
1660 $value = $this->getOption( 'date' );
1661 $map = $wgLang->getDatePreferenceMigrationMap();
1662 if ( isset( $map[$value] ) ) {
1663 $value = $map[$value];
1665 $this->mDatePreference = $value;
1667 return $this->mDatePreference;
1671 * @param $oname String: the option to check
1672 * @return bool False if the option is not selected, true if it is
1674 function getBoolOption( $oname ) {
1675 return (bool)$this->getOption( $oname );
1679 * Get an option as an integer value from the source string.
1680 * @param $oname String: the option to check
1681 * @param $default Int: optional value to return if option is unset/blank.
1682 * @return int
1684 function getIntOption( $oname, $default=0 ) {
1685 $val = $this->getOption( $oname );
1686 if( $val == '' ) {
1687 $val = $default;
1689 return intval( $val );
1692 function setOption( $oname, $val ) {
1693 $this->load();
1694 if ( is_null( $this->mOptions ) ) {
1695 $this->mOptions = User::getDefaultOptions();
1697 if ( $oname == 'skin' ) {
1698 # Clear cached skin, so the new one displays immediately in Special:Preferences
1699 unset( $this->mSkin );
1701 // Filter out any newlines that may have passed through input validation.
1702 // Newlines are used to separate items in the options blob.
1703 if( $val ) {
1704 $val = str_replace( "\r\n", "\n", $val );
1705 $val = str_replace( "\r", "\n", $val );
1706 $val = str_replace( "\n", " ", $val );
1708 // Explicitly NULL values should refer to defaults
1709 global $wgDefaultUserOptions;
1710 if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
1711 $val = $wgDefaultUserOptions[$oname];
1713 $this->mOptions[$oname] = $val;
1716 function getRights() {
1717 if ( is_null( $this->mRights ) ) {
1718 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1719 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
1720 // Force reindexation of rights when a hook has unset one of them
1721 $this->mRights = array_values( $this->mRights );
1723 return $this->mRights;
1727 * Get the list of explicit group memberships this user has.
1728 * The implicit * and user groups are not included.
1729 * @return array of strings
1731 function getGroups() {
1732 $this->load();
1733 return $this->mGroups;
1737 * Get the list of implicit group memberships this user has.
1738 * This includes all explicit groups, plus 'user' if logged in,
1739 * '*' for all accounts and autopromoted groups
1740 * @param $recache Boolean: don't use the cache
1741 * @return array of strings
1743 function getEffectiveGroups( $recache = false ) {
1744 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
1745 $this->mEffectiveGroups = $this->getGroups();
1746 $this->mEffectiveGroups[] = '*';
1747 if( $this->getId() ) {
1748 $this->mEffectiveGroups[] = 'user';
1750 $this->mEffectiveGroups = array_unique( array_merge(
1751 $this->mEffectiveGroups,
1752 Autopromote::getAutopromoteGroups( $this )
1753 ) );
1755 # Hook for additional groups
1756 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
1759 return $this->mEffectiveGroups;
1762 /* Return the edit count for the user. This is where User::edits should have been */
1763 function getEditCount() {
1764 if ($this->mId) {
1765 if ( !isset( $this->mEditCount ) ) {
1766 /* Populate the count, if it has not been populated yet */
1767 $this->mEditCount = User::edits($this->mId);
1769 return $this->mEditCount;
1770 } else {
1771 /* nil */
1772 return null;
1777 * Add the user to the given group.
1778 * This takes immediate effect.
1779 * @param $group string
1781 function addGroup( $group ) {
1782 $dbw = wfGetDB( DB_MASTER );
1783 if( $this->getId() ) {
1784 $dbw->insert( 'user_groups',
1785 array(
1786 'ug_user' => $this->getID(),
1787 'ug_group' => $group,
1789 'User::addGroup',
1790 array( 'IGNORE' ) );
1793 $this->loadGroups();
1794 $this->mGroups[] = $group;
1795 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1797 $this->invalidateCache();
1801 * Remove the user from the given group.
1802 * This takes immediate effect.
1803 * @param $group string
1805 function removeGroup( $group ) {
1806 $this->load();
1807 $dbw = wfGetDB( DB_MASTER );
1808 $dbw->delete( 'user_groups',
1809 array(
1810 'ug_user' => $this->getID(),
1811 'ug_group' => $group,
1813 'User::removeGroup' );
1815 $this->loadGroups();
1816 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1817 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1819 $this->invalidateCache();
1824 * A more legible check for non-anonymousness.
1825 * Returns true if the user is not an anonymous visitor.
1827 * @return bool
1829 function isLoggedIn() {
1830 return $this->getID() != 0;
1834 * A more legible check for anonymousness.
1835 * Returns true if the user is an anonymous visitor.
1837 * @return bool
1839 function isAnon() {
1840 return !$this->isLoggedIn();
1844 * Whether the user is a bot
1845 * @deprecated
1847 function isBot() {
1848 wfDeprecated( __METHOD__ );
1849 return $this->isAllowed( 'bot' );
1853 * Check if user is allowed to access a feature / make an action
1854 * @param $action String: action to be checked
1855 * @return boolean True: action is allowed, False: action should not be allowed
1857 function isAllowed($action='') {
1858 if ( $action === '' )
1859 // In the spirit of DWIM
1860 return true;
1862 return in_array( $action, $this->getRights() );
1866 * Check whether to enable recent changes patrol features for this user
1867 * @return bool
1869 public function useRCPatrol() {
1870 global $wgUseRCPatrol;
1871 return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
1875 * Check whether to enable recent changes patrol features for this user
1876 * @return bool
1878 public function useNPPatrol() {
1879 global $wgUseRCPatrol, $wgUseNPPatrol;
1880 return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
1884 * Load a skin if it doesn't exist or return it
1885 * @todo FIXME : need to check the old failback system [AV]
1887 function &getSkin() {
1888 global $wgRequest;
1889 if ( ! isset( $this->mSkin ) ) {
1890 wfProfileIn( __METHOD__ );
1892 # get the user skin
1893 $userSkin = $this->getOption( 'skin' );
1894 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1896 $this->mSkin =& Skin::newFromKey( $userSkin );
1897 wfProfileOut( __METHOD__ );
1899 return $this->mSkin;
1902 /**#@+
1903 * @param $title Title: article title to look at
1907 * Check watched status of an article
1908 * @return bool True if article is watched
1910 function isWatched( $title ) {
1911 $wl = WatchedItem::fromUserTitle( $this, $title );
1912 return $wl->isWatched();
1916 * Watch an article
1918 function addWatch( $title ) {
1919 $wl = WatchedItem::fromUserTitle( $this, $title );
1920 $wl->addWatch();
1921 $this->invalidateCache();
1925 * Stop watching an article
1927 function removeWatch( $title ) {
1928 $wl = WatchedItem::fromUserTitle( $this, $title );
1929 $wl->removeWatch();
1930 $this->invalidateCache();
1934 * Clear the user's notification timestamp for the given title.
1935 * If e-notif e-mails are on, they will receive notification mails on
1936 * the next change of the page if it's watched etc.
1938 function clearNotification( &$title ) {
1939 global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
1941 # Do nothing if the database is locked to writes
1942 if( wfReadOnly() ) {
1943 return;
1946 if ($title->getNamespace() == NS_USER_TALK &&
1947 $title->getText() == $this->getName() ) {
1948 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1949 return;
1950 $this->setNewtalk( false );
1953 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
1954 return;
1957 if( $this->isAnon() ) {
1958 // Nothing else to do...
1959 return;
1962 // Only update the timestamp if the page is being watched.
1963 // The query to find out if it is watched is cached both in memcached and per-invocation,
1964 // and when it does have to be executed, it can be on a slave
1965 // If this is the user's newtalk page, we always update the timestamp
1966 if ($title->getNamespace() == NS_USER_TALK &&
1967 $title->getText() == $wgUser->getName())
1969 $watched = true;
1970 } elseif ( $this->getId() == $wgUser->getId() ) {
1971 $watched = $title->userIsWatching();
1972 } else {
1973 $watched = true;
1976 // If the page is watched by the user (or may be watched), update the timestamp on any
1977 // any matching rows
1978 if ( $watched ) {
1979 $dbw = wfGetDB( DB_MASTER );
1980 $dbw->update( 'watchlist',
1981 array( /* SET */
1982 'wl_notificationtimestamp' => NULL
1983 ), array( /* WHERE */
1984 'wl_title' => $title->getDBkey(),
1985 'wl_namespace' => $title->getNamespace(),
1986 'wl_user' => $this->getID()
1987 ), __METHOD__
1992 /**#@-*/
1995 * Resets all of the given user's page-change notification timestamps.
1996 * If e-notif e-mails are on, they will receive notification mails on
1997 * the next change of any watched page.
1999 * @param $currentUser Int: user ID number
2001 function clearAllNotifications( $currentUser ) {
2002 global $wgUseEnotif, $wgShowUpdatedMarker;
2003 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2004 $this->setNewtalk( false );
2005 return;
2007 if( $currentUser != 0 ) {
2008 $dbw = wfGetDB( DB_MASTER );
2009 $dbw->update( 'watchlist',
2010 array( /* SET */
2011 'wl_notificationtimestamp' => NULL
2012 ), array( /* WHERE */
2013 'wl_user' => $currentUser
2014 ), __METHOD__
2016 # We also need to clear here the "you have new message" notification for the own user_talk page
2017 # This is cleared one page view later in Article::viewUpdates();
2022 * @private
2023 * @return string Encoding options
2025 function encodeOptions() {
2026 $this->load();
2027 if ( is_null( $this->mOptions ) ) {
2028 $this->mOptions = User::getDefaultOptions();
2030 $a = array();
2031 foreach ( $this->mOptions as $oname => $oval ) {
2032 array_push( $a, $oname.'='.$oval );
2034 $s = implode( "\n", $a );
2035 return $s;
2039 * @private
2041 function decodeOptions( $str ) {
2042 $this->mOptions = array();
2043 $a = explode( "\n", $str );
2044 foreach ( $a as $s ) {
2045 $m = array();
2046 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2047 $this->mOptions[$m[1]] = $m[2];
2052 protected function setCookie( $name, $value, $exp=0 ) {
2053 global $wgCookiePrefix,$wgCookieDomain,$wgCookieSecure,$wgCookieExpiration, $wgCookieHttpOnly;
2054 if( $exp == 0 ) {
2055 $exp = time() + $wgCookieExpiration;
2057 $httpOnlySafe = wfHttpOnlySafe();
2058 wfDebugLog( 'cookie',
2059 'setcookie: "' . implode( '", "',
2060 array(
2061 $wgCookiePrefix . $name,
2062 $value,
2063 $exp,
2064 '/',
2065 $wgCookieDomain,
2066 $wgCookieSecure,
2067 $httpOnlySafe && $wgCookieHttpOnly ) ) . '"' );
2068 if( $httpOnlySafe && isset( $wgCookieHttpOnly ) ) {
2069 setcookie( $wgCookiePrefix . $name,
2070 $value,
2071 $exp,
2072 '/',
2073 $wgCookieDomain,
2074 $wgCookieSecure,
2075 $wgCookieHttpOnly );
2076 } else {
2077 // setcookie() fails on PHP 5.1 if you give it future-compat paramters.
2078 // stab stab!
2079 setcookie( $wgCookiePrefix . $name,
2080 $value,
2081 $exp,
2082 '/',
2083 $wgCookieDomain,
2084 $wgCookieSecure );
2088 protected function clearCookie( $name ) {
2089 $this->setCookie( $name, '', time() - 86400 );
2092 function setCookies() {
2093 $this->load();
2094 if ( 0 == $this->mId ) return;
2095 $session = array(
2096 'wsUserID' => $this->mId,
2097 'wsToken' => $this->mToken,
2098 'wsUserName' => $this->getName()
2100 $cookies = array(
2101 'UserID' => $this->mId,
2102 'UserName' => $this->getName(),
2104 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2105 $cookies['Token'] = $this->mToken;
2106 } else {
2107 $cookies['Token'] = false;
2110 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2111 $_SESSION = $session + $_SESSION;
2112 foreach ( $cookies as $name => $value ) {
2113 if ( $value === false ) {
2114 $this->clearCookie( $name );
2115 } else {
2116 $this->setCookie( $name, $value );
2122 * Logout user.
2124 function logout() {
2125 global $wgUser;
2126 if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
2127 $this->doLogout();
2132 * Really logout user
2133 * Clears the cookies and session, resets the instance cache
2135 function doLogout() {
2136 $this->clearInstanceCache( 'defaults' );
2138 $_SESSION['wsUserID'] = 0;
2140 $this->clearCookie( 'UserID' );
2141 $this->clearCookie( 'Token' );
2143 # Remember when user logged out, to prevent seeing cached pages
2144 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2148 * Save object settings into database
2149 * @todo Only rarely do all these fields need to be set!
2151 function saveSettings() {
2152 $this->load();
2153 if ( wfReadOnly() ) { return; }
2154 if ( 0 == $this->mId ) { return; }
2156 $this->mTouched = self::newTouchedTimestamp();
2158 $dbw = wfGetDB( DB_MASTER );
2159 $dbw->update( 'user',
2160 array( /* SET */
2161 'user_name' => $this->mName,
2162 'user_password' => $this->mPassword,
2163 'user_newpassword' => $this->mNewpassword,
2164 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2165 'user_real_name' => $this->mRealName,
2166 'user_email' => $this->mEmail,
2167 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2168 'user_options' => $this->encodeOptions(),
2169 'user_touched' => $dbw->timestamp($this->mTouched),
2170 'user_token' => $this->mToken,
2171 'user_email_token' => $this->mEmailToken,
2172 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2173 ), array( /* WHERE */
2174 'user_id' => $this->mId
2175 ), __METHOD__
2177 wfRunHooks( 'UserSaveSettings', array( $this ) );
2178 $this->clearSharedCache();
2182 * Checks if a user with the given name exists, returns the ID.
2184 function idForName() {
2185 $s = trim( $this->getName() );
2186 if ( $s === '' ) return 0;
2188 $dbr = wfGetDB( DB_SLAVE );
2189 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2190 if ( $id === false ) {
2191 $id = 0;
2193 return $id;
2197 * Add a user to the database, return the user object
2199 * @param $name String: the user's name
2200 * @param $params Associative array of non-default parameters to save to the database:
2201 * password The user's password. Password logins will be disabled if this is omitted.
2202 * newpassword A temporary password mailed to the user
2203 * email The user's email address
2204 * email_authenticated The email authentication timestamp
2205 * real_name The user's real name
2206 * options An associative array of non-default options
2207 * token Random authentication token. Do not set.
2208 * registration Registration timestamp. Do not set.
2210 * @return User object, or null if the username already exists
2212 static function createNew( $name, $params = array() ) {
2213 $user = new User;
2214 $user->load();
2215 if ( isset( $params['options'] ) ) {
2216 $user->mOptions = $params['options'] + $user->mOptions;
2217 unset( $params['options'] );
2219 $dbw = wfGetDB( DB_MASTER );
2220 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2221 $fields = array(
2222 'user_id' => $seqVal,
2223 'user_name' => $name,
2224 'user_password' => $user->mPassword,
2225 'user_newpassword' => $user->mNewpassword,
2226 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2227 'user_email' => $user->mEmail,
2228 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2229 'user_real_name' => $user->mRealName,
2230 'user_options' => $user->encodeOptions(),
2231 'user_token' => $user->mToken,
2232 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2233 'user_editcount' => 0,
2235 foreach ( $params as $name => $value ) {
2236 $fields["user_$name"] = $value;
2238 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2239 if ( $dbw->affectedRows() ) {
2240 $newUser = User::newFromId( $dbw->insertId() );
2241 } else {
2242 $newUser = null;
2244 return $newUser;
2248 * Add an existing user object to the database
2250 function addToDatabase() {
2251 $this->load();
2252 $dbw = wfGetDB( DB_MASTER );
2253 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2254 $dbw->insert( 'user',
2255 array(
2256 'user_id' => $seqVal,
2257 'user_name' => $this->mName,
2258 'user_password' => $this->mPassword,
2259 'user_newpassword' => $this->mNewpassword,
2260 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2261 'user_email' => $this->mEmail,
2262 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2263 'user_real_name' => $this->mRealName,
2264 'user_options' => $this->encodeOptions(),
2265 'user_token' => $this->mToken,
2266 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2267 'user_editcount' => 0,
2268 ), __METHOD__
2270 $this->mId = $dbw->insertId();
2272 # Clear instance cache other than user table data, which is already accurate
2273 $this->clearInstanceCache();
2277 * If the (non-anonymous) user is blocked, this function will block any IP address
2278 * that they successfully log on from.
2280 function spreadBlock() {
2281 wfDebug( __METHOD__."()\n" );
2282 $this->load();
2283 if ( $this->mId == 0 ) {
2284 return;
2287 $userblock = Block::newFromDB( '', $this->mId );
2288 if ( !$userblock ) {
2289 return;
2292 $userblock->doAutoblock( wfGetIp() );
2297 * Generate a string which will be different for any combination of
2298 * user options which would produce different parser output.
2299 * This will be used as part of the hash key for the parser cache,
2300 * so users will the same options can share the same cached data
2301 * safely.
2303 * Extensions which require it should install 'PageRenderingHash' hook,
2304 * which will give them a chance to modify this key based on their own
2305 * settings.
2307 * @return string
2309 function getPageRenderingHash() {
2310 global $wgContLang, $wgUseDynamicDates, $wgLang;
2311 if( $this->mHash ){
2312 return $this->mHash;
2315 // stubthreshold is only included below for completeness,
2316 // it will always be 0 when this function is called by parsercache.
2318 $confstr = $this->getOption( 'math' );
2319 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2320 if ( $wgUseDynamicDates ) {
2321 $confstr .= '!' . $this->getDatePreference();
2323 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2324 $confstr .= '!' . $wgLang->getCode();
2325 $confstr .= '!' . $this->getOption( 'thumbsize' );
2326 // add in language specific options, if any
2327 $extra = $wgContLang->getExtraHashOptions();
2328 $confstr .= $extra;
2330 // Give a chance for extensions to modify the hash, if they have
2331 // extra options or other effects on the parser cache.
2332 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2334 // Make it a valid memcached key fragment
2335 $confstr = str_replace( ' ', '_', $confstr );
2336 $this->mHash = $confstr;
2337 return $confstr;
2340 function isBlockedFromCreateAccount() {
2341 $this->getBlockedStatus();
2342 return $this->mBlock && $this->mBlock->mCreateAccount;
2346 * Determine if the user is blocked from using Special:Emailuser.
2348 * @return boolean
2350 function isBlockedFromEmailuser() {
2351 $this->getBlockedStatus();
2352 return $this->mBlock && $this->mBlock->mBlockEmail;
2355 function isAllowedToCreateAccount() {
2356 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2360 * @deprecated
2362 function setLoaded( $loaded ) {
2363 wfDeprecated( __METHOD__ );
2367 * Get this user's personal page title.
2369 * @return Title
2371 function getUserPage() {
2372 return Title::makeTitle( NS_USER, $this->getName() );
2376 * Get this user's talk page title.
2378 * @return Title
2380 function getTalkPage() {
2381 $title = $this->getUserPage();
2382 return $title->getTalkPage();
2386 * @static
2388 function getMaxID() {
2389 static $res; // cache
2391 if ( isset( $res ) )
2392 return $res;
2393 else {
2394 $dbr = wfGetDB( DB_SLAVE );
2395 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2400 * Determine whether the user is a newbie. Newbies are either
2401 * anonymous IPs, or the most recently created accounts.
2402 * @return bool True if it is a newbie.
2404 function isNewbie() {
2405 return !$this->isAllowed( 'autoconfirmed' );
2409 * Is the user active? We check to see if they've made at least
2410 * X number of edits in the last Y days.
2412 * @return bool true if the user is active, false if not
2414 public function isActiveEditor() {
2415 global $wgActiveUserEditCount, $wgActiveUserDays;
2416 $dbr = wfGetDB( DB_SLAVE );
2418 // Stolen without shame from RC
2419 $cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
2420 $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
2421 $oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
2423 $res = $dbr->select( 'revision', '1',
2424 array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
2425 __METHOD__,
2426 array('LIMIT' => $wgActiveUserEditCount ) );
2428 $count = $dbr->numRows($res);
2429 $dbr->freeResult($res);
2431 return $count == $wgActiveUserEditCount;
2435 * Check to see if the given clear-text password is one of the accepted passwords
2436 * @param $password String: user password.
2437 * @return bool True if the given password is correct otherwise False.
2439 function checkPassword( $password ) {
2440 global $wgAuth;
2441 $this->load();
2443 // Even though we stop people from creating passwords that
2444 // are shorter than this, doesn't mean people wont be able
2445 // to. Certain authentication plugins do NOT want to save
2446 // domain passwords in a mysql database, so we should
2447 // check this (incase $wgAuth->strict() is false).
2448 if( !$this->isValidPassword( $password ) ) {
2449 return false;
2452 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2453 return true;
2454 } elseif( $wgAuth->strict() ) {
2455 /* Auth plugin doesn't allow local authentication */
2456 return false;
2457 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2458 /* Auth plugin doesn't allow local authentication for this user name */
2459 return false;
2461 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2462 return true;
2463 } elseif ( function_exists( 'iconv' ) ) {
2464 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2465 # Check for this with iconv
2466 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2467 if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2468 return true;
2471 return false;
2475 * Check if the given clear-text password matches the temporary password
2476 * sent by e-mail for password reset operations.
2477 * @return bool
2479 function checkTemporaryPassword( $plaintext ) {
2480 return self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() );
2484 * Initialize (if necessary) and return a session token value
2485 * which can be used in edit forms to show that the user's
2486 * login credentials aren't being hijacked with a foreign form
2487 * submission.
2489 * @param $salt Mixed: optional function-specific data for hash.
2490 * Use a string or an array of strings.
2491 * @return string
2493 function editToken( $salt = '' ) {
2494 if ( $this->isAnon() ) {
2495 return EDIT_TOKEN_SUFFIX;
2496 } else {
2497 if( !isset( $_SESSION['wsEditToken'] ) ) {
2498 $token = $this->generateToken();
2499 $_SESSION['wsEditToken'] = $token;
2500 } else {
2501 $token = $_SESSION['wsEditToken'];
2503 if( is_array( $salt ) ) {
2504 $salt = implode( '|', $salt );
2506 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2511 * Generate a hex-y looking random token for various uses.
2512 * Could be made more cryptographically sure if someone cares.
2513 * @return string
2515 function generateToken( $salt = '' ) {
2516 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2517 return md5( $token . $salt );
2521 * Check given value against the token value stored in the session.
2522 * A match should confirm that the form was submitted from the
2523 * user's own login session, not a form submission from a third-party
2524 * site.
2526 * @param $val String: the input value to compare
2527 * @param $salt String: optional function-specific data for hash
2528 * @return bool
2530 function matchEditToken( $val, $salt = '' ) {
2531 $sessionToken = $this->editToken( $salt );
2532 if ( $val != $sessionToken ) {
2533 wfDebug( "User::matchEditToken: broken session data\n" );
2535 return $val == $sessionToken;
2539 * Check whether the edit token is fine except for the suffix
2541 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2542 $sessionToken = $this->editToken( $salt );
2543 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2547 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2548 * mail to the user's given address.
2550 * Calls saveSettings() internally; as it has side effects, not committing changes
2551 * would be pretty silly.
2553 * @return mixed True on success, a WikiError object on failure.
2555 function sendConfirmationMail() {
2556 global $wgLang;
2557 $expiration = null; // gets passed-by-ref and defined in next line.
2558 $token = $this->confirmationToken( $expiration );
2559 $url = $this->confirmationTokenUrl( $token );
2560 $invalidateURL = $this->invalidationTokenUrl( $token );
2561 $this->saveSettings();
2563 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2564 wfMsg( 'confirmemail_body',
2565 wfGetIP(),
2566 $this->getName(),
2567 $url,
2568 $wgLang->timeanddate( $expiration, false ),
2569 $invalidateURL ) );
2573 * Send an e-mail to this user's account. Does not check for
2574 * confirmed status or validity.
2576 * @param $subject string
2577 * @param $body string
2578 * @param $from string: optional from address; default $wgPasswordSender will be used otherwise.
2579 * @param $replyto string
2580 * @return mixed True on success, a WikiError object on failure.
2582 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2583 if( is_null( $from ) ) {
2584 global $wgPasswordSender;
2585 $from = $wgPasswordSender;
2588 $to = new MailAddress( $this );
2589 $sender = new MailAddress( $from );
2590 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2594 * Generate, store, and return a new e-mail confirmation code.
2595 * A hash (unsalted since it's used as a key) is stored.
2597 * Call saveSettings() after calling this function to commit
2598 * this change to the database.
2600 * @param &$expiration mixed output: accepts the expiration time
2601 * @return string
2602 * @private
2604 function confirmationToken( &$expiration ) {
2605 $now = time();
2606 $expires = $now + 7 * 24 * 60 * 60;
2607 $expiration = wfTimestamp( TS_MW, $expires );
2608 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2609 $hash = md5( $token );
2610 $this->load();
2611 $this->mEmailToken = $hash;
2612 $this->mEmailTokenExpires = $expiration;
2613 return $token;
2617 * Return a URL the user can use to confirm their email address.
2618 * @param $token accepts the email confirmation token
2619 * @return string
2620 * @private
2622 function confirmationTokenUrl( $token ) {
2623 return $this->getTokenUrl( 'ConfirmEmail', $token );
2626 * Return a URL the user can use to invalidate their email address.
2627 * @param $token accepts the email confirmation token
2628 * @return string
2629 * @private
2631 function invalidationTokenUrl( $token ) {
2632 return $this->getTokenUrl( 'Invalidateemail', $token );
2636 * Internal function to format the e-mail validation/invalidation URLs.
2637 * This uses $wgArticlePath directly as a quickie hack to use the
2638 * hardcoded English names of the Special: pages, for ASCII safety.
2640 * Since these URLs get dropped directly into emails, using the
2641 * short English names avoids insanely long URL-encoded links, which
2642 * also sometimes can get corrupted in some browsers/mailers
2643 * (bug 6957 with Gmail and Internet Explorer).
2645 protected function getTokenUrl( $page, $token ) {
2646 global $wgArticlePath;
2647 return wfExpandUrl(
2648 str_replace(
2649 '$1',
2650 "Special:$page/$token",
2651 $wgArticlePath ) );
2655 * Mark the e-mail address confirmed.
2657 * Call saveSettings() after calling this function to commit the change.
2659 function confirmEmail() {
2660 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2661 return true;
2665 * Invalidate the user's email confirmation, unauthenticate the email
2666 * if it was already confirmed.
2668 * Call saveSettings() after calling this function to commit the change.
2670 function invalidateEmail() {
2671 $this->load();
2672 $this->mEmailToken = null;
2673 $this->mEmailTokenExpires = null;
2674 $this->setEmailAuthenticationTimestamp( null );
2675 return true;
2678 function setEmailAuthenticationTimestamp( $timestamp ) {
2679 $this->load();
2680 $this->mEmailAuthenticated = $timestamp;
2681 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2685 * Is this user allowed to send e-mails within limits of current
2686 * site configuration?
2687 * @return bool
2689 function canSendEmail() {
2690 $canSend = $this->isEmailConfirmed();
2691 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2692 return $canSend;
2696 * Is this user allowed to receive e-mails within limits of current
2697 * site configuration?
2698 * @return bool
2700 function canReceiveEmail() {
2701 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
2705 * Is this user's e-mail address valid-looking and confirmed within
2706 * limits of the current site configuration?
2708 * If $wgEmailAuthentication is on, this may require the user to have
2709 * confirmed their address by returning a code or using a password
2710 * sent to the address from the wiki.
2712 * @return bool
2714 function isEmailConfirmed() {
2715 global $wgEmailAuthentication;
2716 $this->load();
2717 $confirmed = true;
2718 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2719 if( $this->isAnon() )
2720 return false;
2721 if( !self::isValidEmailAddr( $this->mEmail ) )
2722 return false;
2723 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2724 return false;
2725 return true;
2726 } else {
2727 return $confirmed;
2732 * Return true if there is an outstanding request for e-mail confirmation.
2733 * @return bool
2735 function isEmailConfirmationPending() {
2736 global $wgEmailAuthentication;
2737 return $wgEmailAuthentication &&
2738 !$this->isEmailConfirmed() &&
2739 $this->mEmailToken &&
2740 $this->mEmailTokenExpires > wfTimestamp();
2744 * Get the timestamp of account creation, or false for
2745 * non-existent/anonymous user accounts
2747 * @return mixed
2749 public function getRegistration() {
2750 return $this->mId > 0
2751 ? $this->mRegistration
2752 : false;
2756 * @param $groups Array: list of groups
2757 * @return array list of permission key names for given groups combined
2759 static function getGroupPermissions( $groups ) {
2760 global $wgGroupPermissions;
2761 $rights = array();
2762 foreach( $groups as $group ) {
2763 if( isset( $wgGroupPermissions[$group] ) ) {
2764 $rights = array_merge( $rights,
2765 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2768 return $rights;
2772 * @param $roles Array: list of roles
2773 * @return array list of groups with the given permission
2775 static function getGroupsInRoles( $roles ) {
2776 global $wgGroupPermissions;
2777 $allowedGroups = array();
2778 foreach ( $roles as $role ) {
2779 foreach ( $wgGroupPermissions as $group => $rights ) {
2780 if ( $rights[$role] === true ) {
2781 $allowedGroups[$role][] = $group;
2785 return $allowedGroups;
2789 * @param $group String: key name
2790 * @return string localized descriptive name for group, if provided
2792 static function getGroupName( $group ) {
2793 global $wgMessageCache;
2794 $wgMessageCache->loadAllMessages();
2795 $key = "group-$group";
2796 $name = wfMsg( $key );
2797 return $name == '' || wfEmptyMsg( $key, $name )
2798 ? $group
2799 : $name;
2803 * @param $group String: key name
2804 * @return string localized descriptive name for member of a group, if provided
2806 static function getGroupMember( $group ) {
2807 global $wgMessageCache;
2808 $wgMessageCache->loadAllMessages();
2809 $key = "group-$group-member";
2810 $name = wfMsg( $key );
2811 return $name == '' || wfEmptyMsg( $key, $name )
2812 ? $group
2813 : $name;
2817 * Return the set of defined explicit groups.
2818 * The implicit groups (by default *, 'user' and 'autoconfirmed')
2819 * are not included, as they are defined automatically,
2820 * not in the database.
2821 * @return array
2823 static function getAllGroups() {
2824 global $wgGroupPermissions;
2825 return array_diff(
2826 array_keys( $wgGroupPermissions ),
2827 self::getImplicitGroups()
2832 * Get a list of all available permissions
2834 static function getAllRights() {
2835 if ( self::$mAllRights === false ) {
2836 global $wgAvailableRights;
2837 if ( count( $wgAvailableRights ) ) {
2838 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
2839 } else {
2840 self::$mAllRights = self::$mCoreRights;
2842 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
2844 return self::$mAllRights;
2848 * Get a list of implicit groups
2850 * @return array
2852 public static function getImplicitGroups() {
2853 global $wgImplicitGroups;
2854 $groups = $wgImplicitGroups;
2855 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
2856 return $groups;
2860 * Get the title of a page describing a particular group
2862 * @param $group Name of the group
2863 * @return mixed
2865 static function getGroupPage( $group ) {
2866 global $wgMessageCache;
2867 $wgMessageCache->loadAllMessages();
2868 $page = wfMsgForContent( 'grouppage-' . $group );
2869 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2870 $title = Title::newFromText( $page );
2871 if( is_object( $title ) )
2872 return $title;
2874 return false;
2878 * Create a link to the group in HTML, if available
2880 * @param $group Name of the group
2881 * @param $text The text of the link
2882 * @return mixed
2884 static function makeGroupLinkHTML( $group, $text = '' ) {
2885 if( $text == '' ) {
2886 $text = self::getGroupName( $group );
2888 $title = self::getGroupPage( $group );
2889 if( $title ) {
2890 global $wgUser;
2891 $sk = $wgUser->getSkin();
2892 return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
2893 } else {
2894 return $text;
2899 * Create a link to the group in Wikitext, if available
2901 * @param $group Name of the group
2902 * @param $text The text of the link (by default, the name of the group)
2903 * @return mixed
2905 static function makeGroupLinkWiki( $group, $text = '' ) {
2906 if( $text == '' ) {
2907 $text = self::getGroupName( $group );
2909 $title = self::getGroupPage( $group );
2910 if( $title ) {
2911 $page = $title->getPrefixedText();
2912 return "[[$page|$text]]";
2913 } else {
2914 return $text;
2919 * Increment the user's edit-count field.
2920 * Will have no effect for anonymous users.
2922 function incEditCount() {
2923 if( !$this->isAnon() ) {
2924 $dbw = wfGetDB( DB_MASTER );
2925 $dbw->update( 'user',
2926 array( 'user_editcount=user_editcount+1' ),
2927 array( 'user_id' => $this->getId() ),
2928 __METHOD__ );
2930 // Lazy initialization check...
2931 if( $dbw->affectedRows() == 0 ) {
2932 // Pull from a slave to be less cruel to servers
2933 // Accuracy isn't the point anyway here
2934 $dbr = wfGetDB( DB_SLAVE );
2935 $count = $dbr->selectField( 'revision',
2936 'COUNT(rev_user)',
2937 array( 'rev_user' => $this->getId() ),
2938 __METHOD__ );
2940 // Now here's a goddamn hack...
2941 if( $dbr !== $dbw ) {
2942 // If we actually have a slave server, the count is
2943 // at least one behind because the current transaction
2944 // has not been committed and replicated.
2945 $count++;
2946 } else {
2947 // But if DB_SLAVE is selecting the master, then the
2948 // count we just read includes the revision that was
2949 // just added in the working transaction.
2952 $dbw->update( 'user',
2953 array( 'user_editcount' => $count ),
2954 array( 'user_id' => $this->getId() ),
2955 __METHOD__ );
2958 // edit count in user cache too
2959 $this->invalidateCache();
2962 static function getRightDescription( $right ) {
2963 global $wgMessageCache;
2964 $wgMessageCache->loadAllMessages();
2965 $key = "right-$right";
2966 $name = wfMsg( $key );
2967 return $name == '' || wfEmptyMsg( $key, $name )
2968 ? $right
2969 : $name;
2973 * Make an old-style password hash
2975 * @param $password String: plain-text password
2976 * @param $userId String: user ID
2978 static function oldCrypt( $password, $userId ) {
2979 global $wgPasswordSalt;
2980 if ( $wgPasswordSalt ) {
2981 return md5( $userId . '-' . md5( $password ) );
2982 } else {
2983 return md5( $password );
2988 * Make a new-style password hash
2990 * @param $password String: plain-text password
2991 * @param $salt String: salt, may be random or the user ID. False to generate a salt.
2993 static function crypt( $password, $salt = false ) {
2994 global $wgPasswordSalt;
2996 if($wgPasswordSalt) {
2997 if ( $salt === false ) {
2998 $salt = substr( wfGenerateToken(), 0, 8 );
3000 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3001 } else {
3002 return ':A:' . md5( $password);
3007 * Compare a password hash with a plain-text password. Requires the user
3008 * ID if there's a chance that the hash is an old-style hash.
3010 * @param $hash String: password hash
3011 * @param $password String: plain-text password to compare
3012 * @param $userId String: user ID for old-style password salt
3014 static function comparePasswords( $hash, $password, $userId = false ) {
3015 $m = false;
3016 $type = substr( $hash, 0, 3 );
3017 if ( $type == ':A:' ) {
3018 # Unsalted
3019 return md5( $password ) === substr( $hash, 3 );
3020 } elseif ( $type == ':B:' ) {
3021 # Salted
3022 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3023 return md5( $salt.'-'.md5( $password ) ) == $realHash;
3024 } else {
3025 # Old-style
3026 return self::oldCrypt( $password, $userId ) === $hash;