(bug 18533) Add readonly reason to readonly exception
[mediawiki.git] / includes / User.php
blobd3341f8167e98b577f65a4da2b6db1d69a3b4794
1 <?php
2 /**
3 * Implements the User class for the %MediaWiki software.
4 * @file
5 */
7 /**
8 * \int Number of characters in user_token field.
9 * @ingroup Constants
11 define( 'USER_TOKEN_LENGTH', 32 );
13 /**
14 * \int Serialized record version.
15 * @ingroup Constants
17 define( 'MW_USER_VERSION', 8 );
19 /**
20 * \string Some punctuation to prevent editing from broken text-mangling proxies.
21 * @ingroup Constants
23 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
25 /**
26 * Thrown by User::setPassword() on error.
27 * @ingroup Exception
29 class PasswordError extends MWException {
30 // NOP
33 /**
34 * The User object encapsulates all of the user-specific settings (user_id,
35 * name, rights, password, email address, options, last login time). Client
36 * classes use the getXXX() functions to access these fields. These functions
37 * do all the work of determining whether the user is logged in,
38 * whether the requested option can be satisfied from cookies or
39 * whether a database query is needed. Most of the settings needed
40 * for rendering normal pages are set in the cookie to minimize use
41 * of the database.
43 class User {
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 'mTouched',
113 'mToken',
114 'mEmailAuthenticated',
115 'mEmailToken',
116 'mEmailTokenExpires',
117 'mRegistration',
118 'mEditCount',
119 // user_group table
120 'mGroups',
121 // user_properties table
122 'mOptionOverrides',
126 * \type{\arrayof{\string}} Core rights.
127 * Each of these should have a corresponding message of the form
128 * "right-$right".
129 * @showinitializer
131 static $mCoreRights = array(
132 'apihighlimits',
133 'autoconfirmed',
134 'autopatrol',
135 'bigdelete',
136 'block',
137 'blockemail',
138 'bot',
139 'browsearchive',
140 'createaccount',
141 'createpage',
142 'createtalk',
143 'delete',
144 'deletedhistory',
145 'deleterevision',
146 'edit',
147 'editinterface',
148 'editusercssjs',
149 'hideuser',
150 'import',
151 'importupload',
152 'ipblock-exempt',
153 'markbotedits',
154 'minoredit',
155 'move',
156 'movefile',
157 'move-rootuserpages',
158 'move-subpages',
159 'nominornewtalk',
160 'noratelimit',
161 'override-export-depth',
162 'patrol',
163 'protect',
164 'proxyunbannable',
165 'purge',
166 'read',
167 'reupload',
168 'reupload-shared',
169 'rollback',
170 'siteadmin',
171 'suppressionlog',
172 'suppressredirect',
173 'suppressrevision',
174 'trackback',
175 'undelete',
176 'unwatchedpages',
177 'upload',
178 'upload_by_url',
179 'userrights',
180 'userrights-interwiki',
181 'writeapi',
184 * \string Cached results of getAllRights()
186 static $mAllRights = false;
188 /** @name Cache variables */
189 //@{
190 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
191 $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
192 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides;
193 //@}
196 * \bool Whether the cache variables have been loaded.
198 var $mDataLoaded, $mAuthLoaded, $mOptionsLoaded;
201 * \string Initialization data source if mDataLoaded==false. May be one of:
202 * - 'defaults' anonymous user initialised from class defaults
203 * - 'name' initialise from mName
204 * - 'id' initialise from mId
205 * - 'session' log in from cookies or session if possible
207 * Use the User::newFrom*() family of functions to set this.
209 var $mFrom;
211 /** @name Lazy-initialized variables, invalidated with clearInstanceCache */
212 //@{
213 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
214 $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
215 $mLocked, $mHideName, $mOptions;
216 //@}
219 * Lightweight constructor for an anonymous user.
220 * Use the User::newFrom* factory functions for other kinds of users.
222 * @see newFromName()
223 * @see newFromId()
224 * @see newFromConfirmationCode()
225 * @see newFromSession()
226 * @see newFromRow()
228 function User() {
229 $this->clearInstanceCache( 'defaults' );
233 * Load the user table data for this object from the source given by mFrom.
235 function load() {
236 if ( $this->mDataLoaded ) {
237 return;
239 wfProfileIn( __METHOD__ );
241 # Set it now to avoid infinite recursion in accessors
242 $this->mDataLoaded = true;
244 switch ( $this->mFrom ) {
245 case 'defaults':
246 $this->loadDefaults();
247 break;
248 case 'name':
249 $this->mId = self::idFromName( $this->mName );
250 if ( !$this->mId ) {
251 # Nonexistent user placeholder object
252 $this->loadDefaults( $this->mName );
253 } else {
254 $this->loadFromId();
256 break;
257 case 'id':
258 $this->loadFromId();
259 break;
260 case 'session':
261 $this->loadFromSession();
262 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
263 break;
264 default:
265 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
267 wfProfileOut( __METHOD__ );
271 * Load user table data, given mId has already been set.
272 * @return \bool false if the ID does not exist, true otherwise
273 * @private
275 function loadFromId() {
276 global $wgMemc;
277 if ( $this->mId == 0 ) {
278 $this->loadDefaults();
279 return false;
282 # Try cache
283 $key = wfMemcKey( 'user', 'id', $this->mId );
284 $data = $wgMemc->get( $key );
285 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
286 # Object is expired, load from DB
287 $data = false;
290 if ( !$data ) {
291 wfDebug( "Cache miss for user {$this->mId}\n" );
292 # Load from DB
293 if ( !$this->loadFromDatabase() ) {
294 # Can't load from ID, user is anonymous
295 return false;
297 $this->saveToCache();
298 } else {
299 wfDebug( "Got user {$this->mId} from cache\n" );
300 # Restore from cache
301 foreach ( self::$mCacheVars as $name ) {
302 $this->$name = $data[$name];
305 return true;
309 * Save user data to the shared cache
311 function saveToCache() {
312 $this->load();
313 $this->loadGroups();
314 $this->loadOptions();
315 if ( $this->isAnon() ) {
316 // Anonymous users are uncached
317 return;
319 $data = array();
320 foreach ( self::$mCacheVars as $name ) {
321 $data[$name] = $this->$name;
323 $data['mVersion'] = MW_USER_VERSION;
324 $key = wfMemcKey( 'user', 'id', $this->mId );
325 global $wgMemc;
326 $wgMemc->set( $key, $data );
330 /** @name newFrom*() static factory methods */
331 //@{
334 * Static factory method for creation from username.
336 * This is slightly less efficient than newFromId(), so use newFromId() if
337 * you have both an ID and a name handy.
339 * @param $name \string Username, validated by Title::newFromText()
340 * @param $validate \mixed Validate username. Takes the same parameters as
341 * User::getCanonicalName(), except that true is accepted as an alias
342 * for 'valid', for BC.
344 * @return \type{User} The User object, or null if the username is invalid. If the
345 * username is not present in the database, the result will be a user object
346 * with a name, zero user ID and default settings.
348 static function newFromName( $name, $validate = 'valid' ) {
349 if ( $validate === true ) {
350 $validate = 'valid';
352 $name = self::getCanonicalName( $name, $validate );
353 if ( $name === false ) {
354 return null;
355 } else {
356 # Create unloaded user object
357 $u = new User;
358 $u->mName = $name;
359 $u->mFrom = 'name';
360 return $u;
365 * Static factory method for creation from a given user ID.
367 * @param $id \int Valid user ID
368 * @return \type{User} The corresponding User object
370 static function newFromId( $id ) {
371 $u = new User;
372 $u->mId = $id;
373 $u->mFrom = 'id';
374 return $u;
378 * Factory method to fetch whichever user has a given email confirmation code.
379 * This code is generated when an account is created or its e-mail address
380 * has changed.
382 * If the code is invalid or has expired, returns NULL.
384 * @param $code \string Confirmation code
385 * @return \type{User}
387 static function newFromConfirmationCode( $code ) {
388 $dbr = wfGetDB( DB_SLAVE );
389 $id = $dbr->selectField( 'user', 'user_id', array(
390 'user_email_token' => md5( $code ),
391 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
392 ) );
393 if( $id !== false ) {
394 return User::newFromId( $id );
395 } else {
396 return null;
401 * Create a new user object using data from session or cookies. If the
402 * login credentials are invalid, the result is an anonymous user.
404 * @return \type{User}
406 static function newFromSession() {
407 $user = new User;
408 $user->mFrom = 'session';
409 return $user;
413 * Create a new user object from a user row.
414 * The row should have all fields from the user table in it.
415 * @param $row array A row from the user table
416 * @return \type{User}
418 static function newFromRow( $row ) {
419 $user = new User;
420 $user->loadFromRow( $row );
421 return $user;
424 //@}
428 * Get the username corresponding to a given user ID
429 * @param $id \int User ID
430 * @return \string The corresponding username
432 static function whoIs( $id ) {
433 $dbr = wfGetDB( DB_SLAVE );
434 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
438 * Get the real name of a user given their user ID
440 * @param $id \int User ID
441 * @return \string The corresponding user's real name
443 static function whoIsReal( $id ) {
444 $dbr = wfGetDB( DB_SLAVE );
445 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
449 * Get database id given a user name
450 * @param $name \string Username
451 * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
453 static function idFromName( $name ) {
454 $nt = Title::makeTitleSafe( NS_USER, $name );
455 if( is_null( $nt ) ) {
456 # Illegal name
457 return null;
460 static $cache = array();
462 if ( isset($cache[$name]) ) {
463 return $cache[$name];
466 $dbr = wfGetDB( DB_SLAVE );
467 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
469 if ( $s === false ) {
470 $result = 0;
471 } else {
472 $result = $s->user_id;
475 $cache[$name] = $result;
477 if ( count($cache) > 1000 ) {
478 $cache = array();
481 return $result;
485 * Does the string match an anonymous IPv4 address?
487 * This function exists for username validation, in order to reject
488 * usernames which are similar in form to IP addresses. Strings such
489 * as 300.300.300.300 will return true because it looks like an IP
490 * address, despite not being strictly valid.
492 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
493 * address because the usemod software would "cloak" anonymous IP
494 * addresses like this, if we allowed accounts like this to be created
495 * new users could get the old edits of these anonymous users.
497 * @param $name \string String to match
498 * @return \bool True or false
500 static function isIP( $name ) {
501 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
505 * Is the input a valid username?
507 * Checks if the input is a valid username, we don't want an empty string,
508 * an IP address, anything that containins slashes (would mess up subpages),
509 * is longer than the maximum allowed username size or doesn't begin with
510 * a capital letter.
512 * @param $name \string String to match
513 * @return \bool True or false
515 static function isValidUserName( $name ) {
516 global $wgContLang, $wgMaxNameChars;
518 if ( $name == ''
519 || User::isIP( $name )
520 || strpos( $name, '/' ) !== false
521 || strlen( $name ) > $wgMaxNameChars
522 || $name != $wgContLang->ucfirst( $name ) ) {
523 wfDebugLog( 'username', __METHOD__ .
524 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
525 return false;
528 // Ensure that the name can't be misresolved as a different title,
529 // such as with extra namespace keys at the start.
530 $parsed = Title::newFromText( $name );
531 if( is_null( $parsed )
532 || $parsed->getNamespace()
533 || strcmp( $name, $parsed->getPrefixedText() ) ) {
534 wfDebugLog( 'username', __METHOD__ .
535 ": '$name' invalid due to ambiguous prefixes" );
536 return false;
539 // Check an additional blacklist of troublemaker characters.
540 // Should these be merged into the title char list?
541 $unicodeBlacklist = '/[' .
542 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
543 '\x{00a0}' . # non-breaking space
544 '\x{2000}-\x{200f}' . # various whitespace
545 '\x{2028}-\x{202f}' . # breaks and control chars
546 '\x{3000}' . # ideographic space
547 '\x{e000}-\x{f8ff}' . # private use
548 ']/u';
549 if( preg_match( $unicodeBlacklist, $name ) ) {
550 wfDebugLog( 'username', __METHOD__ .
551 ": '$name' invalid due to blacklisted characters" );
552 return false;
555 return true;
559 * Usernames which fail to pass this function will be blocked
560 * from user login and new account registrations, but may be used
561 * internally by batch processes.
563 * If an account already exists in this form, login will be blocked
564 * by a failure to pass this function.
566 * @param $name \string String to match
567 * @return \bool True or false
569 static function isUsableName( $name ) {
570 global $wgReservedUsernames;
571 // Must be a valid username, obviously ;)
572 if ( !self::isValidUserName( $name ) ) {
573 return false;
576 static $reservedUsernames = false;
577 if ( !$reservedUsernames ) {
578 $reservedUsernames = $wgReservedUsernames;
579 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
582 // Certain names may be reserved for batch processes.
583 foreach ( $reservedUsernames as $reserved ) {
584 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
585 $reserved = wfMsgForContent( substr( $reserved, 4 ) );
587 if ( $reserved == $name ) {
588 return false;
591 return true;
595 * Usernames which fail to pass this function will be blocked
596 * from new account registrations, but may be used internally
597 * either by batch processes or by user accounts which have
598 * already been created.
600 * Additional character blacklisting may be added here
601 * rather than in isValidUserName() to avoid disrupting
602 * existing accounts.
604 * @param $name \string String to match
605 * @return \bool True or false
607 static function isCreatableName( $name ) {
608 global $wgInvalidUsernameCharacters;
609 return
610 self::isUsableName( $name ) &&
612 // Registration-time character blacklisting...
613 !preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name );
617 * Is the input a valid password for this user?
619 * @param $password String Desired password
620 * @return mixed: true on success, string of error message on failure
622 function isValidPassword( $password ) {
623 global $wgMinimalPasswordLength, $wgContLang;
625 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
626 return $result;
628 // Password needs to be long enough
629 if( strlen( $password ) < $wgMinimalPasswordLength ) {
630 return 'passwordtooshort';
631 } elseif( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
632 return 'password-name-match';
633 } else {
634 return true;
639 * Does a string look like an e-mail address?
641 * There used to be a regular expression here, it got removed because it
642 * rejected valid addresses. Actually just check if there is '@' somewhere
643 * in the given address.
645 * @todo Check for RFC 2822 compilance (bug 959)
647 * @param $addr \string E-mail address
648 * @return \bool True or false
650 public static function isValidEmailAddr( $addr ) {
651 $result = null;
652 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
653 return $result;
656 return strpos( $addr, '@' ) !== false;
660 * Given unvalidated user input, return a canonical username, or false if
661 * the username is invalid.
662 * @param $name \string User input
663 * @param $validate \types{\string,\bool} Type of validation to use:
664 * - false No validation
665 * - 'valid' Valid for batch processes
666 * - 'usable' Valid for batch processes and login
667 * - 'creatable' Valid for batch processes, login and account creation
669 static function getCanonicalName( $name, $validate = 'valid' ) {
670 # Force usernames to capital
671 global $wgContLang;
672 $name = $wgContLang->ucfirst( $name );
674 # Reject names containing '#'; these will be cleaned up
675 # with title normalisation, but then it's too late to
676 # check elsewhere
677 if( strpos( $name, '#' ) !== false )
678 return false;
680 # Clean up name according to title rules
681 $t = ($validate === 'valid') ?
682 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
683 # Check for invalid titles
684 if( is_null( $t ) ) {
685 return false;
688 # Reject various classes of invalid names
689 $name = $t->getText();
690 global $wgAuth;
691 $name = $wgAuth->getCanonicalName( $t->getText() );
693 switch ( $validate ) {
694 case false:
695 break;
696 case 'valid':
697 if ( !User::isValidUserName( $name ) ) {
698 $name = false;
700 break;
701 case 'usable':
702 if ( !User::isUsableName( $name ) ) {
703 $name = false;
705 break;
706 case 'creatable':
707 if ( !User::isCreatableName( $name ) ) {
708 $name = false;
710 break;
711 default:
712 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
714 return $name;
718 * Count the number of edits of a user
719 * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
721 * @param $uid \int User ID to check
722 * @return \int The user's edit count
724 static function edits( $uid ) {
725 wfProfileIn( __METHOD__ );
726 $dbr = wfGetDB( DB_SLAVE );
727 // check if the user_editcount field has been initialized
728 $field = $dbr->selectField(
729 'user', 'user_editcount',
730 array( 'user_id' => $uid ),
731 __METHOD__
734 if( $field === null ) { // it has not been initialized. do so.
735 $dbw = wfGetDB( DB_MASTER );
736 $count = $dbr->selectField(
737 'revision', 'count(*)',
738 array( 'rev_user' => $uid ),
739 __METHOD__
741 $dbw->update(
742 'user',
743 array( 'user_editcount' => $count ),
744 array( 'user_id' => $uid ),
745 __METHOD__
747 } else {
748 $count = $field;
750 wfProfileOut( __METHOD__ );
751 return $count;
755 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
756 * @todo hash random numbers to improve security, like generateToken()
758 * @return \string New random password
760 static function randomPassword() {
761 global $wgMinimalPasswordLength;
762 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
763 $l = strlen( $pwchars ) - 1;
765 $pwlength = max( 7, $wgMinimalPasswordLength );
766 $digit = mt_rand(0, $pwlength - 1);
767 $np = '';
768 for ( $i = 0; $i < $pwlength; $i++ ) {
769 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
771 return $np;
775 * Set cached properties to default.
777 * @note This no longer clears uncached lazy-initialised properties;
778 * the constructor does that instead.
779 * @private
781 function loadDefaults( $name = false ) {
782 wfProfileIn( __METHOD__ );
784 global $wgCookiePrefix;
786 $this->mId = 0;
787 $this->mName = $name;
788 $this->mRealName = '';
789 $this->mPassword = $this->mNewpassword = '';
790 $this->mNewpassTime = null;
791 $this->mEmail = '';
792 $this->mOptionOverrides = null;
793 $this->mOptionsLoaded = false;
795 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
796 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
797 } else {
798 $this->mTouched = '0'; # Allow any pages to be cached
801 $this->setToken(); # Random
802 $this->mEmailAuthenticated = null;
803 $this->mEmailToken = '';
804 $this->mEmailTokenExpires = null;
805 $this->mRegistration = wfTimestamp( TS_MW );
806 $this->mGroups = array();
808 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
810 wfProfileOut( __METHOD__ );
814 * @deprecated Use wfSetupSession().
816 function SetupSession() {
817 wfDeprecated( __METHOD__ );
818 wfSetupSession();
822 * Load user data from the session or login cookie. If there are no valid
823 * credentials, initialises the user as an anonymous user.
824 * @return \bool True if the user is logged in, false otherwise.
826 private function loadFromSession() {
827 global $wgMemc, $wgCookiePrefix;
829 $result = null;
830 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
831 if ( $result !== null ) {
832 return $result;
835 if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
836 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
837 if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
838 $this->loadDefaults(); // Possible collision!
839 wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
840 cookie user ID ($sId) don't match!" );
841 return false;
843 $_SESSION['wsUserID'] = $sId;
844 } else if ( isset( $_SESSION['wsUserID'] ) ) {
845 if ( $_SESSION['wsUserID'] != 0 ) {
846 $sId = $_SESSION['wsUserID'];
847 } else {
848 $this->loadDefaults();
849 return false;
851 } else {
852 $this->loadDefaults();
853 return false;
856 if ( isset( $_SESSION['wsUserName'] ) ) {
857 $sName = $_SESSION['wsUserName'];
858 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
859 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
860 $_SESSION['wsUserName'] = $sName;
861 } else {
862 $this->loadDefaults();
863 return false;
866 $passwordCorrect = FALSE;
867 $this->mId = $sId;
868 if ( !$this->loadFromId() ) {
869 # Not a valid ID, loadFromId has switched the object to anon for us
870 return false;
873 if ( isset( $_SESSION['wsToken'] ) ) {
874 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
875 $from = 'session';
876 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
877 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
878 $from = 'cookie';
879 } else {
880 # No session or persistent login cookie
881 $this->loadDefaults();
882 return false;
885 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
886 $_SESSION['wsToken'] = $this->mToken;
887 wfDebug( "Logged in from $from\n" );
888 return true;
889 } else {
890 # Invalid credentials
891 wfDebug( "Can't log in from $from, invalid credentials\n" );
892 $this->loadDefaults();
893 return false;
898 * Load user and user_group data from the database.
899 * $this::mId must be set, this is how the user is identified.
901 * @return \bool True if the user exists, false if the user is anonymous
902 * @private
904 function loadFromDatabase() {
905 # Paranoia
906 $this->mId = intval( $this->mId );
908 /** Anonymous user */
909 if( !$this->mId ) {
910 $this->loadDefaults();
911 return false;
914 $dbr = wfGetDB( DB_MASTER );
915 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
917 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
919 if ( $s !== false ) {
920 # Initialise user table data
921 $this->loadFromRow( $s );
922 $this->mGroups = null; // deferred
923 $this->getEditCount(); // revalidation for nulls
924 return true;
925 } else {
926 # Invalid user_id
927 $this->mId = 0;
928 $this->loadDefaults();
929 return false;
934 * Initialize this object from a row from the user table.
936 * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
938 function loadFromRow( $row ) {
939 $this->mDataLoaded = true;
941 if ( isset( $row->user_id ) ) {
942 $this->mId = intval( $row->user_id );
944 $this->mName = $row->user_name;
945 $this->mRealName = $row->user_real_name;
946 $this->mPassword = $row->user_password;
947 $this->mNewpassword = $row->user_newpassword;
948 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
949 $this->mEmail = $row->user_email;
950 $this->decodeOptions( $row->user_options );
951 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
952 $this->mToken = $row->user_token;
953 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
954 $this->mEmailToken = $row->user_email_token;
955 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
956 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
957 $this->mEditCount = $row->user_editcount;
961 * Load the groups from the database if they aren't already loaded.
962 * @private
964 function loadGroups() {
965 if ( is_null( $this->mGroups ) ) {
966 $dbr = wfGetDB( DB_MASTER );
967 $res = $dbr->select( 'user_groups',
968 array( 'ug_group' ),
969 array( 'ug_user' => $this->mId ),
970 __METHOD__ );
971 $this->mGroups = array();
972 while( $row = $dbr->fetchObject( $res ) ) {
973 $this->mGroups[] = $row->ug_group;
979 * Clear various cached data stored in this object.
980 * @param $reloadFrom \string Reload user and user_groups table data from a
981 * given source. May be "name", "id", "defaults", "session", or false for
982 * no reload.
984 function clearInstanceCache( $reloadFrom = false ) {
985 $this->mNewtalk = -1;
986 $this->mDatePreference = null;
987 $this->mBlockedby = -1; # Unset
988 $this->mHash = false;
989 $this->mSkin = null;
990 $this->mRights = null;
991 $this->mEffectiveGroups = null;
992 $this->mOptions = null;
994 if ( $reloadFrom ) {
995 $this->mDataLoaded = false;
996 $this->mFrom = $reloadFrom;
1001 * Combine the language default options with any site-specific options
1002 * and add the default language variants.
1004 * @return \type{\arrayof{\string}} Array of options
1006 static function getDefaultOptions() {
1007 global $wgNamespacesToBeSearchedDefault;
1009 * Site defaults will override the global/language defaults
1011 global $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
1012 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
1015 * default language setting
1017 $variant = $wgContLang->getPreferredVariant( false );
1018 $defOpt['variant'] = $variant;
1019 $defOpt['language'] = $variant;
1020 foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
1021 $defOpt['searchNs'.$nsnum] = !empty($wgNamespacesToBeSearchedDefault[$nsnum]);
1023 $defOpt['skin'] = $wgDefaultSkin;
1025 return $defOpt;
1029 * Get a given default option value.
1031 * @param $opt \string Name of option to retrieve
1032 * @return \string Default option value
1034 public static function getDefaultOption( $opt ) {
1035 $defOpts = self::getDefaultOptions();
1036 if( isset( $defOpts[$opt] ) ) {
1037 return $defOpts[$opt];
1038 } else {
1039 return null;
1044 * Get a list of user toggle names
1045 * @return \type{\arrayof{\string}} Array of user toggle names
1047 static function getToggles() {
1048 global $wgContLang, $wgUseRCPatrol;
1049 $extraToggles = array();
1050 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
1051 if( $wgUseRCPatrol ) {
1052 $extraToggles[] = 'hidepatrolled';
1053 $extraToggles[] = 'newpageshidepatrolled';
1054 $extraToggles[] = 'watchlisthidepatrolled';
1056 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
1061 * Get blocking information
1062 * @private
1063 * @param $bFromSlave \bool Whether to check the slave database first. To
1064 * improve performance, non-critical checks are done
1065 * against slaves. Check when actually saving should be
1066 * done against master.
1068 function getBlockedStatus( $bFromSlave = true ) {
1069 global $wgEnableSorbs, $wgProxyWhitelist;
1071 if ( -1 != $this->mBlockedby ) {
1072 wfDebug( "User::getBlockedStatus: already loaded.\n" );
1073 return;
1076 wfProfileIn( __METHOD__ );
1077 wfDebug( __METHOD__.": checking...\n" );
1079 // Initialize data...
1080 // Otherwise something ends up stomping on $this->mBlockedby when
1081 // things get lazy-loaded later, causing false positive block hits
1082 // due to -1 !== 0. Probably session-related... Nothing should be
1083 // overwriting mBlockedby, surely?
1084 $this->load();
1086 $this->mBlockedby = 0;
1087 $this->mHideName = 0;
1088 $this->mAllowUsertalk = 0;
1089 $ip = wfGetIP();
1091 if ($this->isAllowed( 'ipblock-exempt' ) ) {
1092 # Exempt from all types of IP-block
1093 $ip = '';
1096 # User/IP blocking
1097 $this->mBlock = new Block();
1098 $this->mBlock->fromMaster( !$bFromSlave );
1099 if ( $this->mBlock->load( $ip , $this->mId ) ) {
1100 wfDebug( __METHOD__.": Found block.\n" );
1101 $this->mBlockedby = $this->mBlock->mBy;
1102 $this->mBlockreason = $this->mBlock->mReason;
1103 $this->mHideName = $this->mBlock->mHideName;
1104 $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
1105 if ( $this->isLoggedIn() ) {
1106 $this->spreadBlock();
1108 } else {
1109 // Bug 13611: don't remove mBlock here, to allow account creation blocks to
1110 // apply to users. Note that the existence of $this->mBlock is not used to
1111 // check for edit blocks, $this->mBlockedby is instead.
1114 # Proxy blocking
1115 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
1116 # Local list
1117 if ( wfIsLocallyBlockedProxy( $ip ) ) {
1118 $this->mBlockedby = wfMsg( 'proxyblocker' );
1119 $this->mBlockreason = wfMsg( 'proxyblockreason' );
1122 # DNSBL
1123 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
1124 if ( $this->inSorbsBlacklist( $ip ) ) {
1125 $this->mBlockedby = wfMsg( 'sorbs' );
1126 $this->mBlockreason = wfMsg( 'sorbsreason' );
1131 # Extensions
1132 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1134 wfProfileOut( __METHOD__ );
1138 * Whether the given IP is in the SORBS blacklist.
1140 * @param $ip \string IP to check
1141 * @return \bool True if blacklisted.
1143 function inSorbsBlacklist( $ip ) {
1144 global $wgEnableSorbs, $wgSorbsUrl;
1146 return $wgEnableSorbs &&
1147 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
1151 * Whether the given IP is in a given DNS blacklist.
1153 * @param $ip \string IP to check
1154 * @param $base \string URL of the DNS blacklist
1155 * @return \bool True if blacklisted.
1157 function inDnsBlacklist( $ip, $base ) {
1158 wfProfileIn( __METHOD__ );
1160 $found = false;
1161 $host = '';
1162 // FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
1163 if( IP::isIPv4($ip) ) {
1164 # Make hostname
1165 $host = "$ip.$base";
1167 # Send query
1168 $ipList = gethostbynamel( $host );
1170 if( $ipList ) {
1171 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1172 $found = true;
1173 } else {
1174 wfDebug( "Requested $host, not found in $base.\n" );
1178 wfProfileOut( __METHOD__ );
1179 return $found;
1183 * Is this user subject to rate limiting?
1185 * @return \bool True if rate limited
1187 public function isPingLimitable() {
1188 global $wgRateLimitsExcludedGroups;
1189 if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1190 // Deprecated, but kept for backwards-compatibility config
1191 return false;
1193 return !$this->isAllowed('noratelimit');
1197 * Primitive rate limits: enforce maximum actions per time period
1198 * to put a brake on flooding.
1200 * @note When using a shared cache like memcached, IP-address
1201 * last-hit counters will be shared across wikis.
1203 * @param $action \string Action to enforce; 'edit' if unspecified
1204 * @return \bool True if a rate limiter was tripped
1206 function pingLimiter( $action='edit' ) {
1208 # Call the 'PingLimiter' hook
1209 $result = false;
1210 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1211 return $result;
1214 global $wgRateLimits;
1215 if( !isset( $wgRateLimits[$action] ) ) {
1216 return false;
1219 # Some groups shouldn't trigger the ping limiter, ever
1220 if( !$this->isPingLimitable() )
1221 return false;
1223 global $wgMemc, $wgRateLimitLog;
1224 wfProfileIn( __METHOD__ );
1226 $limits = $wgRateLimits[$action];
1227 $keys = array();
1228 $id = $this->getId();
1229 $ip = wfGetIP();
1230 $userLimit = false;
1232 if( isset( $limits['anon'] ) && $id == 0 ) {
1233 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1236 if( isset( $limits['user'] ) && $id != 0 ) {
1237 $userLimit = $limits['user'];
1239 if( $this->isNewbie() ) {
1240 if( isset( $limits['newbie'] ) && $id != 0 ) {
1241 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1243 if( isset( $limits['ip'] ) ) {
1244 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1246 $matches = array();
1247 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1248 $subnet = $matches[1];
1249 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1252 // Check for group-specific permissions
1253 // If more than one group applies, use the group with the highest limit
1254 foreach ( $this->getGroups() as $group ) {
1255 if ( isset( $limits[$group] ) ) {
1256 if ( $userLimit === false || $limits[$group] > $userLimit ) {
1257 $userLimit = $limits[$group];
1261 // Set the user limit key
1262 if ( $userLimit !== false ) {
1263 wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
1264 $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1267 $triggered = false;
1268 foreach( $keys as $key => $limit ) {
1269 list( $max, $period ) = $limit;
1270 $summary = "(limit $max in {$period}s)";
1271 $count = $wgMemc->get( $key );
1272 if( $count ) {
1273 if( $count > $max ) {
1274 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1275 if( $wgRateLimitLog ) {
1276 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1278 $triggered = true;
1279 } else {
1280 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1282 } else {
1283 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1284 $wgMemc->add( $key, 1, intval( $period ) );
1286 $wgMemc->incr( $key );
1289 wfProfileOut( __METHOD__ );
1290 return $triggered;
1294 * Check if user is blocked
1296 * @param $bFromSlave \bool Whether to check the slave database instead of the master
1297 * @return \bool True if blocked, false otherwise
1299 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1300 wfDebug( "User::isBlocked: enter\n" );
1301 $this->getBlockedStatus( $bFromSlave );
1302 return $this->mBlockedby !== 0;
1306 * Check if user is blocked from editing a particular article
1308 * @param $title \string Title to check
1309 * @param $bFromSlave \bool Whether to check the slave database instead of the master
1310 * @return \bool True if blocked, false otherwise
1312 function isBlockedFrom( $title, $bFromSlave = false ) {
1313 global $wgBlockAllowsUTEdit;
1314 wfProfileIn( __METHOD__ );
1315 wfDebug( __METHOD__.": enter\n" );
1317 wfDebug( __METHOD__.": asking isBlocked()\n" );
1318 $blocked = $this->isBlocked( $bFromSlave );
1319 $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
1320 # If a user's name is suppressed, they cannot make edits anywhere
1321 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1322 $title->getNamespace() == NS_USER_TALK ) {
1323 $blocked = false;
1324 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1326 wfProfileOut( __METHOD__ );
1327 return $blocked;
1331 * If user is blocked, return the name of the user who placed the block
1332 * @return \string name of blocker
1334 function blockedBy() {
1335 $this->getBlockedStatus();
1336 return $this->mBlockedby;
1340 * If user is blocked, return the specified reason for the block
1341 * @return \string Blocking reason
1343 function blockedFor() {
1344 $this->getBlockedStatus();
1345 return $this->mBlockreason;
1349 * If user is blocked, return the ID for the block
1350 * @return \int Block ID
1352 function getBlockId() {
1353 $this->getBlockedStatus();
1354 return ($this->mBlock ? $this->mBlock->mId : false);
1358 * Check if user is blocked on all wikis.
1359 * Do not use for actual edit permission checks!
1360 * This is intented for quick UI checks.
1362 * @param $ip \type{\string} IP address, uses current client if none given
1363 * @return \type{\bool} True if blocked, false otherwise
1365 function isBlockedGlobally( $ip = '' ) {
1366 if( $this->mBlockedGlobally !== null ) {
1367 return $this->mBlockedGlobally;
1369 // User is already an IP?
1370 if( IP::isIPAddress( $this->getName() ) ) {
1371 $ip = $this->getName();
1372 } else if( !$ip ) {
1373 $ip = wfGetIP();
1375 $blocked = false;
1376 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1377 $this->mBlockedGlobally = (bool)$blocked;
1378 return $this->mBlockedGlobally;
1382 * Check if user account is locked
1384 * @return \type{\bool} True if locked, false otherwise
1386 function isLocked() {
1387 if( $this->mLocked !== null ) {
1388 return $this->mLocked;
1390 global $wgAuth;
1391 $authUser = $wgAuth->getUserInstance( $this );
1392 $this->mLocked = (bool)$authUser->isLocked();
1393 return $this->mLocked;
1397 * Check if user account is hidden
1399 * @return \type{\bool} True if hidden, false otherwise
1401 function isHidden() {
1402 if( $this->mHideName !== null ) {
1403 return $this->mHideName;
1405 $this->getBlockedStatus();
1406 if( !$this->mHideName ) {
1407 global $wgAuth;
1408 $authUser = $wgAuth->getUserInstance( $this );
1409 $this->mHideName = (bool)$authUser->isHidden();
1411 return $this->mHideName;
1415 * Get the user's ID.
1416 * @return \int The user's ID; 0 if the user is anonymous or nonexistent
1418 function getId() {
1419 if( $this->mId === null and $this->mName !== null
1420 and User::isIP( $this->mName ) ) {
1421 // Special case, we know the user is anonymous
1422 return 0;
1423 } elseif( $this->mId === null ) {
1424 // Don't load if this was initialized from an ID
1425 $this->load();
1427 return $this->mId;
1431 * Set the user and reload all fields according to a given ID
1432 * @param $v \int User ID to reload
1434 function setId( $v ) {
1435 $this->mId = $v;
1436 $this->clearInstanceCache( 'id' );
1440 * Get the user name, or the IP of an anonymous user
1441 * @return \string User's name or IP address
1443 function getName() {
1444 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1445 # Special case optimisation
1446 return $this->mName;
1447 } else {
1448 $this->load();
1449 if ( $this->mName === false ) {
1450 # Clean up IPs
1451 $this->mName = IP::sanitizeIP( wfGetIP() );
1453 return $this->mName;
1458 * Set the user name.
1460 * This does not reload fields from the database according to the given
1461 * name. Rather, it is used to create a temporary "nonexistent user" for
1462 * later addition to the database. It can also be used to set the IP
1463 * address for an anonymous user to something other than the current
1464 * remote IP.
1466 * @note User::newFromName() has rougly the same function, when the named user
1467 * does not exist.
1468 * @param $str \string New user name to set
1470 function setName( $str ) {
1471 $this->load();
1472 $this->mName = $str;
1476 * Get the user's name escaped by underscores.
1477 * @return \string Username escaped by underscores.
1479 function getTitleKey() {
1480 return str_replace( ' ', '_', $this->getName() );
1484 * Check if the user has new messages.
1485 * @return \bool True if the user has new messages
1487 function getNewtalk() {
1488 $this->load();
1490 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1491 if( $this->mNewtalk === -1 ) {
1492 $this->mNewtalk = false; # reset talk page status
1494 # Check memcached separately for anons, who have no
1495 # entire User object stored in there.
1496 if( !$this->mId ) {
1497 global $wgMemc;
1498 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1499 $newtalk = $wgMemc->get( $key );
1500 if( strval( $newtalk ) !== '' ) {
1501 $this->mNewtalk = (bool)$newtalk;
1502 } else {
1503 // Since we are caching this, make sure it is up to date by getting it
1504 // from the master
1505 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1506 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1508 } else {
1509 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1513 return (bool)$this->mNewtalk;
1517 * Return the talk page(s) this user has new messages on.
1518 * @return \type{\arrayof{\string}} Array of page URLs
1520 function getNewMessageLinks() {
1521 $talks = array();
1522 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1523 return $talks;
1525 if (!$this->getNewtalk())
1526 return array();
1527 $up = $this->getUserPage();
1528 $utp = $up->getTalkPage();
1529 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1534 * Internal uncached check for new messages
1536 * @see getNewtalk()
1537 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1538 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1539 * @param $fromMaster \bool true to fetch from the master, false for a slave
1540 * @return \bool True if the user has new messages
1541 * @private
1543 function checkNewtalk( $field, $id, $fromMaster = false ) {
1544 if ( $fromMaster ) {
1545 $db = wfGetDB( DB_MASTER );
1546 } else {
1547 $db = wfGetDB( DB_SLAVE );
1549 $ok = $db->selectField( 'user_newtalk', $field,
1550 array( $field => $id ), __METHOD__ );
1551 return $ok !== false;
1555 * Add or update the new messages flag
1556 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1557 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1558 * @return \bool True if successful, false otherwise
1559 * @private
1561 function updateNewtalk( $field, $id ) {
1562 $dbw = wfGetDB( DB_MASTER );
1563 $dbw->insert( 'user_newtalk',
1564 array( $field => $id ),
1565 __METHOD__,
1566 'IGNORE' );
1567 if ( $dbw->affectedRows() ) {
1568 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1569 return true;
1570 } else {
1571 wfDebug( __METHOD__." already set ($field, $id)\n" );
1572 return false;
1577 * Clear the new messages flag for the given user
1578 * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1579 * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1580 * @return \bool True if successful, false otherwise
1581 * @private
1583 function deleteNewtalk( $field, $id ) {
1584 $dbw = wfGetDB( DB_MASTER );
1585 $dbw->delete( 'user_newtalk',
1586 array( $field => $id ),
1587 __METHOD__ );
1588 if ( $dbw->affectedRows() ) {
1589 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1590 return true;
1591 } else {
1592 wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1593 return false;
1598 * Update the 'You have new messages!' status.
1599 * @param $val \bool Whether the user has new messages
1601 function setNewtalk( $val ) {
1602 if( wfReadOnly() ) {
1603 return;
1606 $this->load();
1607 $this->mNewtalk = $val;
1609 if( $this->isAnon() ) {
1610 $field = 'user_ip';
1611 $id = $this->getName();
1612 } else {
1613 $field = 'user_id';
1614 $id = $this->getId();
1616 global $wgMemc;
1618 if( $val ) {
1619 $changed = $this->updateNewtalk( $field, $id );
1620 } else {
1621 $changed = $this->deleteNewtalk( $field, $id );
1624 if( $this->isAnon() ) {
1625 // Anons have a separate memcached space, since
1626 // user records aren't kept for them.
1627 $key = wfMemcKey( 'newtalk', 'ip', $id );
1628 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1630 if ( $changed ) {
1631 $this->invalidateCache();
1636 * Generate a current or new-future timestamp to be stored in the
1637 * user_touched field when we update things.
1638 * @return \string Timestamp in TS_MW format
1640 private static function newTouchedTimestamp() {
1641 global $wgClockSkewFudge;
1642 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1646 * Clear user data from memcached.
1647 * Use after applying fun updates to the database; caller's
1648 * responsibility to update user_touched if appropriate.
1650 * Called implicitly from invalidateCache() and saveSettings().
1652 private function clearSharedCache() {
1653 $this->load();
1654 if( $this->mId ) {
1655 global $wgMemc;
1656 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1661 * Immediately touch the user data cache for this account.
1662 * Updates user_touched field, and removes account data from memcached
1663 * for reload on the next hit.
1665 function invalidateCache() {
1666 $this->load();
1667 if( $this->mId ) {
1668 $this->mTouched = self::newTouchedTimestamp();
1670 $dbw = wfGetDB( DB_MASTER );
1671 $dbw->update( 'user',
1672 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1673 array( 'user_id' => $this->mId ),
1674 __METHOD__ );
1676 $this->clearSharedCache();
1681 * Validate the cache for this account.
1682 * @param $timestamp \string A timestamp in TS_MW format
1684 function validateCache( $timestamp ) {
1685 $this->load();
1686 return ($timestamp >= $this->mTouched);
1690 * Get the user touched timestamp
1692 function getTouched() {
1693 $this->load();
1694 return $this->mTouched;
1698 * Set the password and reset the random token.
1699 * Calls through to authentication plugin if necessary;
1700 * will have no effect if the auth plugin refuses to
1701 * pass the change through or if the legal password
1702 * checks fail.
1704 * As a special case, setting the password to null
1705 * wipes it, so the account cannot be logged in until
1706 * a new password is set, for instance via e-mail.
1708 * @param $str \string New password to set
1709 * @throws PasswordError on failure
1711 function setPassword( $str ) {
1712 global $wgAuth;
1714 if( $str !== null ) {
1715 if( !$wgAuth->allowPasswordChange() ) {
1716 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1719 $valid = $this->isValidPassword( $str );
1720 if( $valid !== true ) {
1721 global $wgMinimalPasswordLength;
1722 throw new PasswordError( wfMsgExt( $valid, array( 'parsemag' ),
1723 $wgMinimalPasswordLength ) );
1727 if( !$wgAuth->setPassword( $this, $str ) ) {
1728 throw new PasswordError( wfMsg( 'externaldberror' ) );
1731 $this->setInternalPassword( $str );
1733 return true;
1737 * Set the password and reset the random token unconditionally.
1739 * @param $str \string New password to set
1741 function setInternalPassword( $str ) {
1742 $this->load();
1743 $this->setToken();
1745 if( $str === null ) {
1746 // Save an invalid hash...
1747 $this->mPassword = '';
1748 } else {
1749 $this->mPassword = self::crypt( $str );
1751 $this->mNewpassword = '';
1752 $this->mNewpassTime = null;
1756 * Get the user's current token.
1757 * @return \string Token
1759 function getToken() {
1760 $this->load();
1761 return $this->mToken;
1765 * Set the random token (used for persistent authentication)
1766 * Called from loadDefaults() among other places.
1768 * @param $token \string If specified, set the token to this value
1769 * @private
1771 function setToken( $token = false ) {
1772 global $wgSecretKey, $wgProxyKey;
1773 $this->load();
1774 if ( !$token ) {
1775 if ( $wgSecretKey ) {
1776 $key = $wgSecretKey;
1777 } elseif ( $wgProxyKey ) {
1778 $key = $wgProxyKey;
1779 } else {
1780 $key = microtime();
1782 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1783 } else {
1784 $this->mToken = $token;
1789 * Set the cookie password
1791 * @param $str \string New cookie password
1792 * @private
1794 function setCookiePassword( $str ) {
1795 $this->load();
1796 $this->mCookiePassword = md5( $str );
1800 * Set the password for a password reminder or new account email
1802 * @param $str \string New password to set
1803 * @param $throttle \bool If true, reset the throttle timestamp to the present
1805 function setNewpassword( $str, $throttle = true ) {
1806 $this->load();
1807 $this->mNewpassword = self::crypt( $str );
1808 if ( $throttle ) {
1809 $this->mNewpassTime = wfTimestampNow();
1814 * Has password reminder email been sent within the last
1815 * $wgPasswordReminderResendTime hours?
1816 * @return \bool True or false
1818 function isPasswordReminderThrottled() {
1819 global $wgPasswordReminderResendTime;
1820 $this->load();
1821 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1822 return false;
1824 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1825 return time() < $expiry;
1829 * Get the user's e-mail address
1830 * @return \string User's email address
1832 function getEmail() {
1833 $this->load();
1834 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1835 return $this->mEmail;
1839 * Get the timestamp of the user's e-mail authentication
1840 * @return \string TS_MW timestamp
1842 function getEmailAuthenticationTimestamp() {
1843 $this->load();
1844 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1845 return $this->mEmailAuthenticated;
1849 * Set the user's e-mail address
1850 * @param $str \string New e-mail address
1852 function setEmail( $str ) {
1853 $this->load();
1854 $this->mEmail = $str;
1855 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1859 * Get the user's real name
1860 * @return \string User's real name
1862 function getRealName() {
1863 $this->load();
1864 return $this->mRealName;
1868 * Set the user's real name
1869 * @param $str \string New real name
1871 function setRealName( $str ) {
1872 $this->load();
1873 $this->mRealName = $str;
1877 * Get the user's current setting for a given option.
1879 * @param $oname \string The option to check
1880 * @param $defaultOverride \string A default value returned if the option does not exist
1881 * @return \string User's current value for the option
1882 * @see getBoolOption()
1883 * @see getIntOption()
1885 function getOption( $oname, $defaultOverride = null ) {
1886 $this->loadOptions();
1888 if ( is_null( $this->mOptions ) ) {
1889 if($defaultOverride != '') {
1890 return $defaultOverride;
1892 $this->mOptions = User::getDefaultOptions();
1895 if ( array_key_exists( $oname, $this->mOptions ) ) {
1896 return $this->mOptions[$oname];
1897 } else {
1898 return $defaultOverride;
1903 * Get the user's current setting for a given option, as a boolean value.
1905 * @param $oname \string The option to check
1906 * @return \bool User's current value for the option
1907 * @see getOption()
1909 function getBoolOption( $oname ) {
1910 return (bool)$this->getOption( $oname );
1915 * Get the user's current setting for a given option, as a boolean value.
1917 * @param $oname \string The option to check
1918 * @param $defaultOverride \int A default value returned if the option does not exist
1919 * @return \int User's current value for the option
1920 * @see getOption()
1922 function getIntOption( $oname, $defaultOverride=0 ) {
1923 $val = $this->getOption( $oname );
1924 if( $val == '' ) {
1925 $val = $defaultOverride;
1927 return intval( $val );
1931 * Set the given option for a user.
1933 * @param $oname \string The option to set
1934 * @param $val \mixed New value to set
1936 function setOption( $oname, $val ) {
1937 $this->load();
1938 $this->loadOptions();
1940 if ( $oname == 'skin' ) {
1941 # Clear cached skin, so the new one displays immediately in Special:Preferences
1942 unset( $this->mSkin );
1945 // Explicitly NULL values should refer to defaults
1946 global $wgDefaultUserOptions;
1947 if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
1948 $val = $wgDefaultUserOptions[$oname];
1951 $this->mOptions[$oname] = $val;
1955 * Reset all options to the site defaults
1957 function resetOptions() {
1958 $this->mOptions = User::getDefaultOptions();
1962 * Get the user's preferred date format.
1963 * @return \string User's preferred date format
1965 function getDatePreference() {
1966 // Important migration for old data rows
1967 if ( is_null( $this->mDatePreference ) ) {
1968 global $wgLang;
1969 $value = $this->getOption( 'date' );
1970 $map = $wgLang->getDatePreferenceMigrationMap();
1971 if ( isset( $map[$value] ) ) {
1972 $value = $map[$value];
1974 $this->mDatePreference = $value;
1976 return $this->mDatePreference;
1980 * Get the permissions this user has.
1981 * @return \type{\arrayof{\string}} Array of permission names
1983 function getRights() {
1984 if ( is_null( $this->mRights ) ) {
1985 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1986 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
1987 // Force reindexation of rights when a hook has unset one of them
1988 $this->mRights = array_values( $this->mRights );
1990 return $this->mRights;
1994 * Get the list of explicit group memberships this user has.
1995 * The implicit * and user groups are not included.
1996 * @return \type{\arrayof{\string}} Array of internal group names
1998 function getGroups() {
1999 $this->load();
2000 return $this->mGroups;
2004 * Get the list of implicit group memberships this user has.
2005 * This includes all explicit groups, plus 'user' if logged in,
2006 * '*' for all accounts and autopromoted groups
2007 * @param $recache \bool Whether to avoid the cache
2008 * @return \type{\arrayof{\string}} Array of internal group names
2010 function getEffectiveGroups( $recache = false ) {
2011 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2012 $this->mEffectiveGroups = $this->getGroups();
2013 $this->mEffectiveGroups[] = '*';
2014 if( $this->getId() ) {
2015 $this->mEffectiveGroups[] = 'user';
2017 $this->mEffectiveGroups = array_unique( array_merge(
2018 $this->mEffectiveGroups,
2019 Autopromote::getAutopromoteGroups( $this )
2020 ) );
2022 # Hook for additional groups
2023 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2026 return $this->mEffectiveGroups;
2030 * Get the user's edit count.
2031 * @return \int User'e edit count
2033 function getEditCount() {
2034 if ($this->getId()) {
2035 if ( !isset( $this->mEditCount ) ) {
2036 /* Populate the count, if it has not been populated yet */
2037 $this->mEditCount = User::edits($this->mId);
2039 return $this->mEditCount;
2040 } else {
2041 /* nil */
2042 return null;
2047 * Add the user to the given group.
2048 * This takes immediate effect.
2049 * @param $group \string Name of the group to add
2051 function addGroup( $group ) {
2052 $dbw = wfGetDB( DB_MASTER );
2053 if( $this->getId() ) {
2054 $dbw->insert( 'user_groups',
2055 array(
2056 'ug_user' => $this->getID(),
2057 'ug_group' => $group,
2059 'User::addGroup',
2060 array( 'IGNORE' ) );
2063 $this->loadGroups();
2064 $this->mGroups[] = $group;
2065 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2067 $this->invalidateCache();
2071 * Remove the user from the given group.
2072 * This takes immediate effect.
2073 * @param $group \string Name of the group to remove
2075 function removeGroup( $group ) {
2076 $this->load();
2077 $dbw = wfGetDB( DB_MASTER );
2078 $dbw->delete( 'user_groups',
2079 array(
2080 'ug_user' => $this->getID(),
2081 'ug_group' => $group,
2083 'User::removeGroup' );
2085 $this->loadGroups();
2086 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2087 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2089 $this->invalidateCache();
2094 * Get whether the user is logged in
2095 * @return \bool True or false
2097 function isLoggedIn() {
2098 return $this->getID() != 0;
2102 * Get whether the user is anonymous
2103 * @return \bool True or false
2105 function isAnon() {
2106 return !$this->isLoggedIn();
2110 * Get whether the user is a bot
2111 * @return \bool True or false
2112 * @deprecated
2114 function isBot() {
2115 wfDeprecated( __METHOD__ );
2116 return $this->isAllowed( 'bot' );
2120 * Check if user is allowed to access a feature / make an action
2121 * @param $action \string action to be checked
2122 * @return \bool True if action is allowed, else false
2124 function isAllowed( $action = '' ) {
2125 if ( $action === '' )
2126 return true; // In the spirit of DWIM
2127 # Patrolling may not be enabled
2128 if( $action === 'patrol' || $action === 'autopatrol' ) {
2129 global $wgUseRCPatrol, $wgUseNPPatrol;
2130 if( !$wgUseRCPatrol && !$wgUseNPPatrol )
2131 return false;
2133 # Use strict parameter to avoid matching numeric 0 accidentally inserted
2134 # by misconfiguration: 0 == 'foo'
2135 return in_array( $action, $this->getRights(), true );
2139 * Check whether to enable recent changes patrol features for this user
2140 * @return \bool True or false
2142 public function useRCPatrol() {
2143 global $wgUseRCPatrol;
2144 return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2148 * Check whether to enable new pages patrol features for this user
2149 * @return \bool True or false
2151 public function useNPPatrol() {
2152 global $wgUseRCPatrol, $wgUseNPPatrol;
2153 return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2157 * Get the current skin, loading it if required, and setting a title
2158 * @param $t Title: the title to use in the skin
2159 * @return Skin The current skin
2160 * @todo FIXME : need to check the old failback system [AV]
2162 function &getSkin( $t = null ) {
2163 if ( ! isset( $this->mSkin ) ) {
2164 wfProfileIn( __METHOD__ );
2166 global $wgHiddenPrefs;
2167 if( !in_array( 'skin', $wgHiddenPrefs ) ) {
2168 # get the user skin
2169 global $wgRequest;
2170 $userSkin = $this->getOption( 'skin' );
2171 $userSkin = $wgRequest->getVal('useskin', $userSkin);
2172 } else {
2173 # if we're not allowing users to override, then use the default
2174 global $wgDefaultSkin;
2175 $userSkin = $wgDefaultSkin;
2178 $this->mSkin =& Skin::newFromKey( $userSkin );
2179 wfProfileOut( __METHOD__ );
2181 if( $t || !$this->mSkin->getTitle() ) {
2182 if ( !$t ) {
2183 global $wgOut;
2184 $t = $wgOut->getTitle();
2186 $this->mSkin->setTitle( $t );
2188 return $this->mSkin;
2192 * Check the watched status of an article.
2193 * @param $title \type{Title} Title of the article to look at
2194 * @return \bool True if article is watched
2196 function isWatched( $title ) {
2197 $wl = WatchedItem::fromUserTitle( $this, $title );
2198 return $wl->isWatched();
2202 * Watch an article.
2203 * @param $title \type{Title} Title of the article to look at
2205 function addWatch( $title ) {
2206 $wl = WatchedItem::fromUserTitle( $this, $title );
2207 $wl->addWatch();
2208 $this->invalidateCache();
2212 * Stop watching an article.
2213 * @param $title \type{Title} Title of the article to look at
2215 function removeWatch( $title ) {
2216 $wl = WatchedItem::fromUserTitle( $this, $title );
2217 $wl->removeWatch();
2218 $this->invalidateCache();
2222 * Clear the user's notification timestamp for the given title.
2223 * If e-notif e-mails are on, they will receive notification mails on
2224 * the next change of the page if it's watched etc.
2225 * @param $title \type{Title} Title of the article to look at
2227 function clearNotification( &$title ) {
2228 global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2230 # Do nothing if the database is locked to writes
2231 if( wfReadOnly() ) {
2232 return;
2235 if ($title->getNamespace() == NS_USER_TALK &&
2236 $title->getText() == $this->getName() ) {
2237 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
2238 return;
2239 $this->setNewtalk( false );
2242 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2243 return;
2246 if( $this->isAnon() ) {
2247 // Nothing else to do...
2248 return;
2251 // Only update the timestamp if the page is being watched.
2252 // The query to find out if it is watched is cached both in memcached and per-invocation,
2253 // and when it does have to be executed, it can be on a slave
2254 // If this is the user's newtalk page, we always update the timestamp
2255 if ($title->getNamespace() == NS_USER_TALK &&
2256 $title->getText() == $wgUser->getName())
2258 $watched = true;
2259 } elseif ( $this->getId() == $wgUser->getId() ) {
2260 $watched = $title->userIsWatching();
2261 } else {
2262 $watched = true;
2265 // If the page is watched by the user (or may be watched), update the timestamp on any
2266 // any matching rows
2267 if ( $watched ) {
2268 $dbw = wfGetDB( DB_MASTER );
2269 $dbw->update( 'watchlist',
2270 array( /* SET */
2271 'wl_notificationtimestamp' => NULL
2272 ), array( /* WHERE */
2273 'wl_title' => $title->getDBkey(),
2274 'wl_namespace' => $title->getNamespace(),
2275 'wl_user' => $this->getID()
2276 ), __METHOD__
2282 * Resets all of the given user's page-change notification timestamps.
2283 * If e-notif e-mails are on, they will receive notification mails on
2284 * the next change of any watched page.
2286 * @param $currentUser \int User ID
2288 function clearAllNotifications( $currentUser ) {
2289 global $wgUseEnotif, $wgShowUpdatedMarker;
2290 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2291 $this->setNewtalk( false );
2292 return;
2294 if( $currentUser != 0 ) {
2295 $dbw = wfGetDB( DB_MASTER );
2296 $dbw->update( 'watchlist',
2297 array( /* SET */
2298 'wl_notificationtimestamp' => NULL
2299 ), array( /* WHERE */
2300 'wl_user' => $currentUser
2301 ), __METHOD__
2303 # We also need to clear here the "you have new message" notification for the own user_talk page
2304 # This is cleared one page view later in Article::viewUpdates();
2309 * Set this user's options from an encoded string
2310 * @param $str \string Encoded options to import
2311 * @private
2313 function decodeOptions( $str ) {
2314 if (!$str)
2315 return;
2317 $this->mOptionsLoaded = true;
2318 $this->mOptionOverrides = array();
2320 $this->mOptions = array();
2321 $a = explode( "\n", $str );
2322 foreach ( $a as $s ) {
2323 $m = array();
2324 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2325 $this->mOptions[$m[1]] = $m[2];
2326 $this->mOptionOverrides[$m[1]] = $m[2];
2332 * Set a cookie on the user's client. Wrapper for
2333 * WebResponse::setCookie
2334 * @param $name \string Name of the cookie to set
2335 * @param $value \string Value to set
2336 * @param $exp \int Expiration time, as a UNIX time value;
2337 * if 0 or not specified, use the default $wgCookieExpiration
2339 protected function setCookie( $name, $value, $exp=0 ) {
2340 global $wgRequest;
2341 $wgRequest->response()->setcookie( $name, $value, $exp );
2345 * Clear a cookie on the user's client
2346 * @param $name \string Name of the cookie to clear
2348 protected function clearCookie( $name ) {
2349 $this->setCookie( $name, '', time() - 86400 );
2353 * Set the default cookies for this session on the user's client.
2355 function setCookies() {
2356 $this->load();
2357 if ( 0 == $this->mId ) return;
2358 $session = array(
2359 'wsUserID' => $this->mId,
2360 'wsToken' => $this->mToken,
2361 'wsUserName' => $this->getName()
2363 $cookies = array(
2364 'UserID' => $this->mId,
2365 'UserName' => $this->getName(),
2367 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2368 $cookies['Token'] = $this->mToken;
2369 } else {
2370 $cookies['Token'] = false;
2373 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2374 #check for null, since the hook could cause a null value
2375 if ( !is_null( $session ) && isset( $_SESSION ) ){
2376 $_SESSION = $session + $_SESSION;
2378 foreach ( $cookies as $name => $value ) {
2379 if ( $value === false ) {
2380 $this->clearCookie( $name );
2381 } else {
2382 $this->setCookie( $name, $value );
2388 * Log this user out.
2390 function logout() {
2391 if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
2392 $this->doLogout();
2397 * Clear the user's cookies and session, and reset the instance cache.
2398 * @private
2399 * @see logout()
2401 function doLogout() {
2402 $this->clearInstanceCache( 'defaults' );
2404 $_SESSION['wsUserID'] = 0;
2406 $this->clearCookie( 'UserID' );
2407 $this->clearCookie( 'Token' );
2409 # Remember when user logged out, to prevent seeing cached pages
2410 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2414 * Save this user's settings into the database.
2415 * @todo Only rarely do all these fields need to be set!
2417 function saveSettings() {
2418 $this->load();
2419 if ( wfReadOnly() ) { return; }
2420 if ( 0 == $this->mId ) { return; }
2422 $this->mTouched = self::newTouchedTimestamp();
2424 $dbw = wfGetDB( DB_MASTER );
2425 $dbw->update( 'user',
2426 array( /* SET */
2427 'user_name' => $this->mName,
2428 'user_password' => $this->mPassword,
2429 'user_newpassword' => $this->mNewpassword,
2430 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2431 'user_real_name' => $this->mRealName,
2432 'user_email' => $this->mEmail,
2433 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2434 'user_options' => '',
2435 'user_touched' => $dbw->timestamp($this->mTouched),
2436 'user_token' => $this->mToken,
2437 'user_email_token' => $this->mEmailToken,
2438 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2439 ), array( /* WHERE */
2440 'user_id' => $this->mId
2441 ), __METHOD__
2444 $this->saveOptions();
2446 wfRunHooks( 'UserSaveSettings', array( $this ) );
2447 $this->clearSharedCache();
2448 $this->getUserPage()->invalidateCache();
2452 * If only this user's username is known, and it exists, return the user ID.
2454 function idForName() {
2455 $s = trim( $this->getName() );
2456 if ( $s === '' ) return 0;
2458 $dbr = wfGetDB( DB_SLAVE );
2459 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2460 if ( $id === false ) {
2461 $id = 0;
2463 return $id;
2467 * Add a user to the database, return the user object
2469 * @param $name \string Username to add
2470 * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
2471 * - password The user's password. Password logins will be disabled if this is omitted.
2472 * - newpassword A temporary password mailed to the user
2473 * - email The user's email address
2474 * - email_authenticated The email authentication timestamp
2475 * - real_name The user's real name
2476 * - options An associative array of non-default options
2477 * - token Random authentication token. Do not set.
2478 * - registration Registration timestamp. Do not set.
2480 * @return \type{User} A new User object, or null if the username already exists
2482 static function createNew( $name, $params = array() ) {
2483 $user = new User;
2484 $user->load();
2485 if ( isset( $params['options'] ) ) {
2486 $user->mOptions = $params['options'] + $user->mOptions;
2487 unset( $params['options'] );
2489 $dbw = wfGetDB( DB_MASTER );
2490 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2491 $fields = array(
2492 'user_id' => $seqVal,
2493 'user_name' => $name,
2494 'user_password' => $user->mPassword,
2495 'user_newpassword' => $user->mNewpassword,
2496 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2497 'user_email' => $user->mEmail,
2498 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2499 'user_real_name' => $user->mRealName,
2500 'user_options' => '',
2501 'user_token' => $user->mToken,
2502 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2503 'user_editcount' => 0,
2505 foreach ( $params as $name => $value ) {
2506 $fields["user_$name"] = $value;
2508 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2509 if ( $dbw->affectedRows() ) {
2510 $newUser = User::newFromId( $dbw->insertId() );
2511 } else {
2512 $newUser = null;
2514 return $newUser;
2518 * Add this existing user object to the database
2520 function addToDatabase() {
2521 $this->load();
2522 $dbw = wfGetDB( DB_MASTER );
2523 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2524 $dbw->insert( 'user',
2525 array(
2526 'user_id' => $seqVal,
2527 'user_name' => $this->mName,
2528 'user_password' => $this->mPassword,
2529 'user_newpassword' => $this->mNewpassword,
2530 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2531 'user_email' => $this->mEmail,
2532 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2533 'user_real_name' => $this->mRealName,
2534 'user_options' => '',
2535 'user_token' => $this->mToken,
2536 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2537 'user_editcount' => 0,
2538 ), __METHOD__
2540 $this->mId = $dbw->insertId();
2542 // Clear instance cache other than user table data, which is already accurate
2543 $this->clearInstanceCache();
2545 $this->saveOptions();
2549 * If this (non-anonymous) user is blocked, block any IP address
2550 * they've successfully logged in from.
2552 function spreadBlock() {
2553 wfDebug( __METHOD__."()\n" );
2554 $this->load();
2555 if ( $this->mId == 0 ) {
2556 return;
2559 $userblock = Block::newFromDB( '', $this->mId );
2560 if ( !$userblock ) {
2561 return;
2564 $userblock->doAutoblock( wfGetIp() );
2569 * Generate a string which will be different for any combination of
2570 * user options which would produce different parser output.
2571 * This will be used as part of the hash key for the parser cache,
2572 * so users will the same options can share the same cached data
2573 * safely.
2575 * Extensions which require it should install 'PageRenderingHash' hook,
2576 * which will give them a chance to modify this key based on their own
2577 * settings.
2579 * @return \string Page rendering hash
2581 function getPageRenderingHash() {
2582 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2583 if( $this->mHash ){
2584 return $this->mHash;
2587 // stubthreshold is only included below for completeness,
2588 // it will always be 0 when this function is called by parsercache.
2590 $confstr = $this->getOption( 'math' );
2591 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2592 if ( $wgUseDynamicDates ) {
2593 $confstr .= '!' . $this->getDatePreference();
2595 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2596 $confstr .= '!' . $wgLang->getCode();
2597 $confstr .= '!' . $this->getOption( 'thumbsize' );
2598 // add in language specific options, if any
2599 $extra = $wgContLang->getExtraHashOptions();
2600 $confstr .= $extra;
2602 $confstr .= $wgRenderHashAppend;
2604 // Give a chance for extensions to modify the hash, if they have
2605 // extra options or other effects on the parser cache.
2606 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2608 // Make it a valid memcached key fragment
2609 $confstr = str_replace( ' ', '_', $confstr );
2610 $this->mHash = $confstr;
2611 return $confstr;
2615 * Get whether the user is explicitly blocked from account creation.
2616 * @return \bool True if blocked
2618 function isBlockedFromCreateAccount() {
2619 $this->getBlockedStatus();
2620 return $this->mBlock && $this->mBlock->mCreateAccount;
2624 * Get whether the user is blocked from using Special:Emailuser.
2625 * @return \bool True if blocked
2627 function isBlockedFromEmailuser() {
2628 $this->getBlockedStatus();
2629 return $this->mBlock && $this->mBlock->mBlockEmail;
2633 * Get whether the user is allowed to create an account.
2634 * @return \bool True if allowed
2636 function isAllowedToCreateAccount() {
2637 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2641 * @deprecated
2643 function setLoaded( $loaded ) {
2644 wfDeprecated( __METHOD__ );
2648 * Get this user's personal page title.
2650 * @return \type{Title} User's personal page title
2652 function getUserPage() {
2653 return Title::makeTitle( NS_USER, $this->getName() );
2657 * Get this user's talk page title.
2659 * @return \type{Title} User's talk page title
2661 function getTalkPage() {
2662 $title = $this->getUserPage();
2663 return $title->getTalkPage();
2667 * Get the maximum valid user ID.
2668 * @return \int User ID
2669 * @static
2671 function getMaxID() {
2672 static $res; // cache
2674 if ( isset( $res ) )
2675 return $res;
2676 else {
2677 $dbr = wfGetDB( DB_SLAVE );
2678 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2683 * Determine whether the user is a newbie. Newbies are either
2684 * anonymous IPs, or the most recently created accounts.
2685 * @return \bool True if the user is a newbie
2687 function isNewbie() {
2688 return !$this->isAllowed( 'autoconfirmed' );
2692 * Is the user active? We check to see if they've made at least
2693 * X number of edits in the last Y days.
2695 * @return \bool True if the user is active, false if not.
2697 public function isActiveEditor() {
2698 global $wgActiveUserEditCount, $wgActiveUserDays;
2699 $dbr = wfGetDB( DB_SLAVE );
2701 // Stolen without shame from RC
2702 $cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
2703 $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
2704 $oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
2706 $res = $dbr->select( 'revision', '1',
2707 array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
2708 __METHOD__,
2709 array('LIMIT' => $wgActiveUserEditCount ) );
2711 $count = $dbr->numRows($res);
2712 $dbr->freeResult($res);
2714 return $count == $wgActiveUserEditCount;
2718 * Check to see if the given clear-text password is one of the accepted passwords
2719 * @param $password \string user password.
2720 * @return \bool True if the given password is correct, otherwise False.
2722 function checkPassword( $password ) {
2723 global $wgAuth;
2724 $this->load();
2726 // Even though we stop people from creating passwords that
2727 // are shorter than this, doesn't mean people wont be able
2728 // to. Certain authentication plugins do NOT want to save
2729 // domain passwords in a mysql database, so we should
2730 // check this (incase $wgAuth->strict() is false).
2731 if( $this->isValidPassword( $password ) !== true ) {
2732 return false;
2735 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2736 return true;
2737 } elseif( $wgAuth->strict() ) {
2738 /* Auth plugin doesn't allow local authentication */
2739 return false;
2740 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2741 /* Auth plugin doesn't allow local authentication for this user name */
2742 return false;
2744 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2745 return true;
2746 } elseif ( function_exists( 'iconv' ) ) {
2747 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2748 # Check for this with iconv
2749 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2750 if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2751 return true;
2754 return false;
2758 * Check if the given clear-text password matches the temporary password
2759 * sent by e-mail for password reset operations.
2760 * @return \bool True if matches, false otherwise
2762 function checkTemporaryPassword( $plaintext ) {
2763 global $wgNewPasswordExpiry;
2764 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
2765 $this->load();
2766 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
2767 return ( time() < $expiry );
2768 } else {
2769 return false;
2774 * Initialize (if necessary) and return a session token value
2775 * which can be used in edit forms to show that the user's
2776 * login credentials aren't being hijacked with a foreign form
2777 * submission.
2779 * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
2780 * @return \string The new edit token
2782 function editToken( $salt = '' ) {
2783 if ( $this->isAnon() ) {
2784 return EDIT_TOKEN_SUFFIX;
2785 } else {
2786 if( !isset( $_SESSION['wsEditToken'] ) ) {
2787 $token = $this->generateToken();
2788 $_SESSION['wsEditToken'] = $token;
2789 } else {
2790 $token = $_SESSION['wsEditToken'];
2792 if( is_array( $salt ) ) {
2793 $salt = implode( '|', $salt );
2795 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2800 * Generate a looking random token for various uses.
2802 * @param $salt \string Optional salt value
2803 * @return \string The new random token
2805 function generateToken( $salt = '' ) {
2806 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2807 return md5( $token . $salt );
2811 * Check given value against the token value stored in the session.
2812 * A match should confirm that the form was submitted from the
2813 * user's own login session, not a form submission from a third-party
2814 * site.
2816 * @param $val \string Input value to compare
2817 * @param $salt \string Optional function-specific data for hashing
2818 * @return \bool Whether the token matches
2820 function matchEditToken( $val, $salt = '' ) {
2821 $sessionToken = $this->editToken( $salt );
2822 if ( $val != $sessionToken ) {
2823 wfDebug( "User::matchEditToken: broken session data\n" );
2825 return $val == $sessionToken;
2829 * Check given value against the token value stored in the session,
2830 * ignoring the suffix.
2832 * @param $val \string Input value to compare
2833 * @param $salt \string Optional function-specific data for hashing
2834 * @return \bool Whether the token matches
2836 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2837 $sessionToken = $this->editToken( $salt );
2838 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2842 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2843 * mail to the user's given address.
2845 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
2847 function sendConfirmationMail() {
2848 global $wgLang;
2849 $expiration = null; // gets passed-by-ref and defined in next line.
2850 $token = $this->confirmationToken( $expiration );
2851 $url = $this->confirmationTokenUrl( $token );
2852 $invalidateURL = $this->invalidationTokenUrl( $token );
2853 $this->saveSettings();
2855 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2856 wfMsg( 'confirmemail_body',
2857 wfGetIP(),
2858 $this->getName(),
2859 $url,
2860 $wgLang->timeanddate( $expiration, false ),
2861 $invalidateURL,
2862 $wgLang->date( $expiration, false ),
2863 $wgLang->time( $expiration, false ) ) );
2867 * Send an e-mail to this user's account. Does not check for
2868 * confirmed status or validity.
2870 * @param $subject \string Message subject
2871 * @param $body \string Message body
2872 * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
2873 * @param $replyto \string Reply-To address
2874 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
2876 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2877 if( is_null( $from ) ) {
2878 global $wgPasswordSender;
2879 $from = $wgPasswordSender;
2882 $to = new MailAddress( $this );
2883 $sender = new MailAddress( $from );
2884 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2888 * Generate, store, and return a new e-mail confirmation code.
2889 * A hash (unsalted, since it's used as a key) is stored.
2891 * @note Call saveSettings() after calling this function to commit
2892 * this change to the database.
2894 * @param[out] &$expiration \mixed Accepts the expiration time
2895 * @return \string New token
2896 * @private
2898 function confirmationToken( &$expiration ) {
2899 $now = time();
2900 $expires = $now + 7 * 24 * 60 * 60;
2901 $expiration = wfTimestamp( TS_MW, $expires );
2902 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2903 $hash = md5( $token );
2904 $this->load();
2905 $this->mEmailToken = $hash;
2906 $this->mEmailTokenExpires = $expiration;
2907 return $token;
2911 * Return a URL the user can use to confirm their email address.
2912 * @param $token \string Accepts the email confirmation token
2913 * @return \string New token URL
2914 * @private
2916 function confirmationTokenUrl( $token ) {
2917 return $this->getTokenUrl( 'ConfirmEmail', $token );
2920 * Return a URL the user can use to invalidate their email address.
2921 * @param $token \string Accepts the email confirmation token
2922 * @return \string New token URL
2923 * @private
2925 function invalidationTokenUrl( $token ) {
2926 return $this->getTokenUrl( 'Invalidateemail', $token );
2930 * Internal function to format the e-mail validation/invalidation URLs.
2931 * This uses $wgArticlePath directly as a quickie hack to use the
2932 * hardcoded English names of the Special: pages, for ASCII safety.
2934 * @note Since these URLs get dropped directly into emails, using the
2935 * short English names avoids insanely long URL-encoded links, which
2936 * also sometimes can get corrupted in some browsers/mailers
2937 * (bug 6957 with Gmail and Internet Explorer).
2939 * @param $page \string Special page
2940 * @param $token \string Token
2941 * @return \string Formatted URL
2943 protected function getTokenUrl( $page, $token ) {
2944 global $wgArticlePath;
2945 return wfExpandUrl(
2946 str_replace(
2947 '$1',
2948 "Special:$page/$token",
2949 $wgArticlePath ) );
2953 * Mark the e-mail address confirmed.
2955 * @note Call saveSettings() after calling this function to commit the change.
2957 function confirmEmail() {
2958 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2959 return true;
2963 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2964 * address if it was already confirmed.
2966 * @note Call saveSettings() after calling this function to commit the change.
2968 function invalidateEmail() {
2969 $this->load();
2970 $this->mEmailToken = null;
2971 $this->mEmailTokenExpires = null;
2972 $this->setEmailAuthenticationTimestamp( null );
2973 return true;
2977 * Set the e-mail authentication timestamp.
2978 * @param $timestamp \string TS_MW timestamp
2980 function setEmailAuthenticationTimestamp( $timestamp ) {
2981 $this->load();
2982 $this->mEmailAuthenticated = $timestamp;
2983 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2987 * Is this user allowed to send e-mails within limits of current
2988 * site configuration?
2989 * @return \bool True if allowed
2991 function canSendEmail() {
2992 global $wgEnableEmail, $wgEnableUserEmail;
2993 if( !$wgEnableEmail || !$wgEnableUserEmail ) {
2994 return false;
2996 $canSend = $this->isEmailConfirmed();
2997 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2998 return $canSend;
3002 * Is this user allowed to receive e-mails within limits of current
3003 * site configuration?
3004 * @return \bool True if allowed
3006 function canReceiveEmail() {
3007 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
3011 * Is this user's e-mail address valid-looking and confirmed within
3012 * limits of the current site configuration?
3014 * @note If $wgEmailAuthentication is on, this may require the user to have
3015 * confirmed their address by returning a code or using a password
3016 * sent to the address from the wiki.
3018 * @return \bool True if confirmed
3020 function isEmailConfirmed() {
3021 global $wgEmailAuthentication;
3022 $this->load();
3023 $confirmed = true;
3024 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
3025 if( $this->isAnon() )
3026 return false;
3027 if( !self::isValidEmailAddr( $this->mEmail ) )
3028 return false;
3029 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
3030 return false;
3031 return true;
3032 } else {
3033 return $confirmed;
3038 * Check whether there is an outstanding request for e-mail confirmation.
3039 * @return \bool True if pending
3041 function isEmailConfirmationPending() {
3042 global $wgEmailAuthentication;
3043 return $wgEmailAuthentication &&
3044 !$this->isEmailConfirmed() &&
3045 $this->mEmailToken &&
3046 $this->mEmailTokenExpires > wfTimestamp();
3050 * Get the timestamp of account creation.
3052 * @return \types{\string,\bool} string Timestamp of account creation, or false for
3053 * non-existent/anonymous user accounts.
3055 public function getRegistration() {
3056 return $this->getId() > 0
3057 ? $this->mRegistration
3058 : false;
3062 * Get the timestamp of the first edit
3064 * @return \types{\string,\bool} string Timestamp of first edit, or false for
3065 * non-existent/anonymous user accounts.
3067 public function getFirstEditTimestamp() {
3068 if( $this->getId() == 0 ) return false; // anons
3069 $dbr = wfGetDB( DB_SLAVE );
3070 $time = $dbr->selectField( 'revision', 'rev_timestamp',
3071 array( 'rev_user' => $this->getId() ),
3072 __METHOD__,
3073 array( 'ORDER BY' => 'rev_timestamp ASC' )
3075 if( !$time ) return false; // no edits
3076 return wfTimestamp( TS_MW, $time );
3080 * Get the permissions associated with a given list of groups
3082 * @param $groups \type{\arrayof{\string}} List of internal group names
3083 * @return \type{\arrayof{\string}} List of permission key names for given groups combined
3085 static function getGroupPermissions( $groups ) {
3086 global $wgGroupPermissions, $wgRevokePermissions;
3087 $rights = array();
3088 // grant every granted permission first
3089 foreach( $groups as $group ) {
3090 if( isset( $wgGroupPermissions[$group] ) ) {
3091 $rights = array_merge( $rights,
3092 // array_filter removes empty items
3093 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
3096 // now revoke the revoked permissions
3097 foreach( $groups as $group ) {
3098 if( isset( $wgRevokePermissions[$group] ) ) {
3099 $rights = array_diff( $rights,
3100 array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
3103 return array_unique($rights);
3107 * Get all the groups who have a given permission
3109 * @param $role \string Role to check
3110 * @return \type{\arrayof{\string}} List of internal group names with the given permission
3112 static function getGroupsWithPermission( $role ) {
3113 global $wgGroupPermissions;
3114 $allowedGroups = array();
3115 foreach ( $wgGroupPermissions as $group => $rights ) {
3116 if ( isset( $rights[$role] ) && $rights[$role] ) {
3117 $allowedGroups[] = $group;
3120 return $allowedGroups;
3124 * Get the localized descriptive name for a group, if it exists
3126 * @param $group \string Internal group name
3127 * @return \string Localized descriptive group name
3129 static function getGroupName( $group ) {
3130 global $wgMessageCache;
3131 $wgMessageCache->loadAllMessages();
3132 $key = "group-$group";
3133 $name = wfMsg( $key );
3134 return $name == '' || wfEmptyMsg( $key, $name )
3135 ? $group
3136 : $name;
3140 * Get the localized descriptive name for a member of a group, if it exists
3142 * @param $group \string Internal group name
3143 * @return \string Localized name for group member
3145 static function getGroupMember( $group ) {
3146 global $wgMessageCache;
3147 $wgMessageCache->loadAllMessages();
3148 $key = "group-$group-member";
3149 $name = wfMsg( $key );
3150 return $name == '' || wfEmptyMsg( $key, $name )
3151 ? $group
3152 : $name;
3156 * Return the set of defined explicit groups.
3157 * The implicit groups (by default *, 'user' and 'autoconfirmed')
3158 * are not included, as they are defined automatically, not in the database.
3159 * @return \type{\arrayof{\string}} Array of internal group names
3161 static function getAllGroups() {
3162 global $wgGroupPermissions, $wgRevokePermissions;
3163 return array_diff(
3164 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
3165 self::getImplicitGroups()
3170 * Get a list of all available permissions.
3171 * @return \type{\arrayof{\string}} Array of permission names
3173 static function getAllRights() {
3174 if ( self::$mAllRights === false ) {
3175 global $wgAvailableRights;
3176 if ( count( $wgAvailableRights ) ) {
3177 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3178 } else {
3179 self::$mAllRights = self::$mCoreRights;
3181 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3183 return self::$mAllRights;
3187 * Get a list of implicit groups
3188 * @return \type{\arrayof{\string}} Array of internal group names
3190 public static function getImplicitGroups() {
3191 global $wgImplicitGroups;
3192 $groups = $wgImplicitGroups;
3193 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
3194 return $groups;
3198 * Get the title of a page describing a particular group
3200 * @param $group \string Internal group name
3201 * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
3203 static function getGroupPage( $group ) {
3204 global $wgMessageCache;
3205 $wgMessageCache->loadAllMessages();
3206 $page = wfMsgForContent( 'grouppage-' . $group );
3207 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3208 $title = Title::newFromText( $page );
3209 if( is_object( $title ) )
3210 return $title;
3212 return false;
3216 * Create a link to the group in HTML, if available;
3217 * else return the group name.
3219 * @param $group \string Internal name of the group
3220 * @param $text \string The text of the link
3221 * @return \string HTML link to the group
3223 static function makeGroupLinkHTML( $group, $text = '' ) {
3224 if( $text == '' ) {
3225 $text = self::getGroupName( $group );
3227 $title = self::getGroupPage( $group );
3228 if( $title ) {
3229 global $wgUser;
3230 $sk = $wgUser->getSkin();
3231 return $sk->link( $title, htmlspecialchars( $text ) );
3232 } else {
3233 return $text;
3238 * Create a link to the group in Wikitext, if available;
3239 * else return the group name.
3241 * @param $group \string Internal name of the group
3242 * @param $text \string The text of the link
3243 * @return \string Wikilink to the group
3245 static function makeGroupLinkWiki( $group, $text = '' ) {
3246 if( $text == '' ) {
3247 $text = self::getGroupName( $group );
3249 $title = self::getGroupPage( $group );
3250 if( $title ) {
3251 $page = $title->getPrefixedText();
3252 return "[[$page|$text]]";
3253 } else {
3254 return $text;
3259 * Returns an array of the groups that a particular group can add/remove.
3261 * @param $group String: the group to check for whether it can add/remove
3262 * @return Array array( 'add' => array( addablegroups ),
3263 * 'remove' => array( removablegroups ),
3264 * 'add-self' => array( addablegroups to self),
3265 * 'remove-self' => array( removable groups from self) )
3267 static function changeableByGroup( $group ) {
3268 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
3270 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
3271 if( empty($wgAddGroups[$group]) ) {
3272 // Don't add anything to $groups
3273 } elseif( $wgAddGroups[$group] === true ) {
3274 // You get everything
3275 $groups['add'] = self::getAllGroups();
3276 } elseif( is_array($wgAddGroups[$group]) ) {
3277 $groups['add'] = $wgAddGroups[$group];
3280 // Same thing for remove
3281 if( empty($wgRemoveGroups[$group]) ) {
3282 } elseif($wgRemoveGroups[$group] === true ) {
3283 $groups['remove'] = self::getAllGroups();
3284 } elseif( is_array($wgRemoveGroups[$group]) ) {
3285 $groups['remove'] = $wgRemoveGroups[$group];
3288 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
3289 if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
3290 foreach($wgGroupsAddToSelf as $key => $value) {
3291 if( is_int($key) ) {
3292 $wgGroupsAddToSelf['user'][] = $value;
3297 if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
3298 foreach($wgGroupsRemoveFromSelf as $key => $value) {
3299 if( is_int($key) ) {
3300 $wgGroupsRemoveFromSelf['user'][] = $value;
3305 // Now figure out what groups the user can add to him/herself
3306 if( empty($wgGroupsAddToSelf[$group]) ) {
3307 } elseif( $wgGroupsAddToSelf[$group] === true ) {
3308 // No idea WHY this would be used, but it's there
3309 $groups['add-self'] = User::getAllGroups();
3310 } elseif( is_array($wgGroupsAddToSelf[$group]) ) {
3311 $groups['add-self'] = $wgGroupsAddToSelf[$group];
3314 if( empty($wgGroupsRemoveFromSelf[$group]) ) {
3315 } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
3316 $groups['remove-self'] = User::getAllGroups();
3317 } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) {
3318 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
3321 return $groups;
3325 * Returns an array of groups that this user can add and remove
3326 * @return Array array( 'add' => array( addablegroups ),
3327 * 'remove' => array( removablegroups ),
3328 * 'add-self' => array( addablegroups to self),
3329 * 'remove-self' => array( removable groups from self) )
3331 function changeableGroups() {
3332 if( $this->isAllowed( 'userrights' ) ) {
3333 // This group gives the right to modify everything (reverse-
3334 // compatibility with old "userrights lets you change
3335 // everything")
3336 // Using array_merge to make the groups reindexed
3337 $all = array_merge( User::getAllGroups() );
3338 return array(
3339 'add' => $all,
3340 'remove' => $all,
3341 'add-self' => array(),
3342 'remove-self' => array()
3346 // Okay, it's not so simple, we will have to go through the arrays
3347 $groups = array(
3348 'add' => array(),
3349 'remove' => array(),
3350 'add-self' => array(),
3351 'remove-self' => array() );
3352 $addergroups = $this->getEffectiveGroups();
3354 foreach ($addergroups as $addergroup) {
3355 $groups = array_merge_recursive(
3356 $groups, $this->changeableByGroup($addergroup)
3358 $groups['add'] = array_unique( $groups['add'] );
3359 $groups['remove'] = array_unique( $groups['remove'] );
3360 $groups['add-self'] = array_unique( $groups['add-self'] );
3361 $groups['remove-self'] = array_unique( $groups['remove-self'] );
3363 return $groups;
3367 * Increment the user's edit-count field.
3368 * Will have no effect for anonymous users.
3370 function incEditCount() {
3371 if( !$this->isAnon() ) {
3372 $dbw = wfGetDB( DB_MASTER );
3373 $dbw->update( 'user',
3374 array( 'user_editcount=user_editcount+1' ),
3375 array( 'user_id' => $this->getId() ),
3376 __METHOD__ );
3378 // Lazy initialization check...
3379 if( $dbw->affectedRows() == 0 ) {
3380 // Pull from a slave to be less cruel to servers
3381 // Accuracy isn't the point anyway here
3382 $dbr = wfGetDB( DB_SLAVE );
3383 $count = $dbr->selectField( 'revision',
3384 'COUNT(rev_user)',
3385 array( 'rev_user' => $this->getId() ),
3386 __METHOD__ );
3388 // Now here's a goddamn hack...
3389 if( $dbr !== $dbw ) {
3390 // If we actually have a slave server, the count is
3391 // at least one behind because the current transaction
3392 // has not been committed and replicated.
3393 $count++;
3394 } else {
3395 // But if DB_SLAVE is selecting the master, then the
3396 // count we just read includes the revision that was
3397 // just added in the working transaction.
3400 $dbw->update( 'user',
3401 array( 'user_editcount' => $count ),
3402 array( 'user_id' => $this->getId() ),
3403 __METHOD__ );
3406 // edit count in user cache too
3407 $this->invalidateCache();
3411 * Get the description of a given right
3413 * @param $right \string Right to query
3414 * @return \string Localized description of the right
3416 static function getRightDescription( $right ) {
3417 global $wgMessageCache;
3418 $wgMessageCache->loadAllMessages();
3419 $key = "right-$right";
3420 $name = wfMsg( $key );
3421 return $name == '' || wfEmptyMsg( $key, $name )
3422 ? $right
3423 : $name;
3427 * Make an old-style password hash
3429 * @param $password \string Plain-text password
3430 * @param $userId \string User ID
3431 * @return \string Password hash
3433 static function oldCrypt( $password, $userId ) {
3434 global $wgPasswordSalt;
3435 if ( $wgPasswordSalt ) {
3436 return md5( $userId . '-' . md5( $password ) );
3437 } else {
3438 return md5( $password );
3443 * Make a new-style password hash
3445 * @param $password \string Plain-text password
3446 * @param $salt \string Optional salt, may be random or the user ID.
3447 * If unspecified or false, will generate one automatically
3448 * @return \string Password hash
3450 static function crypt( $password, $salt = false ) {
3451 global $wgPasswordSalt;
3453 $hash = '';
3454 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
3455 return $hash;
3458 if( $wgPasswordSalt ) {
3459 if ( $salt === false ) {
3460 $salt = substr( wfGenerateToken(), 0, 8 );
3462 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3463 } else {
3464 return ':A:' . md5( $password );
3469 * Compare a password hash with a plain-text password. Requires the user
3470 * ID if there's a chance that the hash is an old-style hash.
3472 * @param $hash \string Password hash
3473 * @param $password \string Plain-text password to compare
3474 * @param $userId \string User ID for old-style password salt
3475 * @return \bool
3477 static function comparePasswords( $hash, $password, $userId = false ) {
3478 $m = false;
3479 $type = substr( $hash, 0, 3 );
3481 $result = false;
3482 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
3483 return $result;
3486 if ( $type == ':A:' ) {
3487 # Unsalted
3488 return md5( $password ) === substr( $hash, 3 );
3489 } elseif ( $type == ':B:' ) {
3490 # Salted
3491 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3492 return md5( $salt.'-'.md5( $password ) ) == $realHash;
3493 } else {
3494 # Old-style
3495 return self::oldCrypt( $password, $userId ) === $hash;
3500 * Add a newuser log entry for this user
3501 * @param $byEmail Boolean: account made by email?
3503 public function addNewUserLogEntry( $byEmail = false ) {
3504 global $wgUser, $wgContLang, $wgNewUserLog;
3505 if( empty($wgNewUserLog) ) {
3506 return true; // disabled
3508 $talk = $wgContLang->getFormattedNsText( NS_TALK );
3509 if( $this->getName() == $wgUser->getName() ) {
3510 $action = 'create';
3511 $message = '';
3512 } else {
3513 $action = 'create2';
3514 $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
3516 $log = new LogPage( 'newusers' );
3517 $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
3518 return true;
3522 * Add an autocreate newuser log entry for this user
3523 * Used by things like CentralAuth and perhaps other authplugins.
3525 public function addNewUserLogEntryAutoCreate() {
3526 global $wgNewUserLog;
3527 if( empty($wgNewUserLog) ) {
3528 return true; // disabled
3530 $log = new LogPage( 'newusers', false );
3531 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3532 return true;
3535 protected function loadOptions() {
3536 $this->load();
3537 if ($this->mOptionsLoaded || !$this->getId() )
3538 return;
3540 $this->mOptions = self::getDefaultOptions();
3542 // Maybe load from the object
3544 if ( !is_null($this->mOptionOverrides) ) {
3545 wfDebug( "Loading options for user ".$this->getId()." from override cache.\n" );
3546 foreach( $this->mOptionOverrides as $key => $value ) {
3547 $this->mOptions[$key] = $value;
3549 } else {
3550 wfDebug( "Loading options for user ".$this->getId()." from database.\n" );
3551 // Load from database
3552 $dbr = wfGetDB( DB_SLAVE );
3554 $res = $dbr->select( 'user_properties',
3555 '*',
3556 array('up_user' => $this->getId()),
3557 __METHOD__
3560 while( $row = $dbr->fetchObject( $res ) ) {
3561 $this->mOptionOverrides[$row->up_property] = $row->up_value;
3562 $this->mOptions[$row->up_property] = $row->up_value;
3566 $this->mOptionsLoaded = true;
3568 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
3571 protected function saveOptions() {
3572 $this->loadOptions();
3573 $dbw = wfGetDB( DB_MASTER );
3575 $insert_rows = array();
3577 $saveOptions = $this->mOptions;
3579 // Allow hooks to abort, for instance to save to a global profile.
3580 // Reset options to default state before saving.
3581 if (!wfRunHooks( 'UserSaveOptions', array($this, &$saveOptions) ) )
3582 return;
3584 foreach( $saveOptions as $key => $value ) {
3585 if ( ( is_null(self::getDefaultOption($key)) &&
3586 !( $value === false || is_null($value) ) ) ||
3587 $value != self::getDefaultOption( $key ) ) {
3588 $insert_rows[] = array(
3589 'up_user' => $this->getId(),
3590 'up_property' => $key,
3591 'up_value' => $value,
3596 $dbw->begin();
3597 $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ );
3598 $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
3599 $dbw->commit();