Emit CDATA more intelligently
[mediawiki.git] / includes / User.php
blobdf95f3faa09819f064de870ee8353bc7204bc861
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 //@}
218 static $idCacheByName = array();
221 * Lightweight constructor for an anonymous user.
222 * Use the User::newFrom* factory functions for other kinds of users.
224 * @see newFromName()
225 * @see newFromId()
226 * @see newFromConfirmationCode()
227 * @see newFromSession()
228 * @see newFromRow()
230 function User() {
231 $this->clearInstanceCache( 'defaults' );
235 * Load the user table data for this object from the source given by mFrom.
237 function load() {
238 if ( $this->mDataLoaded ) {
239 return;
241 wfProfileIn( __METHOD__ );
243 # Set it now to avoid infinite recursion in accessors
244 $this->mDataLoaded = true;
246 switch ( $this->mFrom ) {
247 case 'defaults':
248 $this->loadDefaults();
249 break;
250 case 'name':
251 $this->mId = self::idFromName( $this->mName );
252 if ( !$this->mId ) {
253 # Nonexistent user placeholder object
254 $this->loadDefaults( $this->mName );
255 } else {
256 $this->loadFromId();
258 break;
259 case 'id':
260 $this->loadFromId();
261 break;
262 case 'session':
263 $this->loadFromSession();
264 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
265 break;
266 default:
267 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
269 wfProfileOut( __METHOD__ );
273 * Load user table data, given mId has already been set.
274 * @return \bool false if the ID does not exist, true otherwise
275 * @private
277 function loadFromId() {
278 global $wgMemc;
279 if ( $this->mId == 0 ) {
280 $this->loadDefaults();
281 return false;
284 # Try cache
285 $key = wfMemcKey( 'user', 'id', $this->mId );
286 $data = $wgMemc->get( $key );
287 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
288 # Object is expired, load from DB
289 $data = false;
292 if ( !$data ) {
293 wfDebug( "Cache miss for user {$this->mId}\n" );
294 # Load from DB
295 if ( !$this->loadFromDatabase() ) {
296 # Can't load from ID, user is anonymous
297 return false;
299 $this->saveToCache();
300 } else {
301 wfDebug( "Got user {$this->mId} from cache\n" );
302 # Restore from cache
303 foreach ( self::$mCacheVars as $name ) {
304 $this->$name = $data[$name];
307 return true;
311 * Save user data to the shared cache
313 function saveToCache() {
314 $this->load();
315 $this->loadGroups();
316 $this->loadOptions();
317 if ( $this->isAnon() ) {
318 // Anonymous users are uncached
319 return;
321 $data = array();
322 foreach ( self::$mCacheVars as $name ) {
323 $data[$name] = $this->$name;
325 $data['mVersion'] = MW_USER_VERSION;
326 $key = wfMemcKey( 'user', 'id', $this->mId );
327 global $wgMemc;
328 $wgMemc->set( $key, $data );
332 /** @name newFrom*() static factory methods */
333 //@{
336 * Static factory method for creation from username.
338 * This is slightly less efficient than newFromId(), so use newFromId() if
339 * you have both an ID and a name handy.
341 * @param $name \string Username, validated by Title::newFromText()
342 * @param $validate \mixed Validate username. Takes the same parameters as
343 * User::getCanonicalName(), except that true is accepted as an alias
344 * for 'valid', for BC.
346 * @return \type{User} The User object, or null if the username is invalid. If the
347 * username is not present in the database, the result will be a user object
348 * with a name, zero user ID and default settings.
350 static function newFromName( $name, $validate = 'valid' ) {
351 if ( $validate === true ) {
352 $validate = 'valid';
354 $name = self::getCanonicalName( $name, $validate );
355 if ( $name === false ) {
356 return null;
357 } else {
358 # Create unloaded user object
359 $u = new User;
360 $u->mName = $name;
361 $u->mFrom = 'name';
362 return $u;
367 * Static factory method for creation from a given user ID.
369 * @param $id \int Valid user ID
370 * @return \type{User} The corresponding User object
372 static function newFromId( $id ) {
373 $u = new User;
374 $u->mId = $id;
375 $u->mFrom = 'id';
376 return $u;
380 * Factory method to fetch whichever user has a given email confirmation code.
381 * This code is generated when an account is created or its e-mail address
382 * has changed.
384 * If the code is invalid or has expired, returns NULL.
386 * @param $code \string Confirmation code
387 * @return \type{User}
389 static function newFromConfirmationCode( $code ) {
390 $dbr = wfGetDB( DB_SLAVE );
391 $id = $dbr->selectField( 'user', 'user_id', array(
392 'user_email_token' => md5( $code ),
393 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
394 ) );
395 if( $id !== false ) {
396 return User::newFromId( $id );
397 } else {
398 return null;
403 * Create a new user object using data from session or cookies. If the
404 * login credentials are invalid, the result is an anonymous user.
406 * @return \type{User}
408 static function newFromSession() {
409 $user = new User;
410 $user->mFrom = 'session';
411 return $user;
415 * Create a new user object from a user row.
416 * The row should have all fields from the user table in it.
417 * @param $row array A row from the user table
418 * @return \type{User}
420 static function newFromRow( $row ) {
421 $user = new User;
422 $user->loadFromRow( $row );
423 return $user;
426 //@}
430 * Get the username corresponding to a given user ID
431 * @param $id \int User ID
432 * @return \string The corresponding username
434 static function whoIs( $id ) {
435 $dbr = wfGetDB( DB_SLAVE );
436 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
440 * Get the real name of a user given their user ID
442 * @param $id \int User ID
443 * @return \string The corresponding user's real name
445 static function whoIsReal( $id ) {
446 $dbr = wfGetDB( DB_SLAVE );
447 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
451 * Get database id given a user name
452 * @param $name \string Username
453 * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
455 static function idFromName( $name ) {
456 $nt = Title::makeTitleSafe( NS_USER, $name );
457 if( is_null( $nt ) ) {
458 # Illegal name
459 return null;
462 if ( isset(self::$idCacheByName[$name]) ) {
463 return self::$idCacheByName[$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 self::$idCacheByName[$name] = $result;
477 if ( count(self::$idCacheByName) > 1000 ) {
478 self::$idCacheByName = 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 * Check to see if the given clear-text password is one of the accepted passwords
2693 * @param $password \string user password.
2694 * @return \bool True if the given password is correct, otherwise False.
2696 function checkPassword( $password ) {
2697 global $wgAuth;
2698 $this->load();
2700 // Even though we stop people from creating passwords that
2701 // are shorter than this, doesn't mean people wont be able
2702 // to. Certain authentication plugins do NOT want to save
2703 // domain passwords in a mysql database, so we should
2704 // check this (incase $wgAuth->strict() is false).
2705 if( $this->isValidPassword( $password ) !== true ) {
2706 return false;
2709 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2710 return true;
2711 } elseif( $wgAuth->strict() ) {
2712 /* Auth plugin doesn't allow local authentication */
2713 return false;
2714 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2715 /* Auth plugin doesn't allow local authentication for this user name */
2716 return false;
2718 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2719 return true;
2720 } elseif ( function_exists( 'iconv' ) ) {
2721 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2722 # Check for this with iconv
2723 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2724 if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2725 return true;
2728 return false;
2732 * Check if the given clear-text password matches the temporary password
2733 * sent by e-mail for password reset operations.
2734 * @return \bool True if matches, false otherwise
2736 function checkTemporaryPassword( $plaintext ) {
2737 global $wgNewPasswordExpiry;
2738 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
2739 $this->load();
2740 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
2741 return ( time() < $expiry );
2742 } else {
2743 return false;
2748 * Initialize (if necessary) and return a session token value
2749 * which can be used in edit forms to show that the user's
2750 * login credentials aren't being hijacked with a foreign form
2751 * submission.
2753 * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
2754 * @return \string The new edit token
2756 function editToken( $salt = '' ) {
2757 if ( $this->isAnon() ) {
2758 return EDIT_TOKEN_SUFFIX;
2759 } else {
2760 if( !isset( $_SESSION['wsEditToken'] ) ) {
2761 $token = $this->generateToken();
2762 $_SESSION['wsEditToken'] = $token;
2763 } else {
2764 $token = $_SESSION['wsEditToken'];
2766 if( is_array( $salt ) ) {
2767 $salt = implode( '|', $salt );
2769 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2774 * Generate a looking random token for various uses.
2776 * @param $salt \string Optional salt value
2777 * @return \string The new random token
2779 function generateToken( $salt = '' ) {
2780 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2781 return md5( $token . $salt );
2785 * Check given value against the token value stored in the session.
2786 * A match should confirm that the form was submitted from the
2787 * user's own login session, not a form submission from a third-party
2788 * site.
2790 * @param $val \string Input value to compare
2791 * @param $salt \string Optional function-specific data for hashing
2792 * @return \bool Whether the token matches
2794 function matchEditToken( $val, $salt = '' ) {
2795 $sessionToken = $this->editToken( $salt );
2796 if ( $val != $sessionToken ) {
2797 wfDebug( "User::matchEditToken: broken session data\n" );
2799 return $val == $sessionToken;
2803 * Check given value against the token value stored in the session,
2804 * ignoring the suffix.
2806 * @param $val \string Input value to compare
2807 * @param $salt \string Optional function-specific data for hashing
2808 * @return \bool Whether the token matches
2810 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2811 $sessionToken = $this->editToken( $salt );
2812 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2816 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2817 * mail to the user's given address.
2819 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
2821 function sendConfirmationMail() {
2822 global $wgLang;
2823 $expiration = null; // gets passed-by-ref and defined in next line.
2824 $token = $this->confirmationToken( $expiration );
2825 $url = $this->confirmationTokenUrl( $token );
2826 $invalidateURL = $this->invalidationTokenUrl( $token );
2827 $this->saveSettings();
2829 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2830 wfMsg( 'confirmemail_body',
2831 wfGetIP(),
2832 $this->getName(),
2833 $url,
2834 $wgLang->timeanddate( $expiration, false ),
2835 $invalidateURL,
2836 $wgLang->date( $expiration, false ),
2837 $wgLang->time( $expiration, false ) ) );
2841 * Send an e-mail to this user's account. Does not check for
2842 * confirmed status or validity.
2844 * @param $subject \string Message subject
2845 * @param $body \string Message body
2846 * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
2847 * @param $replyto \string Reply-To address
2848 * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
2850 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2851 if( is_null( $from ) ) {
2852 global $wgPasswordSender;
2853 $from = $wgPasswordSender;
2856 $to = new MailAddress( $this );
2857 $sender = new MailAddress( $from );
2858 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2862 * Generate, store, and return a new e-mail confirmation code.
2863 * A hash (unsalted, since it's used as a key) is stored.
2865 * @note Call saveSettings() after calling this function to commit
2866 * this change to the database.
2868 * @param[out] &$expiration \mixed Accepts the expiration time
2869 * @return \string New token
2870 * @private
2872 function confirmationToken( &$expiration ) {
2873 $now = time();
2874 $expires = $now + 7 * 24 * 60 * 60;
2875 $expiration = wfTimestamp( TS_MW, $expires );
2876 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2877 $hash = md5( $token );
2878 $this->load();
2879 $this->mEmailToken = $hash;
2880 $this->mEmailTokenExpires = $expiration;
2881 return $token;
2885 * Return a URL the user can use to confirm their email address.
2886 * @param $token \string Accepts the email confirmation token
2887 * @return \string New token URL
2888 * @private
2890 function confirmationTokenUrl( $token ) {
2891 return $this->getTokenUrl( 'ConfirmEmail', $token );
2894 * Return a URL the user can use to invalidate their email address.
2895 * @param $token \string Accepts the email confirmation token
2896 * @return \string New token URL
2897 * @private
2899 function invalidationTokenUrl( $token ) {
2900 return $this->getTokenUrl( 'Invalidateemail', $token );
2904 * Internal function to format the e-mail validation/invalidation URLs.
2905 * This uses $wgArticlePath directly as a quickie hack to use the
2906 * hardcoded English names of the Special: pages, for ASCII safety.
2908 * @note Since these URLs get dropped directly into emails, using the
2909 * short English names avoids insanely long URL-encoded links, which
2910 * also sometimes can get corrupted in some browsers/mailers
2911 * (bug 6957 with Gmail and Internet Explorer).
2913 * @param $page \string Special page
2914 * @param $token \string Token
2915 * @return \string Formatted URL
2917 protected function getTokenUrl( $page, $token ) {
2918 global $wgArticlePath;
2919 return wfExpandUrl(
2920 str_replace(
2921 '$1',
2922 "Special:$page/$token",
2923 $wgArticlePath ) );
2927 * Mark the e-mail address confirmed.
2929 * @note Call saveSettings() after calling this function to commit the change.
2931 function confirmEmail() {
2932 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2933 return true;
2937 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2938 * address if it was already confirmed.
2940 * @note Call saveSettings() after calling this function to commit the change.
2942 function invalidateEmail() {
2943 $this->load();
2944 $this->mEmailToken = null;
2945 $this->mEmailTokenExpires = null;
2946 $this->setEmailAuthenticationTimestamp( null );
2947 return true;
2951 * Set the e-mail authentication timestamp.
2952 * @param $timestamp \string TS_MW timestamp
2954 function setEmailAuthenticationTimestamp( $timestamp ) {
2955 $this->load();
2956 $this->mEmailAuthenticated = $timestamp;
2957 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2961 * Is this user allowed to send e-mails within limits of current
2962 * site configuration?
2963 * @return \bool True if allowed
2965 function canSendEmail() {
2966 global $wgEnableEmail, $wgEnableUserEmail;
2967 if( !$wgEnableEmail || !$wgEnableUserEmail ) {
2968 return false;
2970 $canSend = $this->isEmailConfirmed();
2971 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2972 return $canSend;
2976 * Is this user allowed to receive e-mails within limits of current
2977 * site configuration?
2978 * @return \bool True if allowed
2980 function canReceiveEmail() {
2981 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
2985 * Is this user's e-mail address valid-looking and confirmed within
2986 * limits of the current site configuration?
2988 * @note If $wgEmailAuthentication is on, this may require the user to have
2989 * confirmed their address by returning a code or using a password
2990 * sent to the address from the wiki.
2992 * @return \bool True if confirmed
2994 function isEmailConfirmed() {
2995 global $wgEmailAuthentication;
2996 $this->load();
2997 $confirmed = true;
2998 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2999 if( $this->isAnon() )
3000 return false;
3001 if( !self::isValidEmailAddr( $this->mEmail ) )
3002 return false;
3003 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
3004 return false;
3005 return true;
3006 } else {
3007 return $confirmed;
3012 * Check whether there is an outstanding request for e-mail confirmation.
3013 * @return \bool True if pending
3015 function isEmailConfirmationPending() {
3016 global $wgEmailAuthentication;
3017 return $wgEmailAuthentication &&
3018 !$this->isEmailConfirmed() &&
3019 $this->mEmailToken &&
3020 $this->mEmailTokenExpires > wfTimestamp();
3024 * Get the timestamp of account creation.
3026 * @return \types{\string,\bool} string Timestamp of account creation, or false for
3027 * non-existent/anonymous user accounts.
3029 public function getRegistration() {
3030 return $this->getId() > 0
3031 ? $this->mRegistration
3032 : false;
3036 * Get the timestamp of the first edit
3038 * @return \types{\string,\bool} string Timestamp of first edit, or false for
3039 * non-existent/anonymous user accounts.
3041 public function getFirstEditTimestamp() {
3042 if( $this->getId() == 0 ) return false; // anons
3043 $dbr = wfGetDB( DB_SLAVE );
3044 $time = $dbr->selectField( 'revision', 'rev_timestamp',
3045 array( 'rev_user' => $this->getId() ),
3046 __METHOD__,
3047 array( 'ORDER BY' => 'rev_timestamp ASC' )
3049 if( !$time ) return false; // no edits
3050 return wfTimestamp( TS_MW, $time );
3054 * Get the permissions associated with a given list of groups
3056 * @param $groups \type{\arrayof{\string}} List of internal group names
3057 * @return \type{\arrayof{\string}} List of permission key names for given groups combined
3059 static function getGroupPermissions( $groups ) {
3060 global $wgGroupPermissions, $wgRevokePermissions;
3061 $rights = array();
3062 // grant every granted permission first
3063 foreach( $groups as $group ) {
3064 if( isset( $wgGroupPermissions[$group] ) ) {
3065 $rights = array_merge( $rights,
3066 // array_filter removes empty items
3067 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
3070 // now revoke the revoked permissions
3071 foreach( $groups as $group ) {
3072 if( isset( $wgRevokePermissions[$group] ) ) {
3073 $rights = array_diff( $rights,
3074 array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
3077 return array_unique($rights);
3081 * Get all the groups who have a given permission
3083 * @param $role \string Role to check
3084 * @return \type{\arrayof{\string}} List of internal group names with the given permission
3086 static function getGroupsWithPermission( $role ) {
3087 global $wgGroupPermissions;
3088 $allowedGroups = array();
3089 foreach ( $wgGroupPermissions as $group => $rights ) {
3090 if ( isset( $rights[$role] ) && $rights[$role] ) {
3091 $allowedGroups[] = $group;
3094 return $allowedGroups;
3098 * Get the localized descriptive name for a group, if it exists
3100 * @param $group \string Internal group name
3101 * @return \string Localized descriptive group name
3103 static function getGroupName( $group ) {
3104 global $wgMessageCache;
3105 $wgMessageCache->loadAllMessages();
3106 $key = "group-$group";
3107 $name = wfMsg( $key );
3108 return $name == '' || wfEmptyMsg( $key, $name )
3109 ? $group
3110 : $name;
3114 * Get the localized descriptive name for a member of a group, if it exists
3116 * @param $group \string Internal group name
3117 * @return \string Localized name for group member
3119 static function getGroupMember( $group ) {
3120 global $wgMessageCache;
3121 $wgMessageCache->loadAllMessages();
3122 $key = "group-$group-member";
3123 $name = wfMsg( $key );
3124 return $name == '' || wfEmptyMsg( $key, $name )
3125 ? $group
3126 : $name;
3130 * Return the set of defined explicit groups.
3131 * The implicit groups (by default *, 'user' and 'autoconfirmed')
3132 * are not included, as they are defined automatically, not in the database.
3133 * @return \type{\arrayof{\string}} Array of internal group names
3135 static function getAllGroups() {
3136 global $wgGroupPermissions, $wgRevokePermissions;
3137 return array_diff(
3138 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
3139 self::getImplicitGroups()
3144 * Get a list of all available permissions.
3145 * @return \type{\arrayof{\string}} Array of permission names
3147 static function getAllRights() {
3148 if ( self::$mAllRights === false ) {
3149 global $wgAvailableRights;
3150 if ( count( $wgAvailableRights ) ) {
3151 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3152 } else {
3153 self::$mAllRights = self::$mCoreRights;
3155 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3157 return self::$mAllRights;
3161 * Get a list of implicit groups
3162 * @return \type{\arrayof{\string}} Array of internal group names
3164 public static function getImplicitGroups() {
3165 global $wgImplicitGroups;
3166 $groups = $wgImplicitGroups;
3167 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
3168 return $groups;
3172 * Get the title of a page describing a particular group
3174 * @param $group \string Internal group name
3175 * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
3177 static function getGroupPage( $group ) {
3178 global $wgMessageCache;
3179 $wgMessageCache->loadAllMessages();
3180 $page = wfMsgForContent( 'grouppage-' . $group );
3181 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3182 $title = Title::newFromText( $page );
3183 if( is_object( $title ) )
3184 return $title;
3186 return false;
3190 * Create a link to the group in HTML, if available;
3191 * else return the group name.
3193 * @param $group \string Internal name of the group
3194 * @param $text \string The text of the link
3195 * @return \string HTML link to the group
3197 static function makeGroupLinkHTML( $group, $text = '' ) {
3198 if( $text == '' ) {
3199 $text = self::getGroupName( $group );
3201 $title = self::getGroupPage( $group );
3202 if( $title ) {
3203 global $wgUser;
3204 $sk = $wgUser->getSkin();
3205 return $sk->link( $title, htmlspecialchars( $text ) );
3206 } else {
3207 return $text;
3212 * Create a link to the group in Wikitext, if available;
3213 * else return the group name.
3215 * @param $group \string Internal name of the group
3216 * @param $text \string The text of the link
3217 * @return \string Wikilink to the group
3219 static function makeGroupLinkWiki( $group, $text = '' ) {
3220 if( $text == '' ) {
3221 $text = self::getGroupName( $group );
3223 $title = self::getGroupPage( $group );
3224 if( $title ) {
3225 $page = $title->getPrefixedText();
3226 return "[[$page|$text]]";
3227 } else {
3228 return $text;
3233 * Returns an array of the groups that a particular group can add/remove.
3235 * @param $group String: the group to check for whether it can add/remove
3236 * @return Array array( 'add' => array( addablegroups ),
3237 * 'remove' => array( removablegroups ),
3238 * 'add-self' => array( addablegroups to self),
3239 * 'remove-self' => array( removable groups from self) )
3241 static function changeableByGroup( $group ) {
3242 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
3244 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
3245 if( empty($wgAddGroups[$group]) ) {
3246 // Don't add anything to $groups
3247 } elseif( $wgAddGroups[$group] === true ) {
3248 // You get everything
3249 $groups['add'] = self::getAllGroups();
3250 } elseif( is_array($wgAddGroups[$group]) ) {
3251 $groups['add'] = $wgAddGroups[$group];
3254 // Same thing for remove
3255 if( empty($wgRemoveGroups[$group]) ) {
3256 } elseif($wgRemoveGroups[$group] === true ) {
3257 $groups['remove'] = self::getAllGroups();
3258 } elseif( is_array($wgRemoveGroups[$group]) ) {
3259 $groups['remove'] = $wgRemoveGroups[$group];
3262 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
3263 if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
3264 foreach($wgGroupsAddToSelf as $key => $value) {
3265 if( is_int($key) ) {
3266 $wgGroupsAddToSelf['user'][] = $value;
3271 if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
3272 foreach($wgGroupsRemoveFromSelf as $key => $value) {
3273 if( is_int($key) ) {
3274 $wgGroupsRemoveFromSelf['user'][] = $value;
3279 // Now figure out what groups the user can add to him/herself
3280 if( empty($wgGroupsAddToSelf[$group]) ) {
3281 } elseif( $wgGroupsAddToSelf[$group] === true ) {
3282 // No idea WHY this would be used, but it's there
3283 $groups['add-self'] = User::getAllGroups();
3284 } elseif( is_array($wgGroupsAddToSelf[$group]) ) {
3285 $groups['add-self'] = $wgGroupsAddToSelf[$group];
3288 if( empty($wgGroupsRemoveFromSelf[$group]) ) {
3289 } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
3290 $groups['remove-self'] = User::getAllGroups();
3291 } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) {
3292 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
3295 return $groups;
3299 * Returns an array of groups that this user can add and remove
3300 * @return Array array( 'add' => array( addablegroups ),
3301 * 'remove' => array( removablegroups ),
3302 * 'add-self' => array( addablegroups to self),
3303 * 'remove-self' => array( removable groups from self) )
3305 function changeableGroups() {
3306 if( $this->isAllowed( 'userrights' ) ) {
3307 // This group gives the right to modify everything (reverse-
3308 // compatibility with old "userrights lets you change
3309 // everything")
3310 // Using array_merge to make the groups reindexed
3311 $all = array_merge( User::getAllGroups() );
3312 return array(
3313 'add' => $all,
3314 'remove' => $all,
3315 'add-self' => array(),
3316 'remove-self' => array()
3320 // Okay, it's not so simple, we will have to go through the arrays
3321 $groups = array(
3322 'add' => array(),
3323 'remove' => array(),
3324 'add-self' => array(),
3325 'remove-self' => array() );
3326 $addergroups = $this->getEffectiveGroups();
3328 foreach ($addergroups as $addergroup) {
3329 $groups = array_merge_recursive(
3330 $groups, $this->changeableByGroup($addergroup)
3332 $groups['add'] = array_unique( $groups['add'] );
3333 $groups['remove'] = array_unique( $groups['remove'] );
3334 $groups['add-self'] = array_unique( $groups['add-self'] );
3335 $groups['remove-self'] = array_unique( $groups['remove-self'] );
3337 return $groups;
3341 * Increment the user's edit-count field.
3342 * Will have no effect for anonymous users.
3344 function incEditCount() {
3345 if( !$this->isAnon() ) {
3346 $dbw = wfGetDB( DB_MASTER );
3347 $dbw->update( 'user',
3348 array( 'user_editcount=user_editcount+1' ),
3349 array( 'user_id' => $this->getId() ),
3350 __METHOD__ );
3352 // Lazy initialization check...
3353 if( $dbw->affectedRows() == 0 ) {
3354 // Pull from a slave to be less cruel to servers
3355 // Accuracy isn't the point anyway here
3356 $dbr = wfGetDB( DB_SLAVE );
3357 $count = $dbr->selectField( 'revision',
3358 'COUNT(rev_user)',
3359 array( 'rev_user' => $this->getId() ),
3360 __METHOD__ );
3362 // Now here's a goddamn hack...
3363 if( $dbr !== $dbw ) {
3364 // If we actually have a slave server, the count is
3365 // at least one behind because the current transaction
3366 // has not been committed and replicated.
3367 $count++;
3368 } else {
3369 // But if DB_SLAVE is selecting the master, then the
3370 // count we just read includes the revision that was
3371 // just added in the working transaction.
3374 $dbw->update( 'user',
3375 array( 'user_editcount' => $count ),
3376 array( 'user_id' => $this->getId() ),
3377 __METHOD__ );
3380 // edit count in user cache too
3381 $this->invalidateCache();
3385 * Get the description of a given right
3387 * @param $right \string Right to query
3388 * @return \string Localized description of the right
3390 static function getRightDescription( $right ) {
3391 global $wgMessageCache;
3392 $wgMessageCache->loadAllMessages();
3393 $key = "right-$right";
3394 $name = wfMsg( $key );
3395 return $name == '' || wfEmptyMsg( $key, $name )
3396 ? $right
3397 : $name;
3401 * Make an old-style password hash
3403 * @param $password \string Plain-text password
3404 * @param $userId \string User ID
3405 * @return \string Password hash
3407 static function oldCrypt( $password, $userId ) {
3408 global $wgPasswordSalt;
3409 if ( $wgPasswordSalt ) {
3410 return md5( $userId . '-' . md5( $password ) );
3411 } else {
3412 return md5( $password );
3417 * Make a new-style password hash
3419 * @param $password \string Plain-text password
3420 * @param $salt \string Optional salt, may be random or the user ID.
3421 * If unspecified or false, will generate one automatically
3422 * @return \string Password hash
3424 static function crypt( $password, $salt = false ) {
3425 global $wgPasswordSalt;
3427 $hash = '';
3428 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
3429 return $hash;
3432 if( $wgPasswordSalt ) {
3433 if ( $salt === false ) {
3434 $salt = substr( wfGenerateToken(), 0, 8 );
3436 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3437 } else {
3438 return ':A:' . md5( $password );
3443 * Compare a password hash with a plain-text password. Requires the user
3444 * ID if there's a chance that the hash is an old-style hash.
3446 * @param $hash \string Password hash
3447 * @param $password \string Plain-text password to compare
3448 * @param $userId \string User ID for old-style password salt
3449 * @return \bool
3451 static function comparePasswords( $hash, $password, $userId = false ) {
3452 $m = false;
3453 $type = substr( $hash, 0, 3 );
3455 $result = false;
3456 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
3457 return $result;
3460 if ( $type == ':A:' ) {
3461 # Unsalted
3462 return md5( $password ) === substr( $hash, 3 );
3463 } elseif ( $type == ':B:' ) {
3464 # Salted
3465 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3466 return md5( $salt.'-'.md5( $password ) ) == $realHash;
3467 } else {
3468 # Old-style
3469 return self::oldCrypt( $password, $userId ) === $hash;
3474 * Add a newuser log entry for this user
3475 * @param $byEmail Boolean: account made by email?
3477 public function addNewUserLogEntry( $byEmail = false ) {
3478 global $wgUser, $wgContLang, $wgNewUserLog;
3479 if( empty($wgNewUserLog) ) {
3480 return true; // disabled
3482 $talk = $wgContLang->getFormattedNsText( NS_TALK );
3483 if( $this->getName() == $wgUser->getName() ) {
3484 $action = 'create';
3485 $message = '';
3486 } else {
3487 $action = 'create2';
3488 $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
3490 $log = new LogPage( 'newusers' );
3491 $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
3492 return true;
3496 * Add an autocreate newuser log entry for this user
3497 * Used by things like CentralAuth and perhaps other authplugins.
3499 public function addNewUserLogEntryAutoCreate() {
3500 global $wgNewUserLog;
3501 if( empty($wgNewUserLog) ) {
3502 return true; // disabled
3504 $log = new LogPage( 'newusers', false );
3505 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3506 return true;
3509 protected function loadOptions() {
3510 $this->load();
3511 if ($this->mOptionsLoaded || !$this->getId() )
3512 return;
3514 $this->mOptions = self::getDefaultOptions();
3516 // Maybe load from the object
3518 if ( !is_null($this->mOptionOverrides) ) {
3519 wfDebug( "Loading options for user ".$this->getId()." from override cache.\n" );
3520 foreach( $this->mOptionOverrides as $key => $value ) {
3521 $this->mOptions[$key] = $value;
3523 } else {
3524 wfDebug( "Loading options for user ".$this->getId()." from database.\n" );
3525 // Load from database
3526 $dbr = wfGetDB( DB_SLAVE );
3528 $res = $dbr->select( 'user_properties',
3529 '*',
3530 array('up_user' => $this->getId()),
3531 __METHOD__
3534 while( $row = $dbr->fetchObject( $res ) ) {
3535 $this->mOptionOverrides[$row->up_property] = $row->up_value;
3536 $this->mOptions[$row->up_property] = $row->up_value;
3540 $this->mOptionsLoaded = true;
3542 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
3545 protected function saveOptions() {
3546 global $wgAllowPrefChange;
3548 $extuser = ExternalUser::newFromUser( $this );
3550 $this->loadOptions();
3551 $dbw = wfGetDB( DB_MASTER );
3553 $insert_rows = array();
3555 $saveOptions = $this->mOptions;
3557 // Allow hooks to abort, for instance to save to a global profile.
3558 // Reset options to default state before saving.
3559 if (!wfRunHooks( 'UserSaveOptions', array($this, &$saveOptions) ) )
3560 return;
3562 foreach( $saveOptions as $key => $value ) {
3563 # Don't bother storing default values
3564 if ( ( is_null( self::getDefaultOption( $key ) ) &&
3565 !( $value === false || is_null($value) ) ) ||
3566 $value != self::getDefaultOption( $key ) ) {
3567 $insert_rows[] = array(
3568 'up_user' => $this->getId(),
3569 'up_property' => $key,
3570 'up_value' => $value,
3573 if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
3574 switch ( $wgAllowPrefChange[$key] ) {
3575 case 'local': case 'message':
3576 break;
3577 case 'semiglobal': case 'global':
3578 $extuser->setPref( $key, $value );
3583 $dbw->begin();
3584 $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ );
3585 $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
3586 $dbw->commit();
3590 * Provide an array of HTML 5 attributes to put on an input element
3591 * intended for the user to enter a new password. This may include
3592 * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
3594 * Do *not* use this when asking the user to enter his current password!
3595 * Regardless of configuration, users may have invalid passwords for whatever
3596 * reason (e.g., they were set before requirements were tightened up).
3597 * Only use it when asking for a new password, like on account creation or
3598 * ResetPass.
3600 * Obviously, you still need to do server-side checking.
3602 * @return array Array of HTML attributes suitable for feeding to
3603 * Html::element(), directly or indirectly. (Don't feed to Xml::*()!
3604 * That will potentially output invalid XHTML 1.0 Transitional, and will
3605 * get confused by the boolean attribute syntax used.)
3607 public static function passwordChangeInputAttribs() {
3608 global $wgMinimalPasswordLength;
3610 if ( $wgMinimalPasswordLength == 0 ) {
3611 return array();
3614 # Note that the pattern requirement will always be satisfied if the
3615 # input is empty, so we need required in all cases.
3616 $ret = array( 'required' );
3618 # We can't actually do this right now, because Opera 9.6 will print out
3619 # the entered password visibly in its error message! When other
3620 # browsers add support for this attribute, or Opera fixes its support,
3621 # we can add support with a version check to avoid doing this on Opera
3622 # versions where it will be a problem. Reported to Opera as
3623 # DSK-262266, but they don't have a public bug tracker for us to follow.
3625 if ( $wgMinimalPasswordLength > 1 ) {
3626 $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
3627 $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag',
3628 $wgMinimalPasswordLength );
3632 return $ret;