Fix more double quoting
[mediawiki.git] / includes / User.php
blob81246a1190764c1a6e34e72a0f57e36ff03d8d38
1 <?php
2 /**
3 * Implements the User class for the %MediaWiki software.
4 * @file
5 */
7 /**
8 * \int Number of characters in user_token field.
9 * @ingroup Constants
11 define( 'USER_TOKEN_LENGTH', 32 );
13 /**
14 * \int Serialized record version.
15 * @ingroup Constants
17 define( 'MW_USER_VERSION', 6 );
19 /**
20 * \string Some punctuation to prevent editing from broken text-mangling proxies.
21 * @ingroup Constants
23 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
25 /**
26 * Thrown by User::setPassword() on error.
27 * @ingroup Exception
29 class PasswordError extends MWException {
30 // NOP
33 /**
34 * The User object encapsulates all of the user-specific settings (user_id,
35 * name, rights, password, email address, options, last login time). Client
36 * classes use the getXXX() functions to access these fields. These functions
37 * do all the work of determining whether the user is logged in,
38 * whether the requested option can be satisfied from cookies or
39 * whether a database query is needed. Most of the settings needed
40 * for rendering normal pages are set in the cookie to minimize use
41 * of the database.
43 class User {
45 /**
46 * \type{\arrayof{\string}} A list of default user toggles, i.e., boolean user
47 * preferences that are displayed by Special:Preferences as checkboxes.
48 * This list can be extended via the UserToggles hook or by
49 * $wgContLang::getExtraUserToggles().
50 * @showinitializer
52 public static $mToggles = array(
53 'highlightbroken',
54 'justify',
55 'hideminor',
56 'extendwatchlist',
57 'usenewrc',
58 'numberheadings',
59 'showtoolbar',
60 'editondblclick',
61 'editsection',
62 'editsectiononrightclick',
63 'showtoc',
64 'rememberpassword',
65 'editwidth',
66 'watchcreations',
67 'watchdefault',
68 'watchmoves',
69 'watchdeletion',
70 'minordefault',
71 'previewontop',
72 'previewonfirst',
73 'nocache',
74 'enotifwatchlistpages',
75 'enotifusertalkpages',
76 'enotifminoredits',
77 'enotifrevealaddr',
78 'shownumberswatching',
79 'fancysig',
80 'externaleditor',
81 'externaldiff',
82 'showjumplinks',
83 'uselivepreview',
84 'forceeditsummary',
85 'watchlisthideminor',
86 'watchlisthidebots',
87 'watchlisthideown',
88 'watchlisthideanons',
89 'watchlisthideliu',
90 'ccmeonemails',
91 'diffonly',
92 'showhiddencats',
93 'noconvertlink',
94 'norollbackdiff',
97 /**
98 * \type{\arrayof{\string}} List of member variables which are saved to the
99 * shared cache (memcached). Any operation which changes the
100 * corresponding database fields must call a cache-clearing function.
101 * @showinitializer
103 static $mCacheVars = array(
104 // user table
105 'mId',
106 'mName',
107 'mRealName',
108 'mPassword',
109 'mNewpassword',
110 'mNewpassTime',
111 'mEmail',
112 'mOptions',
113 'mTouched',
114 'mToken',
115 'mEmailAuthenticated',
116 'mEmailToken',
117 'mEmailTokenExpires',
118 'mRegistration',
119 'mEditCount',
120 // user_group table
121 'mGroups',
125 * \type{\arrayof{\string}} Core rights.
126 * Each of these should have a corresponding message of the form
127 * "right-$right".
128 * @showinitializer
130 static $mCoreRights = array(
131 'apihighlimits',
132 'autoconfirmed',
133 'autopatrol',
134 'bigdelete',
135 'block',
136 'blockemail',
137 'bot',
138 'browsearchive',
139 'createaccount',
140 'createpage',
141 'createtalk',
142 'delete',
143 'deletedhistory',
144 'deleterevision',
145 'edit',
146 'editinterface',
147 'editusercssjs',
148 'hideuser',
149 'import',
150 'importupload',
151 'ipblock-exempt',
152 'markbotedits',
153 'minoredit',
154 'move',
155 'movefile',
156 'move-rootuserpages',
157 'move-subpages',
158 'nominornewtalk',
159 'noratelimit',
160 'patrol',
161 'protect',
162 'proxyunbannable',
163 'purge',
164 'read',
165 'reupload',
166 'reupload-shared',
167 'rollback',
168 'siteadmin',
169 'suppressionlog',
170 'suppressredirect',
171 'suppressrevision',
172 'trackback',
173 'undelete',
174 'unwatchedpages',
175 'upload',
176 'upload_by_url',
177 'userrights',
178 'userrights-interwiki',
179 'writeapi',
182 * \string Cached results of getAllRights()
184 static $mAllRights = false;
186 /** @name Cache variables */
187 //@{
188 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
189 $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
190 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
191 //@}
194 * \bool Whether the cache variables have been loaded.
196 var $mDataLoaded, $mAuthLoaded;
199 * \string Initialization data source if mDataLoaded==false. May be one of:
200 * - 'defaults' anonymous user initialised from class defaults
201 * - 'name' initialise from mName
202 * - 'id' initialise from mId
203 * - 'session' log in from cookies or session if possible
205 * Use the User::newFrom*() family of functions to set this.
207 var $mFrom;
209 /** @name Lazy-initialized variables, invalidated with clearInstanceCache */
210 //@{
211 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
212 $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
213 $mLocked, $mHideName;
214 //@}
217 * Lightweight constructor for an anonymous user.
218 * Use the User::newFrom* factory functions for other kinds of users.
220 * @see newFromName()
221 * @see newFromId()
222 * @see newFromConfirmationCode()
223 * @see newFromSession()
224 * @see newFromRow()
226 function User() {
227 $this->clearInstanceCache( 'defaults' );
231 * Load the user table data for this object from the source given by mFrom.
233 function load() {
234 if ( $this->mDataLoaded ) {
235 return;
237 wfProfileIn( __METHOD__ );
239 # Set it now to avoid infinite recursion in accessors
240 $this->mDataLoaded = true;
242 switch ( $this->mFrom ) {
243 case 'defaults':
244 $this->loadDefaults();
245 break;
246 case 'name':
247 $this->mId = self::idFromName( $this->mName );
248 if ( !$this->mId ) {
249 # Nonexistent user placeholder object
250 $this->loadDefaults( $this->mName );
251 } else {
252 $this->loadFromId();
254 break;
255 case 'id':
256 $this->loadFromId();
257 break;
258 case 'session':
259 $this->loadFromSession();
260 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
261 break;
262 default:
263 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
265 wfProfileOut( __METHOD__ );
269 * Load user table data, given mId has already been set.
270 * @return \bool false if the ID does not exist, true otherwise
271 * @private
273 function loadFromId() {
274 global $wgMemc;
275 if ( $this->mId == 0 ) {
276 $this->loadDefaults();
277 return false;
280 # Try cache
281 $key = wfMemcKey( 'user', 'id', $this->mId );
282 $data = $wgMemc->get( $key );
283 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
284 # Object is expired, load from DB
285 $data = false;
288 if ( !$data ) {
289 wfDebug( "Cache miss for user {$this->mId}\n" );
290 # Load from DB
291 if ( !$this->loadFromDatabase() ) {
292 # Can't load from ID, user is anonymous
293 return false;
295 $this->saveToCache();
296 } else {
297 wfDebug( "Got user {$this->mId} from cache\n" );
298 # Restore from cache
299 foreach ( self::$mCacheVars as $name ) {
300 $this->$name = $data[$name];
303 return true;
307 * Save user data to the shared cache
309 function saveToCache() {
310 $this->load();
311 $this->loadGroups();
312 if ( $this->isAnon() ) {
313 // Anonymous users are uncached
314 return;
316 $data = array();
317 foreach ( self::$mCacheVars as $name ) {
318 $data[$name] = $this->$name;
320 $data['mVersion'] = MW_USER_VERSION;
321 $key = wfMemcKey( 'user', 'id', $this->mId );
322 global $wgMemc;
323 $wgMemc->set( $key, $data );
327 /** @name newFrom*() static factory methods */
328 //@{
331 * Static factory method for creation from username.
333 * This is slightly less efficient than newFromId(), so use newFromId() if
334 * you have both an ID and a name handy.
336 * @param $name \string Username, validated by Title::newFromText()
337 * @param $validate \mixed Validate username. Takes the same parameters as
338 * User::getCanonicalName(), except that true is accepted as an alias
339 * for 'valid', for BC.
341 * @return \type{User} The User object, or null if the username is invalid. If the
342 * username is not present in the database, the result will be a user object
343 * with a name, zero user ID and default settings.
345 static function newFromName( $name, $validate = 'valid' ) {
346 if ( $validate === true ) {
347 $validate = 'valid';
349 $name = self::getCanonicalName( $name, $validate );
350 if ( $name === false ) {
351 return null;
352 } else {
353 # Create unloaded user object
354 $u = new User;
355 $u->mName = $name;
356 $u->mFrom = 'name';
357 return $u;
362 * Static factory method for creation from a given user ID.
364 * @param $id \int Valid user ID
365 * @return \type{User} The corresponding User object
367 static function newFromId( $id ) {
368 $u = new User;
369 $u->mId = $id;
370 $u->mFrom = 'id';
371 return $u;
375 * Factory method to fetch whichever user has a given email confirmation code.
376 * This code is generated when an account is created or its e-mail address
377 * has changed.
379 * If the code is invalid or has expired, returns NULL.
381 * @param $code \string Confirmation code
382 * @return \type{User}
384 static function newFromConfirmationCode( $code ) {
385 $dbr = wfGetDB( DB_SLAVE );
386 $id = $dbr->selectField( 'user', 'user_id', array(
387 'user_email_token' => md5( $code ),
388 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
389 ) );
390 if( $id !== false ) {
391 return User::newFromId( $id );
392 } else {
393 return null;
398 * Create a new user object using data from session or cookies. If the
399 * login credentials are invalid, the result is an anonymous user.
401 * @return \type{User}
403 static function newFromSession() {
404 $user = new User;
405 $user->mFrom = 'session';
406 return $user;
410 * Create a new user object from a user row.
411 * The row should have all fields from the user table in it.
412 * @param $row array A row from the user table
413 * @return \type{User}
415 static function newFromRow( $row ) {
416 $user = new User;
417 $user->loadFromRow( $row );
418 return $user;
421 //@}
425 * Get the username corresponding to a given user ID
426 * @param $id \int User ID
427 * @return \string The corresponding username
429 static function whoIs( $id ) {
430 $dbr = wfGetDB( DB_SLAVE );
431 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
435 * Get the real name of a user given their user ID
437 * @param $id \int User ID
438 * @return \string The corresponding user's real name
440 static function whoIsReal( $id ) {
441 $dbr = wfGetDB( DB_SLAVE );
442 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
446 * Get database id given a user name
447 * @param $name \string Username
448 * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
450 static function idFromName( $name ) {
451 $nt = Title::makeTitleSafe( NS_USER, $name );
452 if( is_null( $nt ) ) {
453 # Illegal name
454 return null;
456 $dbr = wfGetDB( DB_SLAVE );
457 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
459 if ( $s === false ) {
460 return 0;
461 } else {
462 return $s->user_id;
467 * Does the string match an anonymous IPv4 address?
469 * This function exists for username validation, in order to reject
470 * usernames which are similar in form to IP addresses. Strings such
471 * as 300.300.300.300 will return true because it looks like an IP
472 * address, despite not being strictly valid.
474 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
475 * address because the usemod software would "cloak" anonymous IP
476 * addresses like this, if we allowed accounts like this to be created
477 * new users could get the old edits of these anonymous users.
479 * @param $name \string String to match
480 * @return \bool True or false
482 static function isIP( $name ) {
483 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
487 * Is the input a valid username?
489 * Checks if the input is a valid username, we don't want an empty string,
490 * an IP address, anything that containins slashes (would mess up subpages),
491 * is longer than the maximum allowed username size or doesn't begin with
492 * a capital letter.
494 * @param $name \string String to match
495 * @return \bool True or false
497 static function isValidUserName( $name ) {
498 global $wgContLang, $wgMaxNameChars;
500 if ( $name == ''
501 || User::isIP( $name )
502 || strpos( $name, '/' ) !== false
503 || strlen( $name ) > $wgMaxNameChars
504 || $name != $wgContLang->ucfirst( $name ) ) {
505 wfDebugLog( 'username', __METHOD__ .
506 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
507 return false;
510 // Ensure that the name can't be misresolved as a different title,
511 // such as with extra namespace keys at the start.
512 $parsed = Title::newFromText( $name );
513 if( is_null( $parsed )
514 || $parsed->getNamespace()
515 || strcmp( $name, $parsed->getPrefixedText() ) ) {
516 wfDebugLog( 'username', __METHOD__ .
517 ": '$name' invalid due to ambiguous prefixes" );
518 return false;
521 // Check an additional blacklist of troublemaker characters.
522 // Should these be merged into the title char list?
523 $unicodeBlacklist = '/[' .
524 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
525 '\x{00a0}' . # non-breaking space
526 '\x{2000}-\x{200f}' . # various whitespace
527 '\x{2028}-\x{202f}' . # breaks and control chars
528 '\x{3000}' . # ideographic space
529 '\x{e000}-\x{f8ff}' . # private use
530 ']/u';
531 if( preg_match( $unicodeBlacklist, $name ) ) {
532 wfDebugLog( 'username', __METHOD__ .
533 ": '$name' invalid due to blacklisted characters" );
534 return false;
537 return true;
541 * Usernames which fail to pass this function will be blocked
542 * from user login and new account registrations, but may be used
543 * internally by batch processes.
545 * If an account already exists in this form, login will be blocked
546 * by a failure to pass this function.
548 * @param $name \string String to match
549 * @return \bool True or false
551 static function isUsableName( $name ) {
552 global $wgReservedUsernames;
553 // Must be a valid username, obviously ;)
554 if ( !self::isValidUserName( $name ) ) {
555 return false;
558 static $reservedUsernames = false;
559 if ( !$reservedUsernames ) {
560 $reservedUsernames = $wgReservedUsernames;
561 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
564 // Certain names may be reserved for batch processes.
565 foreach ( $reservedUsernames as $reserved ) {
566 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
567 $reserved = wfMsgForContent( substr( $reserved, 4 ) );
569 if ( $reserved == $name ) {
570 return false;
573 return true;
577 * Usernames which fail to pass this function will be blocked
578 * from new account registrations, but may be used internally
579 * either by batch processes or by user accounts which have
580 * already been created.
582 * Additional character blacklisting may be added here
583 * rather than in isValidUserName() to avoid disrupting
584 * existing accounts.
586 * @param $name \string String to match
587 * @return \bool True or false
589 static function isCreatableName( $name ) {
590 return
591 self::isUsableName( $name ) &&
593 // Registration-time character blacklisting...
594 strpos( $name, '@' ) === false;
598 * Is the input a valid password for this user?
600 * @param $password \string Desired password
601 * @return \bool True or false
603 function isValidPassword( $password ) {
604 global $wgMinimalPasswordLength, $wgContLang;
606 $result = null;
607 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
608 return $result;
609 if( $result === false )
610 return false;
612 // Password needs to be long enough, and can't be the same as the username
613 return strlen( $password ) >= $wgMinimalPasswordLength
614 && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
618 * Does a string look like an e-mail address?
620 * There used to be a regular expression here, it got removed because it
621 * rejected valid addresses. Actually just check if there is '@' somewhere
622 * in the given address.
624 * @todo Check for RFC 2822 compilance (bug 959)
626 * @param $addr \string E-mail address
627 * @return \bool True or false
629 public static function isValidEmailAddr( $addr ) {
630 $result = null;
631 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
632 return $result;
635 return strpos( $addr, '@' ) !== false;
639 * Given unvalidated user input, return a canonical username, or false if
640 * the username is invalid.
641 * @param $name \string User input
642 * @param $validate \types{\string,\bool} Type of validation to use:
643 * - false No validation
644 * - 'valid' Valid for batch processes
645 * - 'usable' Valid for batch processes and login
646 * - 'creatable' Valid for batch processes, login and account creation
648 static function getCanonicalName( $name, $validate = 'valid' ) {
649 # Force usernames to capital
650 global $wgContLang;
651 $name = $wgContLang->ucfirst( $name );
653 # Reject names containing '#'; these will be cleaned up
654 # with title normalisation, but then it's too late to
655 # check elsewhere
656 if( strpos( $name, '#' ) !== false )
657 return false;
659 # Clean up name according to title rules
660 $t = ($validate === 'valid') ?
661 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
662 # Check for invalid titles
663 if( is_null( $t ) ) {
664 return false;
667 # Reject various classes of invalid names
668 $name = $t->getText();
669 global $wgAuth;
670 $name = $wgAuth->getCanonicalName( $t->getText() );
672 switch ( $validate ) {
673 case false:
674 break;
675 case 'valid':
676 if ( !User::isValidUserName( $name ) ) {
677 $name = false;
679 break;
680 case 'usable':
681 if ( !User::isUsableName( $name ) ) {
682 $name = false;
684 break;
685 case 'creatable':
686 if ( !User::isCreatableName( $name ) ) {
687 $name = false;
689 break;
690 default:
691 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
693 return $name;
697 * Count the number of edits of a user
698 * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
700 * @param $uid \int User ID to check
701 * @return \int The user's edit count
703 static function edits( $uid ) {
704 wfProfileIn( __METHOD__ );
705 $dbr = wfGetDB( DB_SLAVE );
706 // check if the user_editcount field has been initialized
707 $field = $dbr->selectField(
708 'user', 'user_editcount',
709 array( 'user_id' => $uid ),
710 __METHOD__
713 if( $field === null ) { // it has not been initialized. do so.
714 $dbw = wfGetDB( DB_MASTER );
715 $count = $dbr->selectField(
716 'revision', 'count(*)',
717 array( 'rev_user' => $uid ),
718 __METHOD__
720 $dbw->update(
721 'user',
722 array( 'user_editcount' => $count ),
723 array( 'user_id' => $uid ),
724 __METHOD__
726 } else {
727 $count = $field;
729 wfProfileOut( __METHOD__ );
730 return $count;
734 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
735 * @todo hash random numbers to improve security, like generateToken()
737 * @return \string New random password
739 static function randomPassword() {
740 global $wgMinimalPasswordLength;
741 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
742 $l = strlen( $pwchars ) - 1;
744 $pwlength = max( 7, $wgMinimalPasswordLength );
745 $digit = mt_rand(0, $pwlength - 1);
746 $np = '';
747 for ( $i = 0; $i < $pwlength; $i++ ) {
748 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
750 return $np;
754 * Set cached properties to default.
756 * @note This no longer clears uncached lazy-initialised properties;
757 * the constructor does that instead.
758 * @private
760 function loadDefaults( $name = false ) {
761 wfProfileIn( __METHOD__ );
763 global $wgCookiePrefix;
765 $this->mId = 0;
766 $this->mName = $name;
767 $this->mRealName = '';
768 $this->mPassword = $this->mNewpassword = '';
769 $this->mNewpassTime = null;
770 $this->mEmail = '';
771 $this->mOptions = null; # Defer init
773 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
774 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
775 } else {
776 $this->mTouched = '0'; # Allow any pages to be cached
779 $this->setToken(); # Random
780 $this->mEmailAuthenticated = null;
781 $this->mEmailToken = '';
782 $this->mEmailTokenExpires = null;
783 $this->mRegistration = wfTimestamp( TS_MW );
784 $this->mGroups = array();
786 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
788 wfProfileOut( __METHOD__ );
792 * @deprecated Use wfSetupSession().
794 function SetupSession() {
795 wfDeprecated( __METHOD__ );
796 wfSetupSession();
800 * Load user data from the session or login cookie. If there are no valid
801 * credentials, initialises the user as an anonymous user.
802 * @return \bool True if the user is logged in, false otherwise.
804 private function loadFromSession() {
805 global $wgMemc, $wgCookiePrefix;
807 $result = null;
808 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
809 if ( $result !== null ) {
810 return $result;
813 if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
814 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
815 if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
816 $this->loadDefaults(); // Possible collision!
817 wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
818 cookie user ID ($sId) don't match!" );
819 return false;
821 $_SESSION['wsUserID'] = $sId;
822 } else if ( isset( $_SESSION['wsUserID'] ) ) {
823 if ( $_SESSION['wsUserID'] != 0 ) {
824 $sId = $_SESSION['wsUserID'];
825 } else {
826 $this->loadDefaults();
827 return false;
829 } else {
830 $this->loadDefaults();
831 return false;
834 if ( isset( $_SESSION['wsUserName'] ) ) {
835 $sName = $_SESSION['wsUserName'];
836 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
837 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
838 $_SESSION['wsUserName'] = $sName;
839 } else {
840 $this->loadDefaults();
841 return false;
844 $passwordCorrect = FALSE;
845 $this->mId = $sId;
846 if ( !$this->loadFromId() ) {
847 # Not a valid ID, loadFromId has switched the object to anon for us
848 return false;
851 if ( isset( $_SESSION['wsToken'] ) ) {
852 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
853 $from = 'session';
854 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
855 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
856 $from = 'cookie';
857 } else {
858 # No session or persistent login cookie
859 $this->loadDefaults();
860 return false;
863 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
864 $_SESSION['wsToken'] = $this->mToken;
865 wfDebug( "Logged in from $from\n" );
866 return true;
867 } else {
868 # Invalid credentials
869 wfDebug( "Can't log in from $from, invalid credentials\n" );
870 $this->loadDefaults();
871 return false;
876 * Load user and user_group data from the database.
877 * $this::mId must be set, this is how the user is identified.
879 * @return \bool True if the user exists, false if the user is anonymous
880 * @private
882 function loadFromDatabase() {
883 # Paranoia
884 $this->mId = intval( $this->mId );
886 /** Anonymous user */
887 if( !$this->mId ) {
888 $this->loadDefaults();
889 return false;
892 $dbr = wfGetDB( DB_MASTER );
893 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
895 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
897 if ( $s !== false ) {
898 # Initialise user table data
899 $this->loadFromRow( $s );
900 $this->mGroups = null; // deferred
901 $this->getEditCount(); // revalidation for nulls
902 return true;
903 } else {
904 # Invalid user_id
905 $this->mId = 0;
906 $this->loadDefaults();
907 return false;
912 * Initialize this object from a row from the user table.
914 * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
916 function loadFromRow( $row ) {
917 $this->mDataLoaded = true;
919 if ( isset( $row->user_id ) ) {
920 $this->mId = intval( $row->user_id );
922 $this->mName = $row->user_name;
923 $this->mRealName = $row->user_real_name;
924 $this->mPassword = $row->user_password;
925 $this->mNewpassword = $row->user_newpassword;
926 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
927 $this->mEmail = $row->user_email;
928 $this->decodeOptions( $row->user_options );
929 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
930 $this->mToken = $row->user_token;
931 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
932 $this->mEmailToken = $row->user_email_token;
933 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
934 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
935 $this->mEditCount = $row->user_editcount;
939 * Load the groups from the database if they aren't already loaded.
940 * @private
942 function loadGroups() {
943 if ( is_null( $this->mGroups ) ) {
944 $dbr = wfGetDB( DB_MASTER );
945 $res = $dbr->select( 'user_groups',
946 array( 'ug_group' ),
947 array( 'ug_user' => $this->mId ),
948 __METHOD__ );
949 $this->mGroups = array();
950 while( $row = $dbr->fetchObject( $res ) ) {
951 $this->mGroups[] = $row->ug_group;
957 * Clear various cached data stored in this object.
958 * @param $reloadFrom \string Reload user and user_groups table data from a
959 * given source. May be "name", "id", "defaults", "session", or false for
960 * no reload.
962 function clearInstanceCache( $reloadFrom = false ) {
963 $this->mNewtalk = -1;
964 $this->mDatePreference = null;
965 $this->mBlockedby = -1; # Unset
966 $this->mHash = false;
967 $this->mSkin = null;
968 $this->mRights = null;
969 $this->mEffectiveGroups = null;
971 if ( $reloadFrom ) {
972 $this->mDataLoaded = false;
973 $this->mFrom = $reloadFrom;
978 * Combine the language default options with any site-specific options
979 * and add the default language variants.
981 * @return \type{\arrayof{\string}} Array of options
983 static function getDefaultOptions() {
984 global $wgNamespacesToBeSearchedDefault;
986 * Site defaults will override the global/language defaults
988 global $wgDefaultUserOptions, $wgContLang;
989 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
992 * default language setting
994 $variant = $wgContLang->getPreferredVariant( false );
995 $defOpt['variant'] = $variant;
996 $defOpt['language'] = $variant;
998 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
999 $defOpt['searchNs'.$nsnum] = $val;
1001 return $defOpt;
1005 * Get a given default option value.
1007 * @param $opt \string Name of option to retrieve
1008 * @return \string Default option value
1010 public static function getDefaultOption( $opt ) {
1011 $defOpts = self::getDefaultOptions();
1012 if( isset( $defOpts[$opt] ) ) {
1013 return $defOpts[$opt];
1014 } else {
1015 return '';
1020 * Get a list of user toggle names
1021 * @return \type{\arrayof{\string}} Array of user toggle names
1023 static function getToggles() {
1024 global $wgContLang, $wgUseRCPatrol;
1025 $extraToggles = array();
1026 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
1027 if( $wgUseRCPatrol ) {
1028 $extraToggles[] = 'hidepatrolled';
1029 $extraToggles[] = 'newpageshidepatrolled';
1030 $extraToggles[] = 'watchlisthidepatrolled';
1032 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
1037 * Get blocking information
1038 * @private
1039 * @param $bFromSlave \bool Whether to check the slave database first. To
1040 * improve performance, non-critical checks are done
1041 * against slaves. Check when actually saving should be
1042 * done against master.
1044 function getBlockedStatus( $bFromSlave = true ) {
1045 global $wgEnableSorbs, $wgProxyWhitelist;
1047 if ( -1 != $this->mBlockedby ) {
1048 wfDebug( "User::getBlockedStatus: already loaded.\n" );
1049 return;
1052 wfProfileIn( __METHOD__ );
1053 wfDebug( __METHOD__.": checking...\n" );
1055 // Initialize data...
1056 // Otherwise something ends up stomping on $this->mBlockedby when
1057 // things get lazy-loaded later, causing false positive block hits
1058 // due to -1 !== 0. Probably session-related... Nothing should be
1059 // overwriting mBlockedby, surely?
1060 $this->load();
1062 $this->mBlockedby = 0;
1063 $this->mHideName = 0;
1064 $this->mAllowUsertalk = 0;
1065 $ip = wfGetIP();
1067 if ($this->isAllowed( 'ipblock-exempt' ) ) {
1068 # Exempt from all types of IP-block
1069 $ip = '';
1072 # User/IP blocking
1073 $this->mBlock = new Block();
1074 $this->mBlock->fromMaster( !$bFromSlave );
1075 if ( $this->mBlock->load( $ip , $this->mId ) ) {
1076 wfDebug( __METHOD__.": Found block.\n" );
1077 $this->mBlockedby = $this->mBlock->mBy;
1078 $this->mBlockreason = $this->mBlock->mReason;
1079 $this->mHideName = $this->mBlock->mHideName;
1080 $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
1081 if ( $this->isLoggedIn() ) {
1082 $this->spreadBlock();
1084 } else {
1085 // Bug 13611: don't remove mBlock here, to allow account creation blocks to
1086 // apply to users. Note that the existence of $this->mBlock is not used to
1087 // check for edit blocks, $this->mBlockedby is instead.
1090 # Proxy blocking
1091 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
1092 # Local list
1093 if ( wfIsLocallyBlockedProxy( $ip ) ) {
1094 $this->mBlockedby = wfMsg( 'proxyblocker' );
1095 $this->mBlockreason = wfMsg( 'proxyblockreason' );
1098 # DNSBL
1099 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
1100 if ( $this->inSorbsBlacklist( $ip ) ) {
1101 $this->mBlockedby = wfMsg( 'sorbs' );
1102 $this->mBlockreason = wfMsg( 'sorbsreason' );
1107 # Extensions
1108 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1110 wfProfileOut( __METHOD__ );
1114 * Whether the given IP is in the SORBS blacklist.
1116 * @param $ip \string IP to check
1117 * @return \bool True if blacklisted.
1119 function inSorbsBlacklist( $ip ) {
1120 global $wgEnableSorbs, $wgSorbsUrl;
1122 return $wgEnableSorbs &&
1123 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
1127 * Whether the given IP is in a given DNS blacklist.
1129 * @param $ip \string IP to check
1130 * @param $base \string URL of the DNS blacklist
1131 * @return \bool True if blacklisted.
1133 function inDnsBlacklist( $ip, $base ) {
1134 wfProfileIn( __METHOD__ );
1136 $found = false;
1137 $host = '';
1138 // FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
1139 if( IP::isIPv4($ip) ) {
1140 # Make hostname
1141 $host = "$ip.$base";
1143 # Send query
1144 $ipList = gethostbynamel( $host );
1146 if( $ipList ) {
1147 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1148 $found = true;
1149 } else {
1150 wfDebug( "Requested $host, not found in $base.\n" );
1154 wfProfileOut( __METHOD__ );
1155 return $found;
1159 * Is this user subject to rate limiting?
1161 * @return \bool True if rate limited
1163 public function isPingLimitable() {
1164 global $wgRateLimitsExcludedGroups;
1165 global $wgRateLimitsExcludedIPs;
1166 if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1167 // Deprecated, but kept for backwards-compatibility config
1168 return false;
1170 if( in_array( wfGetIP(), $wgRateLimitsExcludedIPs ) ) {
1171 // No other good way currently to disable rate limits
1172 // for specific IPs. :P
1173 // But this is a crappy hack and should die.
1174 return false;
1176 return !$this->isAllowed('noratelimit');
1180 * Primitive rate limits: enforce maximum actions per time period
1181 * to put a brake on flooding.
1183 * @note When using a shared cache like memcached, IP-address
1184 * last-hit counters will be shared across wikis.
1186 * @param $action \string Action to enforce; 'edit' if unspecified
1187 * @return \bool True if a rate limiter was tripped
1189 function pingLimiter( $action='edit' ) {
1191 # Call the 'PingLimiter' hook
1192 $result = false;
1193 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1194 return $result;
1197 global $wgRateLimits;
1198 if( !isset( $wgRateLimits[$action] ) ) {
1199 return false;
1202 # Some groups shouldn't trigger the ping limiter, ever
1203 if( !$this->isPingLimitable() )
1204 return false;
1206 global $wgMemc, $wgRateLimitLog;
1207 wfProfileIn( __METHOD__ );
1209 $limits = $wgRateLimits[$action];
1210 $keys = array();
1211 $id = $this->getId();
1212 $ip = wfGetIP();
1213 $userLimit = false;
1215 if( isset( $limits['anon'] ) && $id == 0 ) {
1216 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1219 if( isset( $limits['user'] ) && $id != 0 ) {
1220 $userLimit = $limits['user'];
1222 if( $this->isNewbie() ) {
1223 if( isset( $limits['newbie'] ) && $id != 0 ) {
1224 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1226 if( isset( $limits['ip'] ) ) {
1227 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1229 $matches = array();
1230 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1231 $subnet = $matches[1];
1232 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1235 // Check for group-specific permissions
1236 // If more than one group applies, use the group with the highest limit
1237 foreach ( $this->getGroups() as $group ) {
1238 if ( isset( $limits[$group] ) ) {
1239 if ( $userLimit === false || $limits[$group] > $userLimit ) {
1240 $userLimit = $limits[$group];
1244 // Set the user limit key
1245 if ( $userLimit !== false ) {
1246 wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
1247 $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1250 $triggered = false;
1251 foreach( $keys as $key => $limit ) {
1252 list( $max, $period ) = $limit;
1253 $summary = "(limit $max in {$period}s)";
1254 $count = $wgMemc->get( $key );
1255 if( $count ) {
1256 if( $count > $max ) {
1257 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1258 if( $wgRateLimitLog ) {
1259 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1261 $triggered = true;
1262 } else {
1263 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1265 } else {
1266 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1267 $wgMemc->add( $key, 1, intval( $period ) );
1269 $wgMemc->incr( $key );
1272 wfProfileOut( __METHOD__ );
1273 return $triggered;
1277 * Check if user is blocked
1279 * @param $bFromSlave \bool Whether to check the slave database instead of the master
1280 * @return \bool True if blocked, false otherwise
1282 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1283 wfDebug( "User::isBlocked: enter\n" );
1284 $this->getBlockedStatus( $bFromSlave );
1285 return $this->mBlockedby !== 0;
1289 * Check if user is blocked from editing a particular article
1291 * @param $title \string Title to check
1292 * @param $bFromSlave \bool Whether to check the slave database instead of the master
1293 * @return \bool True if blocked, false otherwise
1295 function isBlockedFrom( $title, $bFromSlave = false ) {
1296 global $wgBlockAllowsUTEdit;
1297 wfProfileIn( __METHOD__ );
1298 wfDebug( __METHOD__.": enter\n" );
1300 wfDebug( __METHOD__.": asking isBlocked()\n" );
1301 $blocked = $this->isBlocked( $bFromSlave );
1302 $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
1303 # If a user's name is suppressed, they cannot make edits anywhere
1304 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1305 $title->getNamespace() == NS_USER_TALK ) {
1306 $blocked = false;
1307 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1309 wfProfileOut( __METHOD__ );
1310 return $blocked;
1314 * If user is blocked, return the name of the user who placed the block
1315 * @return \string name of blocker
1317 function blockedBy() {
1318 $this->getBlockedStatus();
1319 return $this->mBlockedby;
1323 * If user is blocked, return the specified reason for the block
1324 * @return \string Blocking reason
1326 function blockedFor() {
1327 $this->getBlockedStatus();
1328 return $this->mBlockreason;
1332 * If user is blocked, return the ID for the block
1333 * @return \int Block ID
1335 function getBlockId() {
1336 $this->getBlockedStatus();
1337 return ($this->mBlock ? $this->mBlock->mId : false);
1341 * Check if user is blocked on all wikis.
1342 * Do not use for actual edit permission checks!
1343 * This is intented for quick UI checks.
1345 * @param $ip \type{\string} IP address, uses current client if none given
1346 * @return \type{\bool} True if blocked, false otherwise
1348 function isBlockedGlobally( $ip = '' ) {
1349 if( $this->mBlockedGlobally !== null ) {
1350 return $this->mBlockedGlobally;
1352 // User is already an IP?
1353 if( IP::isIPAddress( $this->getName() ) ) {
1354 $ip = $this->getName();
1355 } else if( !$ip ) {
1356 $ip = wfGetIP();
1358 $blocked = false;
1359 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1360 $this->mBlockedGlobally = (bool)$blocked;
1361 return $this->mBlockedGlobally;
1365 * Check if user account is locked
1367 * @return \type{\bool} True if locked, false otherwise
1369 function isLocked() {
1370 if( $this->mLocked !== null ) {
1371 return $this->mLocked;
1373 global $wgAuth;
1374 $authUser = $wgAuth->getUserInstance( $this );
1375 $this->mLocked = (bool)$authUser->isLocked();
1376 return $this->mLocked;
1380 * Check if user account is hidden
1382 * @return \type{\bool} True if hidden, false otherwise
1384 function isHidden() {
1385 if( $this->mHideName !== null ) {
1386 return $this->mHideName;
1388 $this->getBlockedStatus();
1389 if( !$this->mHideName ) {
1390 global $wgAuth;
1391 $authUser = $wgAuth->getUserInstance( $this );
1392 $this->mHideName = (bool)$authUser->isHidden();
1394 return $this->mHideName;
1398 * Get the user's ID.
1399 * @return \int The user's ID; 0 if the user is anonymous or nonexistent
1401 function getId() {
1402 if( $this->mId === null and $this->mName !== null
1403 and User::isIP( $this->mName ) ) {
1404 // Special case, we know the user is anonymous
1405 return 0;
1406 } elseif( $this->mId === null ) {
1407 // Don't load if this was initialized from an ID
1408 $this->load();
1410 return $this->mId;
1414 * Set the user and reload all fields according to a given ID
1415 * @param $v \int User ID to reload
1417 function setId( $v ) {
1418 $this->mId = $v;
1419 $this->clearInstanceCache( 'id' );
1423 * Get the user name, or the IP of an anonymous user
1424 * @return \string User's name or IP address
1426 function getName() {
1427 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1428 # Special case optimisation
1429 return $this->mName;
1430 } else {
1431 $this->load();
1432 if ( $this->mName === false ) {
1433 # Clean up IPs
1434 $this->mName = IP::sanitizeIP( wfGetIP() );
1436 return $this->mName;
1441 * Set the user name.
1443 * This does not reload fields from the database according to the given
1444 * name. Rather, it is used to create a temporary "nonexistent user" for
1445 * later addition to the database. It can also be used to set the IP
1446 * address for an anonymous user to something other than the current
1447 * remote IP.
1449 * @note User::newFromName() has rougly the same function, when the named user
1450 * does not exist.
1451 * @param $str \string New user name to set
1453 function setName( $str ) {
1454 $this->load();
1455 $this->mName = $str;
1459 * Get the user's name escaped by underscores.
1460 * @return \string Username escaped by underscores.
1462 function getTitleKey() {
1463 return str_replace( ' ', '_', $this->getName() );
1467 * Check if the user has new messages.
1468 * @return \bool True if the user has new messages
1470 function getNewtalk() {
1471 $this->load();
1473 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1474 if( $this->mNewtalk === -1 ) {
1475 $this->mNewtalk = false; # reset talk page status
1477 # Check memcached separately for anons, who have no
1478 # entire User object stored in there.
1479 if( !$this->mId ) {
1480 global $wgMemc;
1481 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1482 $newtalk = $wgMemc->get( $key );
1483 if( strval( $newtalk ) !== '' ) {
1484 $this->mNewtalk = (bool)$newtalk;
1485 } else {
1486 // Since we are caching this, make sure it is up to date by getting it
1487 // from the master
1488 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1489 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1491 } else {
1492 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1496 return (bool)$this->mNewtalk;
1500 * Return the talk page(s) this user has new messages on.
1501 * @return \type{\arrayof{\string}} Array of page URLs
1503 function getNewMessageLinks() {
1504 $talks = array();
1505 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1506 return $talks;
1508 if (!$this->getNewtalk())
1509 return array();
1510 $up = $this->getUserPage();
1511 $utp = $up->getTalkPage();
1512 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1517 * Internal uncached check for new messages
1519 * @see getNewtalk()
1520 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1521 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1522 * @param $fromMaster \bool true to fetch from the master, false for a slave
1523 * @return \bool True if the user has new messages
1524 * @private
1526 function checkNewtalk( $field, $id, $fromMaster = false ) {
1527 if ( $fromMaster ) {
1528 $db = wfGetDB( DB_MASTER );
1529 } else {
1530 $db = wfGetDB( DB_SLAVE );
1532 $ok = $db->selectField( 'user_newtalk', $field,
1533 array( $field => $id ), __METHOD__ );
1534 return $ok !== false;
1538 * Add or update the new messages flag
1539 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1540 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1541 * @return \bool True if successful, false otherwise
1542 * @private
1544 function updateNewtalk( $field, $id ) {
1545 $dbw = wfGetDB( DB_MASTER );
1546 $dbw->insert( 'user_newtalk',
1547 array( $field => $id ),
1548 __METHOD__,
1549 'IGNORE' );
1550 if ( $dbw->affectedRows() ) {
1551 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1552 return true;
1553 } else {
1554 wfDebug( __METHOD__." already set ($field, $id)\n" );
1555 return false;
1560 * Clear the new messages flag for the given user
1561 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1562 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1563 * @return \bool True if successful, false otherwise
1564 * @private
1566 function deleteNewtalk( $field, $id ) {
1567 $dbw = wfGetDB( DB_MASTER );
1568 $dbw->delete( 'user_newtalk',
1569 array( $field => $id ),
1570 __METHOD__ );
1571 if ( $dbw->affectedRows() ) {
1572 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1573 return true;
1574 } else {
1575 wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1576 return false;
1581 * Update the 'You have new messages!' status.
1582 * @param $val \bool Whether the user has new messages
1584 function setNewtalk( $val ) {
1585 if( wfReadOnly() ) {
1586 return;
1589 $this->load();
1590 $this->mNewtalk = $val;
1592 if( $this->isAnon() ) {
1593 $field = 'user_ip';
1594 $id = $this->getName();
1595 } else {
1596 $field = 'user_id';
1597 $id = $this->getId();
1599 global $wgMemc;
1601 if( $val ) {
1602 $changed = $this->updateNewtalk( $field, $id );
1603 } else {
1604 $changed = $this->deleteNewtalk( $field, $id );
1607 if( $this->isAnon() ) {
1608 // Anons have a separate memcached space, since
1609 // user records aren't kept for them.
1610 $key = wfMemcKey( 'newtalk', 'ip', $id );
1611 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1613 if ( $changed ) {
1614 $this->invalidateCache();
1619 * Generate a current or new-future timestamp to be stored in the
1620 * user_touched field when we update things.
1621 * @return \string Timestamp in TS_MW format
1623 private static function newTouchedTimestamp() {
1624 global $wgClockSkewFudge;
1625 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1629 * Clear user data from memcached.
1630 * Use after applying fun updates to the database; caller's
1631 * responsibility to update user_touched if appropriate.
1633 * Called implicitly from invalidateCache() and saveSettings().
1635 private function clearSharedCache() {
1636 $this->load();
1637 if( $this->mId ) {
1638 global $wgMemc;
1639 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1644 * Immediately touch the user data cache for this account.
1645 * Updates user_touched field, and removes account data from memcached
1646 * for reload on the next hit.
1648 function invalidateCache() {
1649 $this->load();
1650 if( $this->mId ) {
1651 $this->mTouched = self::newTouchedTimestamp();
1653 $dbw = wfGetDB( DB_MASTER );
1654 $dbw->update( 'user',
1655 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1656 array( 'user_id' => $this->mId ),
1657 __METHOD__ );
1659 $this->clearSharedCache();
1664 * Validate the cache for this account.
1665 * @param $timestamp \string A timestamp in TS_MW format
1667 function validateCache( $timestamp ) {
1668 $this->load();
1669 return ($timestamp >= $this->mTouched);
1673 * Get the user touched timestamp
1675 function getTouched() {
1676 $this->load();
1677 return $this->mTouched;
1681 * Set the password and reset the random token.
1682 * Calls through to authentication plugin if necessary;
1683 * will have no effect if the auth plugin refuses to
1684 * pass the change through or if the legal password
1685 * checks fail.
1687 * As a special case, setting the password to null
1688 * wipes it, so the account cannot be logged in until
1689 * a new password is set, for instance via e-mail.
1691 * @param $str \string New password to set
1692 * @throws PasswordError on failure
1694 function setPassword( $str ) {
1695 global $wgAuth;
1697 if( $str !== null ) {
1698 if( !$wgAuth->allowPasswordChange() ) {
1699 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1702 if( !$this->isValidPassword( $str ) ) {
1703 global $wgMinimalPasswordLength;
1704 throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
1705 $wgMinimalPasswordLength ) );
1709 if( !$wgAuth->setPassword( $this, $str ) ) {
1710 throw new PasswordError( wfMsg( 'externaldberror' ) );
1713 $this->setInternalPassword( $str );
1715 return true;
1719 * Set the password and reset the random token unconditionally.
1721 * @param $str \string New password to set
1723 function setInternalPassword( $str ) {
1724 $this->load();
1725 $this->setToken();
1727 if( $str === null ) {
1728 // Save an invalid hash...
1729 $this->mPassword = '';
1730 } else {
1731 $this->mPassword = self::crypt( $str );
1733 $this->mNewpassword = '';
1734 $this->mNewpassTime = null;
1738 * Get the user's current token.
1739 * @return \string Token
1741 function getToken() {
1742 $this->load();
1743 return $this->mToken;
1747 * Set the random token (used for persistent authentication)
1748 * Called from loadDefaults() among other places.
1750 * @param $token \string If specified, set the token to this value
1751 * @private
1753 function setToken( $token = false ) {
1754 global $wgSecretKey, $wgProxyKey;
1755 $this->load();
1756 if ( !$token ) {
1757 if ( $wgSecretKey ) {
1758 $key = $wgSecretKey;
1759 } elseif ( $wgProxyKey ) {
1760 $key = $wgProxyKey;
1761 } else {
1762 $key = microtime();
1764 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1765 } else {
1766 $this->mToken = $token;
1771 * Set the cookie password
1773 * @param $str \string New cookie password
1774 * @private
1776 function setCookiePassword( $str ) {
1777 $this->load();
1778 $this->mCookiePassword = md5( $str );
1782 * Set the password for a password reminder or new account email
1784 * @param $str \string New password to set
1785 * @param $throttle \bool If true, reset the throttle timestamp to the present
1787 function setNewpassword( $str, $throttle = true ) {
1788 $this->load();
1789 $this->mNewpassword = self::crypt( $str );
1790 if ( $throttle ) {
1791 $this->mNewpassTime = wfTimestampNow();
1796 * Has password reminder email been sent within the last
1797 * $wgPasswordReminderResendTime hours?
1798 * @return \bool True or false
1800 function isPasswordReminderThrottled() {
1801 global $wgPasswordReminderResendTime;
1802 $this->load();
1803 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1804 return false;
1806 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1807 return time() < $expiry;
1811 * Get the user's e-mail address
1812 * @return \string User's email address
1814 function getEmail() {
1815 $this->load();
1816 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1817 return $this->mEmail;
1821 * Get the timestamp of the user's e-mail authentication
1822 * @return \string TS_MW timestamp
1824 function getEmailAuthenticationTimestamp() {
1825 $this->load();
1826 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1827 return $this->mEmailAuthenticated;
1831 * Set the user's e-mail address
1832 * @param $str \string New e-mail address
1834 function setEmail( $str ) {
1835 $this->load();
1836 $this->mEmail = $str;
1837 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1841 * Get the user's real name
1842 * @return \string User's real name
1844 function getRealName() {
1845 $this->load();
1846 return $this->mRealName;
1850 * Set the user's real name
1851 * @param $str \string New real name
1853 function setRealName( $str ) {
1854 $this->load();
1855 $this->mRealName = $str;
1859 * Get the user's current setting for a given option.
1861 * @param $oname \string The option to check
1862 * @param $defaultOverride \string A default value returned if the option does not exist
1863 * @return \string User's current value for the option
1864 * @see getBoolOption()
1865 * @see getIntOption()
1867 function getOption( $oname, $defaultOverride = '' ) {
1868 $this->load();
1870 if ( is_null( $this->mOptions ) ) {
1871 if($defaultOverride != '') {
1872 return $defaultOverride;
1874 $this->mOptions = User::getDefaultOptions();
1877 if ( array_key_exists( $oname, $this->mOptions ) ) {
1878 return trim( $this->mOptions[$oname] );
1879 } else {
1880 return $defaultOverride;
1885 * Get the user's current setting for a given option, as a boolean value.
1887 * @param $oname \string The option to check
1888 * @return \bool User's current value for the option
1889 * @see getOption()
1891 function getBoolOption( $oname ) {
1892 return (bool)$this->getOption( $oname );
1897 * Get the user's current setting for a given option, as a boolean value.
1899 * @param $oname \string The option to check
1900 * @param $defaultOverride \int A default value returned if the option does not exist
1901 * @return \int User's current value for the option
1902 * @see getOption()
1904 function getIntOption( $oname, $defaultOverride=0 ) {
1905 $val = $this->getOption( $oname );
1906 if( $val == '' ) {
1907 $val = $defaultOverride;
1909 return intval( $val );
1913 * Set the given option for a user.
1915 * @param $oname \string The option to set
1916 * @param $val \mixed New value to set
1918 function setOption( $oname, $val ) {
1919 $this->load();
1920 if ( is_null( $this->mOptions ) ) {
1921 $this->mOptions = User::getDefaultOptions();
1923 if ( $oname == 'skin' ) {
1924 # Clear cached skin, so the new one displays immediately in Special:Preferences
1925 unset( $this->mSkin );
1927 // Filter out any newlines that may have passed through input validation.
1928 // Newlines are used to separate items in the options blob.
1929 if( $val ) {
1930 $val = str_replace( "\r\n", "\n", $val );
1931 $val = str_replace( "\r", "\n", $val );
1932 $val = str_replace( "\n", " ", $val );
1934 // Explicitly NULL values should refer to defaults
1935 global $wgDefaultUserOptions;
1936 if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
1937 $val = $wgDefaultUserOptions[$oname];
1939 $this->mOptions[$oname] = $val;
1943 * Reset all options to the site defaults
1945 function restoreOptions() {
1946 $this->mOptions = User::getDefaultOptions();
1950 * Get the user's preferred date format.
1951 * @return \string User's preferred date format
1953 function getDatePreference() {
1954 // Important migration for old data rows
1955 if ( is_null( $this->mDatePreference ) ) {
1956 global $wgLang;
1957 $value = $this->getOption( 'date' );
1958 $map = $wgLang->getDatePreferenceMigrationMap();
1959 if ( isset( $map[$value] ) ) {
1960 $value = $map[$value];
1962 $this->mDatePreference = $value;
1964 return $this->mDatePreference;
1968 * Get the permissions this user has.
1969 * @return \type{\arrayof{\string}} Array of permission names
1971 function getRights() {
1972 if ( is_null( $this->mRights ) ) {
1973 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1974 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
1975 // Force reindexation of rights when a hook has unset one of them
1976 $this->mRights = array_values( $this->mRights );
1978 return $this->mRights;
1982 * Get the list of explicit group memberships this user has.
1983 * The implicit * and user groups are not included.
1984 * @return \type{\arrayof{\string}} Array of internal group names
1986 function getGroups() {
1987 $this->load();
1988 return $this->mGroups;
1992 * Get the list of implicit group memberships this user has.
1993 * This includes all explicit groups, plus 'user' if logged in,
1994 * '*' for all accounts and autopromoted groups
1995 * @param $recache \bool Whether to avoid the cache
1996 * @return \type{\arrayof{\string}} Array of internal group names
1998 function getEffectiveGroups( $recache = false ) {
1999 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2000 $this->mEffectiveGroups = $this->getGroups();
2001 $this->mEffectiveGroups[] = '*';
2002 if( $this->getId() ) {
2003 $this->mEffectiveGroups[] = 'user';
2005 $this->mEffectiveGroups = array_unique( array_merge(
2006 $this->mEffectiveGroups,
2007 Autopromote::getAutopromoteGroups( $this )
2008 ) );
2010 # Hook for additional groups
2011 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2014 return $this->mEffectiveGroups;
2018 * Get the user's edit count.
2019 * @return \int User'e edit count
2021 function getEditCount() {
2022 if ($this->mId) {
2023 if ( !isset( $this->mEditCount ) ) {
2024 /* Populate the count, if it has not been populated yet */
2025 $this->mEditCount = User::edits($this->mId);
2027 return $this->mEditCount;
2028 } else {
2029 /* nil */
2030 return null;
2035 * Add the user to the given group.
2036 * This takes immediate effect.
2037 * @param $group \string Name of the group to add
2039 function addGroup( $group ) {
2040 $dbw = wfGetDB( DB_MASTER );
2041 if( $this->getId() ) {
2042 $dbw->insert( 'user_groups',
2043 array(
2044 'ug_user' => $this->getID(),
2045 'ug_group' => $group,
2047 'User::addGroup',
2048 array( 'IGNORE' ) );
2051 $this->loadGroups();
2052 $this->mGroups[] = $group;
2053 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2055 $this->invalidateCache();
2059 * Remove the user from the given group.
2060 * This takes immediate effect.
2061 * @param $group \string Name of the group to remove
2063 function removeGroup( $group ) {
2064 $this->load();
2065 $dbw = wfGetDB( DB_MASTER );
2066 $dbw->delete( 'user_groups',
2067 array(
2068 'ug_user' => $this->getID(),
2069 'ug_group' => $group,
2071 'User::removeGroup' );
2073 $this->loadGroups();
2074 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2075 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2077 $this->invalidateCache();
2082 * Get whether the user is logged in
2083 * @return \bool True or false
2085 function isLoggedIn() {
2086 return $this->getID() != 0;
2090 * Get whether the user is anonymous
2091 * @return \bool True or false
2093 function isAnon() {
2094 return !$this->isLoggedIn();
2098 * Get whether the user is a bot
2099 * @return \bool True or false
2100 * @deprecated
2102 function isBot() {
2103 wfDeprecated( __METHOD__ );
2104 return $this->isAllowed( 'bot' );
2108 * Check if user is allowed to access a feature / make an action
2109 * @param $action \string action to be checked
2110 * @return \bool True if action is allowed, else false
2112 function isAllowed( $action = '' ) {
2113 if ( $action === '' )
2114 return true; // In the spirit of DWIM
2115 # Patrolling may not be enabled
2116 if( $action === 'patrol' || $action === 'autopatrol' ) {
2117 global $wgUseRCPatrol, $wgUseNPPatrol;
2118 if( !$wgUseRCPatrol && !$wgUseNPPatrol )
2119 return false;
2121 # Use strict parameter to avoid matching numeric 0 accidentally inserted
2122 # by misconfiguration: 0 == 'foo'
2123 return in_array( $action, $this->getRights(), true );
2127 * Check whether to enable recent changes patrol features for this user
2128 * @return \bool True or false
2130 public function useRCPatrol() {
2131 global $wgUseRCPatrol;
2132 return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2136 * Check whether to enable new pages patrol features for this user
2137 * @return \bool True or false
2139 public function useNPPatrol() {
2140 global $wgUseRCPatrol, $wgUseNPPatrol;
2141 return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2145 * Get the current skin, loading it if required
2146 * @return \type{Skin} Current skin
2147 * @todo FIXME : need to check the old failback system [AV]
2149 function &getSkin() {
2150 global $wgRequest, $wgAllowUserSkin, $wgDefaultSkin;
2151 if ( ! isset( $this->mSkin ) ) {
2152 wfProfileIn( __METHOD__ );
2154 if( $wgAllowUserSkin ) {
2155 # get the user skin
2156 $userSkin = $this->getOption( 'skin' );
2157 $userSkin = $wgRequest->getVal('useskin', $userSkin);
2158 } else {
2159 # if we're not allowing users to override, then use the default
2160 $userSkin = $wgDefaultSkin;
2163 $this->mSkin =& Skin::newFromKey( $userSkin );
2164 wfProfileOut( __METHOD__ );
2166 return $this->mSkin;
2170 * Check the watched status of an article.
2171 * @param $title \type{Title} Title of the article to look at
2172 * @return \bool True if article is watched
2174 function isWatched( $title ) {
2175 $wl = WatchedItem::fromUserTitle( $this, $title );
2176 return $wl->isWatched();
2180 * Watch an article.
2181 * @param $title \type{Title} Title of the article to look at
2183 function addWatch( $title ) {
2184 $wl = WatchedItem::fromUserTitle( $this, $title );
2185 $wl->addWatch();
2186 $this->invalidateCache();
2190 * Stop watching an article.
2191 * @param $title \type{Title} Title of the article to look at
2193 function removeWatch( $title ) {
2194 $wl = WatchedItem::fromUserTitle( $this, $title );
2195 $wl->removeWatch();
2196 $this->invalidateCache();
2200 * Clear the user's notification timestamp for the given title.
2201 * If e-notif e-mails are on, they will receive notification mails on
2202 * the next change of the page if it's watched etc.
2203 * @param $title \type{Title} Title of the article to look at
2205 function clearNotification( &$title ) {
2206 global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2208 # Do nothing if the database is locked to writes
2209 if( wfReadOnly() ) {
2210 return;
2213 if ($title->getNamespace() == NS_USER_TALK &&
2214 $title->getText() == $this->getName() ) {
2215 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
2216 return;
2217 $this->setNewtalk( false );
2220 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2221 return;
2224 if( $this->isAnon() ) {
2225 // Nothing else to do...
2226 return;
2229 // Only update the timestamp if the page is being watched.
2230 // The query to find out if it is watched is cached both in memcached and per-invocation,
2231 // and when it does have to be executed, it can be on a slave
2232 // If this is the user's newtalk page, we always update the timestamp
2233 if ($title->getNamespace() == NS_USER_TALK &&
2234 $title->getText() == $wgUser->getName())
2236 $watched = true;
2237 } elseif ( $this->getId() == $wgUser->getId() ) {
2238 $watched = $title->userIsWatching();
2239 } else {
2240 $watched = true;
2243 // If the page is watched by the user (or may be watched), update the timestamp on any
2244 // any matching rows
2245 if ( $watched ) {
2246 $dbw = wfGetDB( DB_MASTER );
2247 $dbw->update( 'watchlist',
2248 array( /* SET */
2249 'wl_notificationtimestamp' => NULL
2250 ), array( /* WHERE */
2251 'wl_title' => $title->getDBkey(),
2252 'wl_namespace' => $title->getNamespace(),
2253 'wl_user' => $this->getID()
2254 ), __METHOD__
2260 * Resets all of the given user's page-change notification timestamps.
2261 * If e-notif e-mails are on, they will receive notification mails on
2262 * the next change of any watched page.
2264 * @param $currentUser \int User ID
2266 function clearAllNotifications( $currentUser ) {
2267 global $wgUseEnotif, $wgShowUpdatedMarker;
2268 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2269 $this->setNewtalk( false );
2270 return;
2272 if( $currentUser != 0 ) {
2273 $dbw = wfGetDB( DB_MASTER );
2274 $dbw->update( 'watchlist',
2275 array( /* SET */
2276 'wl_notificationtimestamp' => NULL
2277 ), array( /* WHERE */
2278 'wl_user' => $currentUser
2279 ), __METHOD__
2281 # We also need to clear here the "you have new message" notification for the own user_talk page
2282 # This is cleared one page view later in Article::viewUpdates();
2287 * Encode this user's options as a string
2288 * @return \string Encoded options
2289 * @private
2291 function encodeOptions() {
2292 $this->load();
2293 if ( is_null( $this->mOptions ) ) {
2294 $this->mOptions = User::getDefaultOptions();
2296 $a = array();
2297 foreach ( $this->mOptions as $oname => $oval ) {
2298 array_push( $a, $oname.'='.$oval );
2300 $s = implode( "\n", $a );
2301 return $s;
2305 * Set this user's options from an encoded string
2306 * @param $str \string Encoded options to import
2307 * @private
2309 function decodeOptions( $str ) {
2310 $this->mOptions = array();
2311 $a = explode( "\n", $str );
2312 foreach ( $a as $s ) {
2313 $m = array();
2314 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2315 $this->mOptions[$m[1]] = $m[2];
2321 * Set a cookie on the user's client. Wrapper for
2322 * WebResponse::setCookie
2323 * @param $name \string Name of the cookie to set
2324 * @param $value \string Value to set
2325 * @param $exp \int Expiration time, as a UNIX time value;
2326 * if 0 or not specified, use the default $wgCookieExpiration
2328 protected function setCookie( $name, $value, $exp=0 ) {
2329 global $wgRequest;
2330 $wgRequest->response()->setcookie( $name, $value, $exp );
2334 * Clear a cookie on the user's client
2335 * @param $name \string Name of the cookie to clear
2337 protected function clearCookie( $name ) {
2338 $this->setCookie( $name, '', time() - 86400 );
2342 * Set the default cookies for this session on the user's client.
2344 function setCookies() {
2345 $this->load();
2346 if ( 0 == $this->mId ) return;
2347 $session = array(
2348 'wsUserID' => $this->mId,
2349 'wsToken' => $this->mToken,
2350 'wsUserName' => $this->getName()
2352 $cookies = array(
2353 'UserID' => $this->mId,
2354 'UserName' => $this->getName(),
2356 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2357 $cookies['Token'] = $this->mToken;
2358 } else {
2359 $cookies['Token'] = false;
2362 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2363 #check for null, since the hook could cause a null value
2364 if ( !is_null( $session ) && isset( $_SESSION ) ){
2365 $_SESSION = $session + $_SESSION;
2367 foreach ( $cookies as $name => $value ) {
2368 if ( $value === false ) {
2369 $this->clearCookie( $name );
2370 } else {
2371 $this->setCookie( $name, $value );
2377 * Log this user out.
2379 function logout() {
2380 global $wgUser;
2381 if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
2382 $this->doLogout();
2387 * Clear the user's cookies and session, and reset the instance cache.
2388 * @private
2389 * @see logout()
2391 function doLogout() {
2392 $this->clearInstanceCache( 'defaults' );
2394 $_SESSION['wsUserID'] = 0;
2396 $this->clearCookie( 'UserID' );
2397 $this->clearCookie( 'Token' );
2399 # Remember when user logged out, to prevent seeing cached pages
2400 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2404 * Save this user's settings into the database.
2405 * @todo Only rarely do all these fields need to be set!
2407 function saveSettings() {
2408 $this->load();
2409 if ( wfReadOnly() ) { return; }
2410 if ( 0 == $this->mId ) { return; }
2412 $this->mTouched = self::newTouchedTimestamp();
2414 $dbw = wfGetDB( DB_MASTER );
2415 $dbw->update( 'user',
2416 array( /* SET */
2417 'user_name' => $this->mName,
2418 'user_password' => $this->mPassword,
2419 'user_newpassword' => $this->mNewpassword,
2420 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2421 'user_real_name' => $this->mRealName,
2422 'user_email' => $this->mEmail,
2423 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2424 'user_options' => $this->encodeOptions(),
2425 'user_touched' => $dbw->timestamp($this->mTouched),
2426 'user_token' => $this->mToken,
2427 'user_email_token' => $this->mEmailToken,
2428 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2429 ), array( /* WHERE */
2430 'user_id' => $this->mId
2431 ), __METHOD__
2433 wfRunHooks( 'UserSaveSettings', array( $this ) );
2434 $this->clearSharedCache();
2435 $this->getUserPage()->invalidateCache();
2439 * If only this user's username is known, and it exists, return the user ID.
2441 function idForName() {
2442 $s = trim( $this->getName() );
2443 if ( $s === '' ) return 0;
2445 $dbr = wfGetDB( DB_SLAVE );
2446 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2447 if ( $id === false ) {
2448 $id = 0;
2450 return $id;
2454 * Add a user to the database, return the user object
2456 * @param $name \string Username to add
2457 * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
2458 * - password The user's password. Password logins will be disabled if this is omitted.
2459 * - newpassword A temporary password mailed to the user
2460 * - email The user's email address
2461 * - email_authenticated The email authentication timestamp
2462 * - real_name The user's real name
2463 * - options An associative array of non-default options
2464 * - token Random authentication token. Do not set.
2465 * - registration Registration timestamp. Do not set.
2467 * @return \type{User} A new User object, or null if the username already exists
2469 static function createNew( $name, $params = array() ) {
2470 $user = new User;
2471 $user->load();
2472 if ( isset( $params['options'] ) ) {
2473 $user->mOptions = $params['options'] + $user->mOptions;
2474 unset( $params['options'] );
2476 $dbw = wfGetDB( DB_MASTER );
2477 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2478 $fields = array(
2479 'user_id' => $seqVal,
2480 'user_name' => $name,
2481 'user_password' => $user->mPassword,
2482 'user_newpassword' => $user->mNewpassword,
2483 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2484 'user_email' => $user->mEmail,
2485 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2486 'user_real_name' => $user->mRealName,
2487 'user_options' => $user->encodeOptions(),
2488 'user_token' => $user->mToken,
2489 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2490 'user_editcount' => 0,
2492 foreach ( $params as $name => $value ) {
2493 $fields["user_$name"] = $value;
2495 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2496 if ( $dbw->affectedRows() ) {
2497 $newUser = User::newFromId( $dbw->insertId() );
2498 } else {
2499 $newUser = null;
2501 return $newUser;
2505 * Add this existing user object to the database
2507 function addToDatabase() {
2508 $this->load();
2509 $dbw = wfGetDB( DB_MASTER );
2510 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2511 $dbw->insert( 'user',
2512 array(
2513 'user_id' => $seqVal,
2514 'user_name' => $this->mName,
2515 'user_password' => $this->mPassword,
2516 'user_newpassword' => $this->mNewpassword,
2517 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2518 'user_email' => $this->mEmail,
2519 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2520 'user_real_name' => $this->mRealName,
2521 'user_options' => $this->encodeOptions(),
2522 'user_token' => $this->mToken,
2523 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2524 'user_editcount' => 0,
2525 ), __METHOD__
2527 $this->mId = $dbw->insertId();
2529 // Clear instance cache other than user table data, which is already accurate
2530 $this->clearInstanceCache();
2534 * If this (non-anonymous) user is blocked, block any IP address
2535 * they've successfully logged in from.
2537 function spreadBlock() {
2538 wfDebug( __METHOD__."()\n" );
2539 $this->load();
2540 if ( $this->mId == 0 ) {
2541 return;
2544 $userblock = Block::newFromDB( '', $this->mId );
2545 if ( !$userblock ) {
2546 return;
2549 $userblock->doAutoblock( wfGetIp() );
2554 * Generate a string which will be different for any combination of
2555 * user options which would produce different parser output.
2556 * This will be used as part of the hash key for the parser cache,
2557 * so users will the same options can share the same cached data
2558 * safely.
2560 * Extensions which require it should install 'PageRenderingHash' hook,
2561 * which will give them a chance to modify this key based on their own
2562 * settings.
2564 * @return \string Page rendering hash
2566 function getPageRenderingHash() {
2567 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2568 if( $this->mHash ){
2569 return $this->mHash;
2572 // stubthreshold is only included below for completeness,
2573 // it will always be 0 when this function is called by parsercache.
2575 $confstr = $this->getOption( 'math' );
2576 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2577 if ( $wgUseDynamicDates ) {
2578 $confstr .= '!' . $this->getDatePreference();
2580 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2581 $confstr .= '!' . $wgLang->getCode();
2582 $confstr .= '!' . $this->getOption( 'thumbsize' );
2583 // add in language specific options, if any
2584 $extra = $wgContLang->getExtraHashOptions();
2585 $confstr .= $extra;
2587 $confstr .= $wgRenderHashAppend;
2589 // Give a chance for extensions to modify the hash, if they have
2590 // extra options or other effects on the parser cache.
2591 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2593 // Make it a valid memcached key fragment
2594 $confstr = str_replace( ' ', '_', $confstr );
2595 $this->mHash = $confstr;
2596 return $confstr;
2600 * Get whether the user is explicitly blocked from account creation.
2601 * @return \bool True if blocked
2603 function isBlockedFromCreateAccount() {
2604 $this->getBlockedStatus();
2605 return $this->mBlock && $this->mBlock->mCreateAccount;
2609 * Get whether the user is blocked from using Special:Emailuser.
2610 * @return \bool True if blocked
2612 function isBlockedFromEmailuser() {
2613 $this->getBlockedStatus();
2614 return $this->mBlock && $this->mBlock->mBlockEmail;
2618 * Get whether the user is allowed to create an account.
2619 * @return \bool True if allowed
2621 function isAllowedToCreateAccount() {
2622 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2626 * @deprecated
2628 function setLoaded( $loaded ) {
2629 wfDeprecated( __METHOD__ );
2633 * Get this user's personal page title.
2635 * @return \type{Title} User's personal page title
2637 function getUserPage() {
2638 return Title::makeTitle( NS_USER, $this->getName() );
2642 * Get this user's talk page title.
2644 * @return \type{Title} User's talk page title
2646 function getTalkPage() {
2647 $title = $this->getUserPage();
2648 return $title->getTalkPage();
2652 * Get the maximum valid user ID.
2653 * @return \int User ID
2654 * @static
2656 function getMaxID() {
2657 static $res; // cache
2659 if ( isset( $res ) )
2660 return $res;
2661 else {
2662 $dbr = wfGetDB( DB_SLAVE );
2663 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2668 * Determine whether the user is a newbie. Newbies are either
2669 * anonymous IPs, or the most recently created accounts.
2670 * @return \bool True if the user is a newbie
2672 function isNewbie() {
2673 return !$this->isAllowed( 'autoconfirmed' );
2677 * Is the user active? We check to see if they've made at least
2678 * X number of edits in the last Y days.
2680 * @return \bool True if the user is active, false if not.
2682 public function isActiveEditor() {
2683 global $wgActiveUserEditCount, $wgActiveUserDays;
2684 $dbr = wfGetDB( DB_SLAVE );
2686 // Stolen without shame from RC
2687 $cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
2688 $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
2689 $oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
2691 $res = $dbr->select( 'revision', '1',
2692 array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
2693 __METHOD__,
2694 array('LIMIT' => $wgActiveUserEditCount ) );
2696 $count = $dbr->numRows($res);
2697 $dbr->freeResult($res);
2699 return $count == $wgActiveUserEditCount;
2703 * Check to see if the given clear-text password is one of the accepted passwords
2704 * @param $password \string user password.
2705 * @return \bool True if the given password is correct, otherwise False.
2707 function checkPassword( $password ) {
2708 global $wgAuth;
2709 $this->load();
2711 // Even though we stop people from creating passwords that
2712 // are shorter than this, doesn't mean people wont be able
2713 // to. Certain authentication plugins do NOT want to save
2714 // domain passwords in a mysql database, so we should
2715 // check this (incase $wgAuth->strict() is false).
2716 if( !$this->isValidPassword( $password ) ) {
2717 return false;
2720 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2721 return true;
2722 } elseif( $wgAuth->strict() ) {
2723 /* Auth plugin doesn't allow local authentication */
2724 return false;
2725 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2726 /* Auth plugin doesn't allow local authentication for this user name */
2727 return false;
2729 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2730 return true;
2731 } elseif ( function_exists( 'iconv' ) ) {
2732 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2733 # Check for this with iconv
2734 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2735 if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2736 return true;
2739 return false;
2743 * Check if the given clear-text password matches the temporary password
2744 * sent by e-mail for password reset operations.
2745 * @return \bool True if matches, false otherwise
2747 function checkTemporaryPassword( $plaintext ) {
2748 global $wgNewPasswordExpiry;
2749 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
2750 $this->load();
2751 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
2752 return ( time() < $expiry );
2753 } else {
2754 return false;
2759 * Initialize (if necessary) and return a session token value
2760 * which can be used in edit forms to show that the user's
2761 * login credentials aren't being hijacked with a foreign form
2762 * submission.
2764 * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
2765 * @return \string The new edit token
2767 function editToken( $salt = '' ) {
2768 if ( $this->isAnon() ) {
2769 return EDIT_TOKEN_SUFFIX;
2770 } else {
2771 if( !isset( $_SESSION['wsEditToken'] ) ) {
2772 $token = $this->generateToken();
2773 $_SESSION['wsEditToken'] = $token;
2774 } else {
2775 $token = $_SESSION['wsEditToken'];
2777 if( is_array( $salt ) ) {
2778 $salt = implode( '|', $salt );
2780 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2785 * Generate a looking random token for various uses.
2787 * @param $salt \string Optional salt value
2788 * @return \string The new random token
2790 function generateToken( $salt = '' ) {
2791 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2792 return md5( $token . $salt );
2796 * Check given value against the token value stored in the session.
2797 * A match should confirm that the form was submitted from the
2798 * user's own login session, not a form submission from a third-party
2799 * site.
2801 * @param $val \string Input value to compare
2802 * @param $salt \string Optional function-specific data for hashing
2803 * @return \bool Whether the token matches
2805 function matchEditToken( $val, $salt = '' ) {
2806 $sessionToken = $this->editToken( $salt );
2807 if ( $val != $sessionToken ) {
2808 wfDebug( "User::matchEditToken: broken session data\n" );
2810 return $val == $sessionToken;
2814 * Check given value against the token value stored in the session,
2815 * ignoring the suffix.
2817 * @param $val \string Input value to compare
2818 * @param $salt \string Optional function-specific data for hashing
2819 * @return \bool Whether the token matches
2821 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2822 $sessionToken = $this->editToken( $salt );
2823 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2827 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2828 * mail to the user's given address.
2830 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
2832 function sendConfirmationMail() {
2833 global $wgLang;
2834 $expiration = null; // gets passed-by-ref and defined in next line.
2835 $token = $this->confirmationToken( $expiration );
2836 $url = $this->confirmationTokenUrl( $token );
2837 $invalidateURL = $this->invalidationTokenUrl( $token );
2838 $this->saveSettings();
2840 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2841 wfMsg( 'confirmemail_body',
2842 wfGetIP(),
2843 $this->getName(),
2844 $url,
2845 $wgLang->timeanddate( $expiration, false ),
2846 $invalidateURL ) );
2850 * Send an e-mail to this user's account. Does not check for
2851 * confirmed status or validity.
2853 * @param $subject \string Message subject
2854 * @param $body \string Message body
2855 * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
2856 * @param $replyto \string Reply-To address
2857 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
2859 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2860 if( is_null( $from ) ) {
2861 global $wgPasswordSender;
2862 $from = $wgPasswordSender;
2865 $to = new MailAddress( $this );
2866 $sender = new MailAddress( $from );
2867 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2871 * Generate, store, and return a new e-mail confirmation code.
2872 * A hash (unsalted, since it's used as a key) is stored.
2874 * @note Call saveSettings() after calling this function to commit
2875 * this change to the database.
2877 * @param[out] &$expiration \mixed Accepts the expiration time
2878 * @return \string New token
2879 * @private
2881 function confirmationToken( &$expiration ) {
2882 $now = time();
2883 $expires = $now + 7 * 24 * 60 * 60;
2884 $expiration = wfTimestamp( TS_MW, $expires );
2885 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2886 $hash = md5( $token );
2887 $this->load();
2888 $this->mEmailToken = $hash;
2889 $this->mEmailTokenExpires = $expiration;
2890 return $token;
2894 * Return a URL the user can use to confirm their email address.
2895 * @param $token \string Accepts the email confirmation token
2896 * @return \string New token URL
2897 * @private
2899 function confirmationTokenUrl( $token ) {
2900 return $this->getTokenUrl( 'ConfirmEmail', $token );
2903 * Return a URL the user can use to invalidate their email address.
2904 * @param $token \string Accepts the email confirmation token
2905 * @return \string New token URL
2906 * @private
2908 function invalidationTokenUrl( $token ) {
2909 return $this->getTokenUrl( 'Invalidateemail', $token );
2913 * Internal function to format the e-mail validation/invalidation URLs.
2914 * This uses $wgArticlePath directly as a quickie hack to use the
2915 * hardcoded English names of the Special: pages, for ASCII safety.
2917 * @note Since these URLs get dropped directly into emails, using the
2918 * short English names avoids insanely long URL-encoded links, which
2919 * also sometimes can get corrupted in some browsers/mailers
2920 * (bug 6957 with Gmail and Internet Explorer).
2922 * @param $page \string Special page
2923 * @param $token \string Token
2924 * @return \string Formatted URL
2926 protected function getTokenUrl( $page, $token ) {
2927 global $wgArticlePath;
2928 return wfExpandUrl(
2929 str_replace(
2930 '$1',
2931 "Special:$page/$token",
2932 $wgArticlePath ) );
2936 * Mark the e-mail address confirmed.
2938 * @note Call saveSettings() after calling this function to commit the change.
2940 function confirmEmail() {
2941 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2942 return true;
2946 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2947 * address if it was already confirmed.
2949 * @note Call saveSettings() after calling this function to commit the change.
2951 function invalidateEmail() {
2952 $this->load();
2953 $this->mEmailToken = null;
2954 $this->mEmailTokenExpires = null;
2955 $this->setEmailAuthenticationTimestamp( null );
2956 return true;
2960 * Set the e-mail authentication timestamp.
2961 * @param $timestamp \string TS_MW timestamp
2963 function setEmailAuthenticationTimestamp( $timestamp ) {
2964 $this->load();
2965 $this->mEmailAuthenticated = $timestamp;
2966 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2970 * Is this user allowed to send e-mails within limits of current
2971 * site configuration?
2972 * @return \bool True if allowed
2974 function canSendEmail() {
2975 global $wgEnableEmail, $wgEnableUserEmail;
2976 if( !$wgEnableEmail || !$wgEnableUserEmail ) {
2977 return false;
2979 $canSend = $this->isEmailConfirmed();
2980 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2981 return $canSend;
2985 * Is this user allowed to receive e-mails within limits of current
2986 * site configuration?
2987 * @return \bool True if allowed
2989 function canReceiveEmail() {
2990 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
2994 * Is this user's e-mail address valid-looking and confirmed within
2995 * limits of the current site configuration?
2997 * @note If $wgEmailAuthentication is on, this may require the user to have
2998 * confirmed their address by returning a code or using a password
2999 * sent to the address from the wiki.
3001 * @return \bool True if confirmed
3003 function isEmailConfirmed() {
3004 global $wgEmailAuthentication;
3005 $this->load();
3006 $confirmed = true;
3007 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
3008 if( $this->isAnon() )
3009 return false;
3010 if( !self::isValidEmailAddr( $this->mEmail ) )
3011 return false;
3012 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
3013 return false;
3014 return true;
3015 } else {
3016 return $confirmed;
3021 * Check whether there is an outstanding request for e-mail confirmation.
3022 * @return \bool True if pending
3024 function isEmailConfirmationPending() {
3025 global $wgEmailAuthentication;
3026 return $wgEmailAuthentication &&
3027 !$this->isEmailConfirmed() &&
3028 $this->mEmailToken &&
3029 $this->mEmailTokenExpires > wfTimestamp();
3033 * Get the timestamp of account creation.
3035 * @return \types{\string,\bool} string Timestamp of account creation, or false for
3036 * non-existent/anonymous user accounts.
3038 public function getRegistration() {
3039 return $this->getId() > 0
3040 ? $this->mRegistration
3041 : false;
3045 * Get the timestamp of the first edit
3047 * @return \types{\string,\bool} string Timestamp of first edit, or false for
3048 * non-existent/anonymous user accounts.
3050 public function getFirstEditTimestamp() {
3051 if( $this->getId() == 0 ) return false; // anons
3052 $dbr = wfGetDB( DB_SLAVE );
3053 $time = $dbr->selectField( 'revision', 'rev_timestamp',
3054 array( 'rev_user' => $this->getId() ),
3055 __METHOD__,
3056 array( 'ORDER BY' => 'rev_timestamp ASC' )
3058 if( !$time ) return false; // no edits
3059 return wfTimestamp( TS_MW, $time );
3063 * Get the permissions associated with a given list of groups
3065 * @param $groups \type{\arrayof{\string}} List of internal group names
3066 * @return \type{\arrayof{\string}} List of permission key names for given groups combined
3068 static function getGroupPermissions( $groups ) {
3069 global $wgGroupPermissions;
3070 $rights = array();
3071 foreach( $groups as $group ) {
3072 if( isset( $wgGroupPermissions[$group] ) ) {
3073 $rights = array_merge( $rights,
3074 // array_filter removes empty items
3075 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
3078 return array_unique($rights);
3082 * Get all the groups who have a given permission
3084 * @param $role \string Role to check
3085 * @return \type{\arrayof{\string}} List of internal group names with the given permission
3087 static function getGroupsWithPermission( $role ) {
3088 global $wgGroupPermissions;
3089 $allowedGroups = array();
3090 foreach ( $wgGroupPermissions as $group => $rights ) {
3091 if ( isset( $rights[$role] ) && $rights[$role] ) {
3092 $allowedGroups[] = $group;
3095 return $allowedGroups;
3099 * Get the localized descriptive name for a group, if it exists
3101 * @param $group \string Internal group name
3102 * @return \string Localized descriptive group name
3104 static function getGroupName( $group ) {
3105 global $wgMessageCache;
3106 $wgMessageCache->loadAllMessages();
3107 $key = "group-$group";
3108 $name = wfMsg( $key );
3109 return $name == '' || wfEmptyMsg( $key, $name )
3110 ? $group
3111 : $name;
3115 * Get the localized descriptive name for a member of a group, if it exists
3117 * @param $group \string Internal group name
3118 * @return \string Localized name for group member
3120 static function getGroupMember( $group ) {
3121 global $wgMessageCache;
3122 $wgMessageCache->loadAllMessages();
3123 $key = "group-$group-member";
3124 $name = wfMsg( $key );
3125 return $name == '' || wfEmptyMsg( $key, $name )
3126 ? $group
3127 : $name;
3131 * Return the set of defined explicit groups.
3132 * The implicit groups (by default *, 'user' and 'autoconfirmed')
3133 * are not included, as they are defined automatically, not in the database.
3134 * @return \type{\arrayof{\string}} Array of internal group names
3136 static function getAllGroups() {
3137 global $wgGroupPermissions;
3138 return array_diff(
3139 array_keys( $wgGroupPermissions ),
3140 self::getImplicitGroups()
3145 * Get a list of all available permissions.
3146 * @return \type{\arrayof{\string}} Array of permission names
3148 static function getAllRights() {
3149 if ( self::$mAllRights === false ) {
3150 global $wgAvailableRights;
3151 if ( count( $wgAvailableRights ) ) {
3152 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3153 } else {
3154 self::$mAllRights = self::$mCoreRights;
3156 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3158 return self::$mAllRights;
3162 * Get a list of implicit groups
3163 * @return \type{\arrayof{\string}} Array of internal group names
3165 public static function getImplicitGroups() {
3166 global $wgImplicitGroups;
3167 $groups = $wgImplicitGroups;
3168 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
3169 return $groups;
3173 * Get the title of a page describing a particular group
3175 * @param $group \string Internal group name
3176 * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
3178 static function getGroupPage( $group ) {
3179 global $wgMessageCache;
3180 $wgMessageCache->loadAllMessages();
3181 $page = wfMsgForContent( 'grouppage-' . $group );
3182 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3183 $title = Title::newFromText( $page );
3184 if( is_object( $title ) )
3185 return $title;
3187 return false;
3191 * Create a link to the group in HTML, if available;
3192 * else return the group name.
3194 * @param $group \string Internal name of the group
3195 * @param $text \string The text of the link
3196 * @return \string HTML link to the group
3198 static function makeGroupLinkHTML( $group, $text = '' ) {
3199 if( $text == '' ) {
3200 $text = self::getGroupName( $group );
3202 $title = self::getGroupPage( $group );
3203 if( $title ) {
3204 global $wgUser;
3205 $sk = $wgUser->getSkin();
3206 return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
3207 } else {
3208 return $text;
3213 * Create a link to the group in Wikitext, if available;
3214 * else return the group name.
3216 * @param $group \string Internal name of the group
3217 * @param $text \string The text of the link
3218 * @return \string Wikilink to the group
3220 static function makeGroupLinkWiki( $group, $text = '' ) {
3221 if( $text == '' ) {
3222 $text = self::getGroupName( $group );
3224 $title = self::getGroupPage( $group );
3225 if( $title ) {
3226 $page = $title->getPrefixedText();
3227 return "[[$page|$text]]";
3228 } else {
3229 return $text;
3234 * Increment the user's edit-count field.
3235 * Will have no effect for anonymous users.
3237 function incEditCount() {
3238 if( !$this->isAnon() ) {
3239 $dbw = wfGetDB( DB_MASTER );
3240 $dbw->update( 'user',
3241 array( 'user_editcount=user_editcount+1' ),
3242 array( 'user_id' => $this->getId() ),
3243 __METHOD__ );
3245 // Lazy initialization check...
3246 if( $dbw->affectedRows() == 0 ) {
3247 // Pull from a slave to be less cruel to servers
3248 // Accuracy isn't the point anyway here
3249 $dbr = wfGetDB( DB_SLAVE );
3250 $count = $dbr->selectField( 'revision',
3251 'COUNT(rev_user)',
3252 array( 'rev_user' => $this->getId() ),
3253 __METHOD__ );
3255 // Now here's a goddamn hack...
3256 if( $dbr !== $dbw ) {
3257 // If we actually have a slave server, the count is
3258 // at least one behind because the current transaction
3259 // has not been committed and replicated.
3260 $count++;
3261 } else {
3262 // But if DB_SLAVE is selecting the master, then the
3263 // count we just read includes the revision that was
3264 // just added in the working transaction.
3267 $dbw->update( 'user',
3268 array( 'user_editcount' => $count ),
3269 array( 'user_id' => $this->getId() ),
3270 __METHOD__ );
3273 // edit count in user cache too
3274 $this->invalidateCache();
3278 * Get the description of a given right
3280 * @param $right \string Right to query
3281 * @return \string Localized description of the right
3283 static function getRightDescription( $right ) {
3284 global $wgMessageCache;
3285 $wgMessageCache->loadAllMessages();
3286 $key = "right-$right";
3287 $name = wfMsg( $key );
3288 return $name == '' || wfEmptyMsg( $key, $name )
3289 ? $right
3290 : $name;
3294 * Make an old-style password hash
3296 * @param $password \string Plain-text password
3297 * @param $userId \string User ID
3298 * @return \string Password hash
3300 static function oldCrypt( $password, $userId ) {
3301 global $wgPasswordSalt;
3302 if ( $wgPasswordSalt ) {
3303 return md5( $userId . '-' . md5( $password ) );
3304 } else {
3305 return md5( $password );
3310 * Make a new-style password hash
3312 * @param $password \string Plain-text password
3313 * @param $salt \string Optional salt, may be random or the user ID.
3314 * If unspecified or false, will generate one automatically
3315 * @return \string Password hash
3317 static function crypt( $password, $salt = false ) {
3318 global $wgPasswordSalt;
3320 $hash = '';
3321 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
3322 return $hash;
3325 if( $wgPasswordSalt ) {
3326 if ( $salt === false ) {
3327 $salt = substr( wfGenerateToken(), 0, 8 );
3329 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3330 } else {
3331 return ':A:' . md5( $password );
3336 * Compare a password hash with a plain-text password. Requires the user
3337 * ID if there's a chance that the hash is an old-style hash.
3339 * @param $hash \string Password hash
3340 * @param $password \string Plain-text password to compare
3341 * @param $userId \string User ID for old-style password salt
3342 * @return \bool
3344 static function comparePasswords( $hash, $password, $userId = false ) {
3345 $m = false;
3346 $type = substr( $hash, 0, 3 );
3348 $result = false;
3349 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
3350 return $result;
3353 if ( $type == ':A:' ) {
3354 # Unsalted
3355 return md5( $password ) === substr( $hash, 3 );
3356 } elseif ( $type == ':B:' ) {
3357 # Salted
3358 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3359 return md5( $salt.'-'.md5( $password ) ) == $realHash;
3360 } else {
3361 # Old-style
3362 return self::oldCrypt( $password, $userId ) === $hash;
3367 * Add a newuser log entry for this user
3368 * @param $byEmail Boolean: account made by email?
3370 public function addNewUserLogEntry( $byEmail = false ) {
3371 global $wgUser, $wgContLang, $wgNewUserLog;
3372 if( empty($wgNewUserLog) ) {
3373 return true; // disabled
3375 $talk = $wgContLang->getFormattedNsText( NS_TALK );
3376 if( $this->getName() == $wgUser->getName() ) {
3377 $action = 'create';
3378 $message = '';
3379 } else {
3380 $action = 'create2';
3381 $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
3383 $log = new LogPage( 'newusers' );
3384 $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
3385 return true;
3389 * Add an autocreate newuser log entry for this user
3390 * Used by things like CentralAuth and perhaps other authplugins.
3392 public function addNewUserLogEntryAutoCreate() {
3393 global $wgNewUserLog;
3394 if( empty($wgNewUserLog) ) {
3395 return true; // disabled
3397 $log = new LogPage( 'newusers', false );
3398 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3399 return true;