(bug 10323) Special:Undelete should have "inverse selection" button
[mediawiki.git] / includes / User.php
blobf49ebf69a230e26c735112eb4dcab62af13bf04c
1 <?php
2 /**
3 * Implements the User class for the %MediaWiki software.
4 * @file
5 */
7 /**
8 * \type{\int} Number of characters in user_token field.
9 * @ingroup Constants
11 define( 'USER_TOKEN_LENGTH', 32 );
13 /**
14 * \type{\int} Serialized record version.
15 * @ingroup Constants
17 define( 'MW_USER_VERSION', 6 );
19 /**
20 * \type{\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',
96 /**
97 * \type{\arrayof{\string}} List of member variables which are saved to the
98 * shared cache (memcached). Any operation which changes the
99 * corresponding database fields must call a cache-clearing function.
100 * @showinitializer
102 static $mCacheVars = array(
103 // user table
104 'mId',
105 'mName',
106 'mRealName',
107 'mPassword',
108 'mNewpassword',
109 'mNewpassTime',
110 'mEmail',
111 'mOptions',
112 'mTouched',
113 'mToken',
114 'mEmailAuthenticated',
115 'mEmailToken',
116 'mEmailTokenExpires',
117 'mRegistration',
118 'mEditCount',
119 // user_group table
120 'mGroups',
124 * \type{\arrayof{\string}} Core rights.
125 * Each of these should have a corresponding message of the form
126 * "right-$right".
127 * @showinitializer
129 static $mCoreRights = array(
130 'apihighlimits',
131 'autoconfirmed',
132 'autopatrol',
133 'bigdelete',
134 'block',
135 'blockemail',
136 'bot',
137 'browsearchive',
138 'createaccount',
139 'createpage',
140 'createtalk',
141 'delete',
142 'deletedhistory',
143 'edit',
144 'editinterface',
145 'editusercssjs',
146 'import',
147 'importupload',
148 'ipblock-exempt',
149 'markbotedits',
150 'minoredit',
151 'move',
152 'nominornewtalk',
153 'noratelimit',
154 'patrol',
155 'protect',
156 'proxyunbannable',
157 'purge',
158 'read',
159 'reupload',
160 'reupload-shared',
161 'rollback',
162 'siteadmin',
163 'suppressredirect',
164 'trackback',
165 'undelete',
166 'unwatchedpages',
167 'upload',
168 'upload_by_url',
169 'userrights',
172 * \type{\string} Cached results of getAllRights()
174 static $mAllRights = false;
176 /** @name Cache variables */
177 //@{
178 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
179 $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
180 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
181 //@}
184 * \type{\bool} Whether the cache variables have been loaded.
186 var $mDataLoaded, $mAuthLoaded;
189 * \type{\string} Initialization data source if mDataLoaded==false. May be one of:
190 * - 'defaults' anonymous user initialised from class defaults
191 * - 'name' initialise from mName
192 * - 'id' initialise from mId
193 * - 'session' log in from cookies or session if possible
195 * Use the User::newFrom*() family of functions to set this.
197 var $mFrom;
199 /** @name Lazy-initialized variables, invalidated with clearInstanceCache */
200 //@{
201 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
202 $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
203 $mLocked, $mHideName;
204 //@}
207 * Lightweight constructor for an anonymous user.
208 * Use the User::newFrom* factory functions for other kinds of users.
210 * @see newFromName()
211 * @see newFromId()
212 * @see newFromConfirmationCode()
213 * @see newFromSession()
214 * @see newFromRow()
216 function User() {
217 $this->clearInstanceCache( 'defaults' );
221 * Load the user table data for this object from the source given by mFrom.
223 function load() {
224 if ( $this->mDataLoaded ) {
225 return;
227 wfProfileIn( __METHOD__ );
229 # Set it now to avoid infinite recursion in accessors
230 $this->mDataLoaded = true;
232 switch ( $this->mFrom ) {
233 case 'defaults':
234 $this->loadDefaults();
235 break;
236 case 'name':
237 $this->mId = self::idFromName( $this->mName );
238 if ( !$this->mId ) {
239 # Nonexistent user placeholder object
240 $this->loadDefaults( $this->mName );
241 } else {
242 $this->loadFromId();
244 break;
245 case 'id':
246 $this->loadFromId();
247 break;
248 case 'session':
249 $this->loadFromSession();
250 break;
251 default:
252 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
254 wfProfileOut( __METHOD__ );
257 protected function callAuthPlugin( $fname /* $args */ ) {
258 $args = func_get_args();
259 array_shift( $args );
260 // Load auth plugin conterpart functions for User functions
261 if( !$this->mAuthLoaded ) {
262 global $wgAuth;
263 $this->mAuthCallbacks = array();
264 $wgAuth->setUserCallbacks( $this, $this->mAuthCallbacks );
265 $this->mAuthLoaded = true;
267 // Try to call the auth plugin version of this function
268 if( isset($this->mAuthCallbacks[$fname]) && is_callable($this->mAuthCallbacks[$fname]) ) {
269 return call_user_func_array( $this->mAuthCallbacks[$fname], $args );
271 return NULL;
275 * Load user table data, given mId has already been set.
276 * @return \type{\bool} false if the ID does not exist, true otherwise
277 * @private
279 function loadFromId() {
280 global $wgMemc;
281 if ( $this->mId == 0 ) {
282 $this->loadDefaults();
283 return false;
286 # Try cache
287 $key = wfMemcKey( 'user', 'id', $this->mId );
288 $data = $wgMemc->get( $key );
289 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
290 # Object is expired, load from DB
291 $data = false;
294 if ( !$data ) {
295 wfDebug( "Cache miss for user {$this->mId}\n" );
296 # Load from DB
297 if ( !$this->loadFromDatabase() ) {
298 # Can't load from ID, user is anonymous
299 return false;
301 $this->saveToCache();
302 } else {
303 wfDebug( "Got user {$this->mId} from cache\n" );
304 # Restore from cache
305 foreach ( self::$mCacheVars as $name ) {
306 $this->$name = $data[$name];
309 return true;
313 * Save user data to the shared cache
315 function saveToCache() {
316 $this->load();
317 $this->loadGroups();
318 if ( $this->isAnon() ) {
319 // Anonymous users are uncached
320 return;
322 $data = array();
323 foreach ( self::$mCacheVars as $name ) {
324 $data[$name] = $this->$name;
326 $data['mVersion'] = MW_USER_VERSION;
327 $key = wfMemcKey( 'user', 'id', $this->mId );
328 global $wgMemc;
329 $wgMemc->set( $key, $data );
333 /** @name newFrom*() static factory methods */
334 //@{
337 * Static factory method for creation from username.
339 * This is slightly less efficient than newFromId(), so use newFromId() if
340 * you have both an ID and a name handy.
342 * @param $name \type{\string} Username, validated by Title::newFromText()
343 * @param $validate \type{\mixed} Validate username. Takes the same parameters as
344 * User::getCanonicalName(), except that true is accepted as an alias
345 * for 'valid', for BC.
347 * @return \type{User} The User object, or null if the username is invalid. If the
348 * username is not present in the database, the result will be a user object
349 * with a name, zero user ID and default settings.
351 static function newFromName( $name, $validate = 'valid' ) {
352 if ( $validate === true ) {
353 $validate = 'valid';
355 $name = self::getCanonicalName( $name, $validate );
356 if ( $name === false ) {
357 return null;
358 } else {
359 # Create unloaded user object
360 $u = new User;
361 $u->mName = $name;
362 $u->mFrom = 'name';
363 return $u;
368 * Static factory method for creation from a given user ID.
370 * @param $id \type{\int} Valid user ID
371 * @return \type{User} The corresponding User object
373 static function newFromId( $id ) {
374 $u = new User;
375 $u->mId = $id;
376 $u->mFrom = 'id';
377 return $u;
381 * Factory method to fetch whichever user has a given email confirmation code.
382 * This code is generated when an account is created or its e-mail address
383 * has changed.
385 * If the code is invalid or has expired, returns NULL.
387 * @param $code \type{\string} Confirmation code
388 * @return \type{User}
390 static function newFromConfirmationCode( $code ) {
391 $dbr = wfGetDB( DB_SLAVE );
392 $id = $dbr->selectField( 'user', 'user_id', array(
393 'user_email_token' => md5( $code ),
394 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
395 ) );
396 if( $id !== false ) {
397 return User::newFromId( $id );
398 } else {
399 return null;
404 * Create a new user object using data from session or cookies. If the
405 * login credentials are invalid, the result is an anonymous user.
407 * @return \type{User}
409 static function newFromSession() {
410 $user = new User;
411 $user->mFrom = 'session';
412 return $user;
416 * Create a new user object from a user row.
417 * The row should have all fields from the user table in it.
418 * @param $row array A row from the user table
419 * @return \type{User}
421 static function newFromRow( $row ) {
422 $user = new User;
423 $user->loadFromRow( $row );
424 return $user;
427 //@}
431 * Get the username corresponding to a given user ID
432 * @param $id \type{\int} %User ID
433 * @return \type{\string} The corresponding username
435 static function whoIs( $id ) {
436 $dbr = wfGetDB( DB_SLAVE );
437 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
441 * Get the real name of a user given their user ID
443 * @param $id \type{\int} %User ID
444 * @return \type{\string} The corresponding user's real name
446 static function whoIsReal( $id ) {
447 $dbr = wfGetDB( DB_SLAVE );
448 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
452 * Get database id given a user name
453 * @param $name \type{\string} Username
454 * @return \twotypes{\int,\null} The corresponding user's ID, or null if user is nonexistent
455 * @static
457 static function idFromName( $name ) {
458 $nt = Title::newFromText( $name );
459 if( is_null( $nt ) ) {
460 # Illegal name
461 return null;
463 $dbr = wfGetDB( DB_SLAVE );
464 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
466 if ( $s === false ) {
467 return 0;
468 } else {
469 return $s->user_id;
474 * Does the string match an anonymous IPv4 address?
476 * This function exists for username validation, in order to reject
477 * usernames which are similar in form to IP addresses. Strings such
478 * as 300.300.300.300 will return true because it looks like an IP
479 * address, despite not being strictly valid.
481 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
482 * address because the usemod software would "cloak" anonymous IP
483 * addresses like this, if we allowed accounts like this to be created
484 * new users could get the old edits of these anonymous users.
486 * @param $name \type{\string} String to match
487 * @return \type{\bool} True or false
489 static function isIP( $name ) {
490 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
494 * Is the input a valid username?
496 * Checks if the input is a valid username, we don't want an empty string,
497 * an IP address, anything that containins slashes (would mess up subpages),
498 * is longer than the maximum allowed username size or doesn't begin with
499 * a capital letter.
501 * @param $name \type{\string} String to match
502 * @return \type{\bool} True or false
504 static function isValidUserName( $name ) {
505 global $wgContLang, $wgMaxNameChars;
507 if ( $name == ''
508 || User::isIP( $name )
509 || strpos( $name, '/' ) !== false
510 || strlen( $name ) > $wgMaxNameChars
511 || $name != $wgContLang->ucfirst( $name ) ) {
512 wfDebugLog( 'username', __METHOD__ .
513 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
514 return false;
517 // Ensure that the name can't be misresolved as a different title,
518 // such as with extra namespace keys at the start.
519 $parsed = Title::newFromText( $name );
520 if( is_null( $parsed )
521 || $parsed->getNamespace()
522 || strcmp( $name, $parsed->getPrefixedText() ) ) {
523 wfDebugLog( 'username', __METHOD__ .
524 ": '$name' invalid due to ambiguous prefixes" );
525 return false;
528 // Check an additional blacklist of troublemaker characters.
529 // Should these be merged into the title char list?
530 $unicodeBlacklist = '/[' .
531 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
532 '\x{00a0}' . # non-breaking space
533 '\x{2000}-\x{200f}' . # various whitespace
534 '\x{2028}-\x{202f}' . # breaks and control chars
535 '\x{3000}' . # ideographic space
536 '\x{e000}-\x{f8ff}' . # private use
537 ']/u';
538 if( preg_match( $unicodeBlacklist, $name ) ) {
539 wfDebugLog( 'username', __METHOD__ .
540 ": '$name' invalid due to blacklisted characters" );
541 return false;
544 return true;
548 * Usernames which fail to pass this function will be blocked
549 * from user login and new account registrations, but may be used
550 * internally by batch processes.
552 * If an account already exists in this form, login will be blocked
553 * by a failure to pass this function.
555 * @param $name \type{\string} String to match
556 * @return \type{\bool} True or false
558 static function isUsableName( $name ) {
559 global $wgReservedUsernames;
560 // Must be a valid username, obviously ;)
561 if ( !self::isValidUserName( $name ) ) {
562 return false;
565 static $reservedUsernames = false;
566 if ( !$reservedUsernames ) {
567 $reservedUsernames = $wgReservedUsernames;
568 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
571 // Certain names may be reserved for batch processes.
572 foreach ( $reservedUsernames as $reserved ) {
573 if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
574 $reserved = wfMsgForContent( substr( $reserved, 4 ) );
576 if ( $reserved == $name ) {
577 return false;
580 return true;
584 * Usernames which fail to pass this function will be blocked
585 * from new account registrations, but may be used internally
586 * either by batch processes or by user accounts which have
587 * already been created.
589 * Additional character blacklisting may be added here
590 * rather than in isValidUserName() to avoid disrupting
591 * existing accounts.
593 * @param $name \type{\string} String to match
594 * @return \type{\bool} True or false
596 static function isCreatableName( $name ) {
597 return
598 self::isUsableName( $name ) &&
600 // Registration-time character blacklisting...
601 strpos( $name, '@' ) === false;
605 * Is the input a valid password for this user?
607 * @param $password \type{\string} Desired password
608 * @return \type{\bool} True or false
610 function isValidPassword( $password ) {
611 global $wgMinimalPasswordLength, $wgContLang;
613 $result = null;
614 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
615 return $result;
616 if( $result === false )
617 return false;
619 // Password needs to be long enough, and can't be the same as the username
620 return strlen( $password ) >= $wgMinimalPasswordLength
621 && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
625 * Does a string look like an e-mail address?
627 * There used to be a regular expression here, it got removed because it
628 * rejected valid addresses. Actually just check if there is '@' somewhere
629 * in the given address.
631 * @todo Check for RFC 2822 compilance (bug 959)
633 * @param $addr \type{\string} E-mail address
634 * @return \type{\bool} True or false
636 public static function isValidEmailAddr( $addr ) {
637 $result = null;
638 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
639 return $result;
642 return strpos( $addr, '@' ) !== false;
646 * Given unvalidated user input, return a canonical username, or false if
647 * the username is invalid.
648 * @param $name \type{\string} User input
649 * @param $validate \twotypes{\string,\bool} Type of validation to use:
650 * - false No validation
651 * - 'valid' Valid for batch processes
652 * - 'usable' Valid for batch processes and login
653 * - 'creatable' Valid for batch processes, login and account creation
655 static function getCanonicalName( $name, $validate = 'valid' ) {
656 # Force usernames to capital
657 global $wgContLang;
658 $name = $wgContLang->ucfirst( $name );
660 # Reject names containing '#'; these will be cleaned up
661 # with title normalisation, but then it's too late to
662 # check elsewhere
663 if( strpos( $name, '#' ) !== false )
664 return false;
666 # Clean up name according to title rules
667 $t = ($validate === 'valid') ?
668 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
669 # Check for invalid titles
670 if( is_null( $t ) ) {
671 return false;
674 # Reject various classes of invalid names
675 $name = $t->getText();
676 global $wgAuth;
677 $name = $wgAuth->getCanonicalName( $t->getText() );
679 switch ( $validate ) {
680 case false:
681 break;
682 case 'valid':
683 if ( !User::isValidUserName( $name ) ) {
684 $name = false;
686 break;
687 case 'usable':
688 if ( !User::isUsableName( $name ) ) {
689 $name = false;
691 break;
692 case 'creatable':
693 if ( !User::isCreatableName( $name ) ) {
694 $name = false;
696 break;
697 default:
698 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
700 return $name;
704 * Count the number of edits of a user
705 * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
707 * @param $uid \type{\int} %User ID to check
708 * @return \type{\int} The user's edit count
710 static function edits( $uid ) {
711 wfProfileIn( __METHOD__ );
712 $dbr = wfGetDB( DB_SLAVE );
713 // check if the user_editcount field has been initialized
714 $field = $dbr->selectField(
715 'user', 'user_editcount',
716 array( 'user_id' => $uid ),
717 __METHOD__
720 if( $field === null ) { // it has not been initialized. do so.
721 $dbw = wfGetDB( DB_MASTER );
722 $count = $dbr->selectField(
723 'revision', 'count(*)',
724 array( 'rev_user' => $uid ),
725 __METHOD__
727 $dbw->update(
728 'user',
729 array( 'user_editcount' => $count ),
730 array( 'user_id' => $uid ),
731 __METHOD__
733 } else {
734 $count = $field;
736 wfProfileOut( __METHOD__ );
737 return $count;
741 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
742 * @todo hash random numbers to improve security, like generateToken()
744 * @return \type{\string} New random password
746 static function randomPassword() {
747 global $wgMinimalPasswordLength;
748 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
749 $l = strlen( $pwchars ) - 1;
751 $pwlength = max( 7, $wgMinimalPasswordLength );
752 $digit = mt_rand(0, $pwlength - 1);
753 $np = '';
754 for ( $i = 0; $i < $pwlength; $i++ ) {
755 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
757 return $np;
761 * Set cached properties to default.
763 * @note This no longer clears uncached lazy-initialised properties;
764 * the constructor does that instead.
765 * @private
767 function loadDefaults( $name = false ) {
768 wfProfileIn( __METHOD__ );
770 global $wgCookiePrefix;
772 $this->mId = 0;
773 $this->mName = $name;
774 $this->mRealName = '';
775 $this->mPassword = $this->mNewpassword = '';
776 $this->mNewpassTime = null;
777 $this->mEmail = '';
778 $this->mOptions = null; # Defer init
780 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
781 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
782 } else {
783 $this->mTouched = '0'; # Allow any pages to be cached
786 $this->setToken(); # Random
787 $this->mEmailAuthenticated = null;
788 $this->mEmailToken = '';
789 $this->mEmailTokenExpires = null;
790 $this->mRegistration = wfTimestamp( TS_MW );
791 $this->mGroups = array();
793 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
795 wfProfileOut( __METHOD__ );
799 * @deprecated Use wfSetupSession().
801 function SetupSession() {
802 wfDeprecated( __METHOD__ );
803 wfSetupSession();
807 * Load user data from the session or login cookie. If there are no valid
808 * credentials, initialises the user as an anonymous user.
809 * @return \type{\bool} True if the user is logged in, false otherwise.
811 private function loadFromSession() {
812 global $wgMemc, $wgCookiePrefix;
814 $result = null;
815 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
816 if ( $result !== null ) {
817 return $result;
820 if ( isset( $_SESSION['wsUserID'] ) ) {
821 if ( 0 != $_SESSION['wsUserID'] ) {
822 $sId = $_SESSION['wsUserID'];
823 } else {
824 $this->loadDefaults();
825 return false;
827 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
828 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
829 $_SESSION['wsUserID'] = $sId;
830 } else {
831 $this->loadDefaults();
832 return false;
834 if ( isset( $_SESSION['wsUserName'] ) ) {
835 $sName = $_SESSION['wsUserName'];
836 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
837 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
838 $_SESSION['wsUserName'] = $sName;
839 } else {
840 $this->loadDefaults();
841 return false;
844 $passwordCorrect = FALSE;
845 $this->mId = $sId;
846 if ( !$this->loadFromId() ) {
847 # Not a valid ID, loadFromId has switched the object to anon for us
848 return false;
851 if ( isset( $_SESSION['wsToken'] ) ) {
852 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
853 $from = 'session';
854 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
855 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
856 $from = 'cookie';
857 } else {
858 # No session or persistent login cookie
859 $this->loadDefaults();
860 return false;
863 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
864 $_SESSION['wsToken'] = $this->mToken;
865 wfDebug( "Logged in from $from\n" );
866 return true;
867 } else {
868 # Invalid credentials
869 wfDebug( "Can't log in from $from, invalid credentials\n" );
870 $this->loadDefaults();
871 return false;
876 * Load user and user_group data from the database.
877 * $this::mId must be set, this is how the user is identified.
879 * @return \type{\bool} True if the user exists, false if the user is anonymous
880 * @private
882 function loadFromDatabase() {
883 # Paranoia
884 $this->mId = intval( $this->mId );
886 /** Anonymous user */
887 if( !$this->mId ) {
888 $this->loadDefaults();
889 return false;
892 $dbr = wfGetDB( DB_MASTER );
893 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
895 if ( $s !== false ) {
896 # Initialise user table data
897 $this->loadFromRow( $s );
898 $this->mGroups = null; // deferred
899 $this->getEditCount(); // revalidation for nulls
900 return true;
901 } else {
902 # Invalid user_id
903 $this->mId = 0;
904 $this->loadDefaults();
905 return false;
910 * Initialize this object from a row from the user table.
912 * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
914 function loadFromRow( $row ) {
915 $this->mDataLoaded = true;
917 if ( isset( $row->user_id ) ) {
918 $this->mId = $row->user_id;
920 $this->mName = $row->user_name;
921 $this->mRealName = $row->user_real_name;
922 $this->mPassword = $row->user_password;
923 $this->mNewpassword = $row->user_newpassword;
924 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
925 $this->mEmail = $row->user_email;
926 $this->decodeOptions( $row->user_options );
927 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
928 $this->mToken = $row->user_token;
929 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
930 $this->mEmailToken = $row->user_email_token;
931 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
932 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
933 $this->mEditCount = $row->user_editcount;
937 * Load the groups from the database if they aren't already loaded.
938 * @private
940 function loadGroups() {
941 if ( is_null( $this->mGroups ) ) {
942 $dbr = wfGetDB( DB_MASTER );
943 $res = $dbr->select( 'user_groups',
944 array( 'ug_group' ),
945 array( 'ug_user' => $this->mId ),
946 __METHOD__ );
947 $this->mGroups = array();
948 while( $row = $dbr->fetchObject( $res ) ) {
949 $this->mGroups[] = $row->ug_group;
955 * Clear various cached data stored in this object.
956 * @param $reloadFrom \type{\string} Reload user and user_groups table data from a
957 * given source. May be "name", "id", "defaults", "session", or false for
958 * no reload.
960 function clearInstanceCache( $reloadFrom = false ) {
961 $this->mNewtalk = -1;
962 $this->mDatePreference = null;
963 $this->mBlockedby = -1; # Unset
964 $this->mHash = false;
965 $this->mSkin = null;
966 $this->mRights = null;
967 $this->mEffectiveGroups = null;
969 if ( $reloadFrom ) {
970 $this->mDataLoaded = false;
971 $this->mFrom = $reloadFrom;
976 * Combine the language default options with any site-specific options
977 * and add the default language variants.
979 * @return \type{\arrayof{\string}} Array of options
981 static function getDefaultOptions() {
982 global $wgNamespacesToBeSearchedDefault;
984 * Site defaults will override the global/language defaults
986 global $wgDefaultUserOptions, $wgContLang;
987 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
990 * default language setting
992 $variant = $wgContLang->getPreferredVariant( false );
993 $defOpt['variant'] = $variant;
994 $defOpt['language'] = $variant;
996 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
997 $defOpt['searchNs'.$nsnum] = $val;
999 return $defOpt;
1003 * Get a given default option value.
1005 * @param $opt \type{\string} Name of option to retrieve
1006 * @return \type{\string} Default option value
1008 public static function getDefaultOption( $opt ) {
1009 $defOpts = self::getDefaultOptions();
1010 if( isset( $defOpts[$opt] ) ) {
1011 return $defOpts[$opt];
1012 } else {
1013 return '';
1018 * Get a list of user toggle names
1019 * @return \type{\arrayof{\string}} Array of user toggle names
1021 static function getToggles() {
1022 global $wgContLang;
1023 $extraToggles = array();
1024 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
1025 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
1030 * Get blocking information
1031 * @private
1032 * @param $bFromSlave \type{\bool} Whether to check the slave database first. To
1033 * improve performance, non-critical checks are done
1034 * against slaves. Check when actually saving should be
1035 * done against master.
1037 function getBlockedStatus( $bFromSlave = true ) {
1038 global $wgEnableSorbs, $wgProxyWhitelist;
1040 if ( -1 != $this->mBlockedby ) {
1041 wfDebug( "User::getBlockedStatus: already loaded.\n" );
1042 return;
1045 wfProfileIn( __METHOD__ );
1046 wfDebug( __METHOD__.": checking...\n" );
1048 // Initialize data...
1049 // Otherwise something ends up stomping on $this->mBlockedby when
1050 // things get lazy-loaded later, causing false positive block hits
1051 // due to -1 !== 0. Probably session-related... Nothing should be
1052 // overwriting mBlockedby, surely?
1053 $this->load();
1055 $this->mBlockedby = 0;
1056 $this->mHideName = 0;
1057 $this->mAllowUsertalk = 0;
1058 $ip = wfGetIP();
1060 if ($this->isAllowed( 'ipblock-exempt' ) ) {
1061 # Exempt from all types of IP-block
1062 $ip = '';
1065 # User/IP blocking
1066 $this->mBlock = new Block();
1067 $this->mBlock->fromMaster( !$bFromSlave );
1068 if ( $this->mBlock->load( $ip , $this->mId ) ) {
1069 wfDebug( __METHOD__.": Found block.\n" );
1070 $this->mBlockedby = $this->mBlock->mBy;
1071 $this->mBlockreason = $this->mBlock->mReason;
1072 $this->mHideName = $this->mBlock->mHideName;
1073 $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
1074 if ( $this->isLoggedIn() ) {
1075 $this->spreadBlock();
1077 } else {
1078 // Bug 13611: don't remove mBlock here, to allow account creation blocks to
1079 // apply to users. Note that the existence of $this->mBlock is not used to
1080 // check for edit blocks, $this->mBlockedby is instead.
1083 # Proxy blocking
1084 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
1085 # Local list
1086 if ( wfIsLocallyBlockedProxy( $ip ) ) {
1087 $this->mBlockedby = wfMsg( 'proxyblocker' );
1088 $this->mBlockreason = wfMsg( 'proxyblockreason' );
1091 # DNSBL
1092 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
1093 if ( $this->inSorbsBlacklist( $ip ) ) {
1094 $this->mBlockedby = wfMsg( 'sorbs' );
1095 $this->mBlockreason = wfMsg( 'sorbsreason' );
1100 # Extensions
1101 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1103 wfProfileOut( __METHOD__ );
1107 * Whether the given IP is in the SORBS blacklist.
1109 * @param $ip \type{\string} IP to check
1110 * @return \type{\bool} True if blacklisted
1112 function inSorbsBlacklist( $ip ) {
1113 global $wgEnableSorbs, $wgSorbsUrl;
1115 return $wgEnableSorbs &&
1116 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
1120 * Whether the given IP is in a given DNS blacklist.
1122 * @param $ip \type{\string} IP to check
1123 * @param $base \type{\string} URL of the DNS blacklist
1124 * @return \type{\bool} True if blacklisted
1126 function inDnsBlacklist( $ip, $base ) {
1127 wfProfileIn( __METHOD__ );
1129 $found = false;
1130 $host = '';
1131 // FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
1132 if( IP::isIPv4($ip) ) {
1133 # Make hostname
1134 $host = "$ip.$base";
1136 # Send query
1137 $ipList = gethostbynamel( $host );
1139 if( $ipList ) {
1140 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1141 $found = true;
1142 } else {
1143 wfDebug( "Requested $host, not found in $base.\n" );
1147 wfProfileOut( __METHOD__ );
1148 return $found;
1152 * Is this user subject to rate limiting?
1154 * @return \type{\bool} True if rate limited
1156 public function isPingLimitable() {
1157 global $wgRateLimitsExcludedGroups;
1158 if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1159 // Deprecated, but kept for backwards-compatibility config
1160 return false;
1162 return !$this->isAllowed('noratelimit');
1166 * Primitive rate limits: enforce maximum actions per time period
1167 * to put a brake on flooding.
1169 * @note When using a shared cache like memcached, IP-address
1170 * last-hit counters will be shared across wikis.
1172 * @param $action \type{\string} Action to enforce; 'edit' if unspecified
1173 * @return \type{\bool} True if a rate limiter was tripped
1175 function pingLimiter( $action='edit' ) {
1177 # Call the 'PingLimiter' hook
1178 $result = false;
1179 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1180 return $result;
1183 global $wgRateLimits;
1184 if( !isset( $wgRateLimits[$action] ) ) {
1185 return false;
1188 # Some groups shouldn't trigger the ping limiter, ever
1189 if( !$this->isPingLimitable() )
1190 return false;
1192 global $wgMemc, $wgRateLimitLog;
1193 wfProfileIn( __METHOD__ );
1195 $limits = $wgRateLimits[$action];
1196 $keys = array();
1197 $id = $this->getId();
1198 $ip = wfGetIP();
1199 $userLimit = false;
1201 if( isset( $limits['anon'] ) && $id == 0 ) {
1202 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1205 if( isset( $limits['user'] ) && $id != 0 ) {
1206 $userLimit = $limits['user'];
1208 if( $this->isNewbie() ) {
1209 if( isset( $limits['newbie'] ) && $id != 0 ) {
1210 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1212 if( isset( $limits['ip'] ) ) {
1213 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1215 $matches = array();
1216 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1217 $subnet = $matches[1];
1218 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1221 // Check for group-specific permissions
1222 // If more than one group applies, use the group with the highest limit
1223 foreach ( $this->getGroups() as $group ) {
1224 if ( isset( $limits[$group] ) ) {
1225 if ( $userLimit === false || $limits[$group] > $userLimit ) {
1226 $userLimit = $limits[$group];
1230 // Set the user limit key
1231 if ( $userLimit !== false ) {
1232 wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
1233 $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1236 $triggered = false;
1237 foreach( $keys as $key => $limit ) {
1238 list( $max, $period ) = $limit;
1239 $summary = "(limit $max in {$period}s)";
1240 $count = $wgMemc->get( $key );
1241 if( $count ) {
1242 if( $count > $max ) {
1243 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1244 if( $wgRateLimitLog ) {
1245 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1247 $triggered = true;
1248 } else {
1249 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1251 } else {
1252 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1253 $wgMemc->add( $key, 1, intval( $period ) );
1255 $wgMemc->incr( $key );
1258 wfProfileOut( __METHOD__ );
1259 return $triggered;
1263 * Check if user is blocked
1265 * @param $bFromSlave \type{\bool} Whether to check the slave database instead of the master
1266 * @return \type{\bool} True if blocked, false otherwise
1268 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1269 wfDebug( "User::isBlocked: enter\n" );
1270 $this->getBlockedStatus( $bFromSlave );
1271 return $this->mBlockedby !== 0;
1275 * Check if user is blocked from editing a particular article
1277 * @param $title \type{\string} Title to check
1278 * @param $bFromSlave \type{\bool} Whether to check the slave database instead of the master
1279 * @return \type{\bool} True if blocked, false otherwise
1281 function isBlockedFrom( $title, $bFromSlave = false ) {
1282 global $wgBlockAllowsUTEdit;
1283 wfProfileIn( __METHOD__ );
1284 wfDebug( __METHOD__.": enter\n" );
1286 wfDebug( __METHOD__.": asking isBlocked()\n" );
1287 $blocked = $this->isBlocked( $bFromSlave );
1288 $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
1289 # If a user's name is suppressed, they cannot make edits anywhere
1290 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1291 $title->getNamespace() == NS_USER_TALK ) {
1292 $blocked = false;
1293 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1295 wfProfileOut( __METHOD__ );
1296 return $blocked;
1300 * If user is blocked, return the name of the user who placed the block
1301 * @return \type{\string} name of blocker
1303 function blockedBy() {
1304 $this->getBlockedStatus();
1305 return $this->mBlockedby;
1309 * If user is blocked, return the specified reason for the block
1310 * @return \type{\string} Blocking reason
1312 function blockedFor() {
1313 $this->getBlockedStatus();
1314 return $this->mBlockreason;
1318 * Check if user is blocked on all wikis.
1319 * Do not use for actual edit permission checks!
1320 * This is intented for quick UI checks.
1322 * @param $ip \type{\string} IP address, uses current client if none given
1323 * @return \type{\bool} True if blocked, false otherwise
1325 function isBlockedGlobally( $ip = '' ) {
1326 if( $this->mBlockedGlobally !== null ) {
1327 return $this->mBlockedGlobally;
1329 // User is already an IP?
1330 if( IP::isIPAddress( $this->getName() ) ) {
1331 $ip = $this->getName();
1332 } else if( !$ip ) {
1333 $ip = wfGetIP();
1335 $blocked = false;
1336 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1337 $this->mBlockedGlobally = (bool)$blocked;
1338 return $this->mBlockedGlobally;
1342 * Check if user account is locked
1344 * @return \type{\bool} True if locked, false otherwise
1346 function isLocked() {
1347 if( $this->mLocked !== null ) {
1348 return $this->mLocked;
1350 $this->mLocked = (bool)$this->callAuthPlugin( __FUNCTION__ );
1351 return $this->mLocked;
1355 * Check if user account is hidden
1357 * @return \type{\bool} True if hidden, false otherwise
1359 function isHidden() {
1360 if( $this->mHideName !== null ) {
1361 return $this->mHideName;
1363 $this->getBlockedStatus();
1364 if( !$this->mHideName ) {
1365 $this->mHideName = (bool)$this->callAuthPlugin( __FUNCTION__ );
1367 return $this->mHideName;
1371 * Get the user's ID.
1372 * @return \type{\int} The user's ID; 0 if the user is anonymous or nonexistent
1374 function getId() {
1375 if( $this->mId === null and $this->mName !== null
1376 and User::isIP( $this->mName ) ) {
1377 // Special case, we know the user is anonymous
1378 return 0;
1379 } elseif( $this->mId === null ) {
1380 // Don't load if this was initialized from an ID
1381 $this->load();
1383 return $this->mId;
1387 * Set the user and reload all fields according to a given ID
1388 * @param $v \type{\int} %User ID to reload
1390 function setId( $v ) {
1391 $this->mId = $v;
1392 $this->clearInstanceCache( 'id' );
1396 * Get the user name, or the IP of an anonymous user
1397 * @return \type{\string} User's name or IP address
1399 function getName() {
1400 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1401 # Special case optimisation
1402 return $this->mName;
1403 } else {
1404 $this->load();
1405 if ( $this->mName === false ) {
1406 # Clean up IPs
1407 $this->mName = IP::sanitizeIP( wfGetIP() );
1409 return $this->mName;
1414 * Set the user name.
1416 * This does not reload fields from the database according to the given
1417 * name. Rather, it is used to create a temporary "nonexistent user" for
1418 * later addition to the database. It can also be used to set the IP
1419 * address for an anonymous user to something other than the current
1420 * remote IP.
1422 * @note User::newFromName() has rougly the same function, when the named user
1423 * does not exist.
1424 * @param $str \type{\string} New user name to set
1426 function setName( $str ) {
1427 $this->load();
1428 $this->mName = $str;
1432 * Get the user's name escaped by underscores.
1433 * @return \type{\string} Username escaped by underscores
1435 function getTitleKey() {
1436 return str_replace( ' ', '_', $this->getName() );
1440 * Check if the user has new messages.
1441 * @return \type{\bool} True if the user has new messages
1443 function getNewtalk() {
1444 $this->load();
1446 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1447 if( $this->mNewtalk === -1 ) {
1448 $this->mNewtalk = false; # reset talk page status
1450 # Check memcached separately for anons, who have no
1451 # entire User object stored in there.
1452 if( !$this->mId ) {
1453 global $wgMemc;
1454 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1455 $newtalk = $wgMemc->get( $key );
1456 if( strval( $newtalk ) !== '' ) {
1457 $this->mNewtalk = (bool)$newtalk;
1458 } else {
1459 // Since we are caching this, make sure it is up to date by getting it
1460 // from the master
1461 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1462 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1464 } else {
1465 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1469 return (bool)$this->mNewtalk;
1473 * Return the talk page(s) this user has new messages on.
1474 * @return \type{\arrayof{\string}} Array of page URLs
1476 function getNewMessageLinks() {
1477 $talks = array();
1478 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1479 return $talks;
1481 if (!$this->getNewtalk())
1482 return array();
1483 $up = $this->getUserPage();
1484 $utp = $up->getTalkPage();
1485 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1490 * Internal uncached check for new messages
1492 * @see getNewtalk()
1493 * @param $field \type{\string} 'user_ip' for anonymous users, 'user_id' otherwise
1494 * @param $id \twotypes{\string,\int} User's IP address for anonymous users, %User ID otherwise
1495 * @param $fromMaster \type{\bool} true to fetch from the master, false for a slave
1496 * @return \type{\bool} True if the user has new messages
1497 * @private
1499 function checkNewtalk( $field, $id, $fromMaster = false ) {
1500 if ( $fromMaster ) {
1501 $db = wfGetDB( DB_MASTER );
1502 } else {
1503 $db = wfGetDB( DB_SLAVE );
1505 $ok = $db->selectField( 'user_newtalk', $field,
1506 array( $field => $id ), __METHOD__ );
1507 return $ok !== false;
1511 * Add or update the new messages flag
1512 * @param $field \type{\string} 'user_ip' for anonymous users, 'user_id' otherwise
1513 * @param $id \twotypes{string,\int} User's IP address for anonymous users, %User ID otherwise
1514 * @return \type{\bool} True if successful, false otherwise
1515 * @private
1517 function updateNewtalk( $field, $id ) {
1518 $dbw = wfGetDB( DB_MASTER );
1519 $dbw->insert( 'user_newtalk',
1520 array( $field => $id ),
1521 __METHOD__,
1522 'IGNORE' );
1523 if ( $dbw->affectedRows() ) {
1524 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1525 return true;
1526 } else {
1527 wfDebug( __METHOD__." already set ($field, $id)\n" );
1528 return false;
1533 * Clear the new messages flag for the given user
1534 * @param $field \type{\string} 'user_ip' for anonymous users, 'user_id' otherwise
1535 * @param $id \twotypes{\string,\int} User's IP address for anonymous users, %User ID otherwise
1536 * @return \type{\bool} True if successful, false otherwise
1537 * @private
1539 function deleteNewtalk( $field, $id ) {
1540 $dbw = wfGetDB( DB_MASTER );
1541 $dbw->delete( 'user_newtalk',
1542 array( $field => $id ),
1543 __METHOD__ );
1544 if ( $dbw->affectedRows() ) {
1545 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1546 return true;
1547 } else {
1548 wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1549 return false;
1554 * Update the 'You have new messages!' status.
1555 * @param $val \type{\bool} Whether the user has new messages
1557 function setNewtalk( $val ) {
1558 if( wfReadOnly() ) {
1559 return;
1562 $this->load();
1563 $this->mNewtalk = $val;
1565 if( $this->isAnon() ) {
1566 $field = 'user_ip';
1567 $id = $this->getName();
1568 } else {
1569 $field = 'user_id';
1570 $id = $this->getId();
1572 global $wgMemc;
1574 if( $val ) {
1575 $changed = $this->updateNewtalk( $field, $id );
1576 } else {
1577 $changed = $this->deleteNewtalk( $field, $id );
1580 if( $this->isAnon() ) {
1581 // Anons have a separate memcached space, since
1582 // user records aren't kept for them.
1583 $key = wfMemcKey( 'newtalk', 'ip', $id );
1584 $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1586 if ( $changed ) {
1587 $this->invalidateCache();
1592 * Generate a current or new-future timestamp to be stored in the
1593 * user_touched field when we update things.
1594 * @return \type{\string} Timestamp in TS_MW format
1596 private static function newTouchedTimestamp() {
1597 global $wgClockSkewFudge;
1598 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1602 * Clear user data from memcached.
1603 * Use after applying fun updates to the database; caller's
1604 * responsibility to update user_touched if appropriate.
1606 * Called implicitly from invalidateCache() and saveSettings().
1608 private function clearSharedCache() {
1609 if( $this->mId ) {
1610 global $wgMemc;
1611 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1616 * Immediately touch the user data cache for this account.
1617 * Updates user_touched field, and removes account data from memcached
1618 * for reload on the next hit.
1620 function invalidateCache() {
1621 $this->load();
1622 if( $this->mId ) {
1623 $this->mTouched = self::newTouchedTimestamp();
1625 $dbw = wfGetDB( DB_MASTER );
1626 $dbw->update( 'user',
1627 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1628 array( 'user_id' => $this->mId ),
1629 __METHOD__ );
1631 $this->clearSharedCache();
1636 * Validate the cache for this account.
1637 * @param $timestamp \type{\string} A timestamp in TS_MW format
1639 function validateCache( $timestamp ) {
1640 $this->load();
1641 return ($timestamp >= $this->mTouched);
1645 * Get the user touched timestamp
1647 function getTouched() {
1648 $this->load();
1649 return $this->mTouched;
1653 * Set the password and reset the random token.
1654 * Calls through to authentication plugin if necessary;
1655 * will have no effect if the auth plugin refuses to
1656 * pass the change through or if the legal password
1657 * checks fail.
1659 * As a special case, setting the password to null
1660 * wipes it, so the account cannot be logged in until
1661 * a new password is set, for instance via e-mail.
1663 * @param $str \type{\string} New password to set
1664 * @throws PasswordError on failure
1666 function setPassword( $str ) {
1667 global $wgAuth;
1669 if( $str !== null ) {
1670 if( !$wgAuth->allowPasswordChange() ) {
1671 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1674 if( !$this->isValidPassword( $str ) ) {
1675 global $wgMinimalPasswordLength;
1676 throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
1677 $wgMinimalPasswordLength ) );
1681 if( !$wgAuth->setPassword( $this, $str ) ) {
1682 throw new PasswordError( wfMsg( 'externaldberror' ) );
1685 $this->setInternalPassword( $str );
1687 return true;
1691 * Set the password and reset the random token unconditionally.
1693 * @param $str \type{\string} New password to set
1695 function setInternalPassword( $str ) {
1696 $this->load();
1697 $this->setToken();
1699 if( $str === null ) {
1700 // Save an invalid hash...
1701 $this->mPassword = '';
1702 } else {
1703 $this->mPassword = self::crypt( $str );
1705 $this->mNewpassword = '';
1706 $this->mNewpassTime = null;
1710 * Get the user's current token.
1711 * @return \type{\string} Token
1713 function getToken() {
1714 $this->load();
1715 return $this->mToken;
1719 * Set the random token (used for persistent authentication)
1720 * Called from loadDefaults() among other places.
1722 * @param $token \type{\string} If specified, set the token to this value
1723 * @private
1725 function setToken( $token = false ) {
1726 global $wgSecretKey, $wgProxyKey;
1727 $this->load();
1728 if ( !$token ) {
1729 if ( $wgSecretKey ) {
1730 $key = $wgSecretKey;
1731 } elseif ( $wgProxyKey ) {
1732 $key = $wgProxyKey;
1733 } else {
1734 $key = microtime();
1736 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1737 } else {
1738 $this->mToken = $token;
1743 * Set the cookie password
1745 * @param $str \type{\string} New cookie password
1746 * @private
1748 function setCookiePassword( $str ) {
1749 $this->load();
1750 $this->mCookiePassword = md5( $str );
1754 * Set the password for a password reminder or new account email
1756 * @param $str \type{\string} New password to set
1757 * @param $throttle \type{\bool} If true, reset the throttle timestamp to the present
1759 function setNewpassword( $str, $throttle = true ) {
1760 $this->load();
1761 $this->mNewpassword = self::crypt( $str );
1762 if ( $throttle ) {
1763 $this->mNewpassTime = wfTimestampNow();
1768 * Has password reminder email been sent within the last
1769 * $wgPasswordReminderResendTime hours?
1770 * @return \type{\bool} True or false
1772 function isPasswordReminderThrottled() {
1773 global $wgPasswordReminderResendTime;
1774 $this->load();
1775 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1776 return false;
1778 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1779 return time() < $expiry;
1783 * Get the user's e-mail address
1784 * @return \type{\string} User's e-mail address
1786 function getEmail() {
1787 $this->load();
1788 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1789 return $this->mEmail;
1793 * Get the timestamp of the user's e-mail authentication
1794 * @return \type{\string} TS_MW timestamp
1796 function getEmailAuthenticationTimestamp() {
1797 $this->load();
1798 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1799 return $this->mEmailAuthenticated;
1803 * Set the user's e-mail address
1804 * @param $str \type{\string} New e-mail address
1806 function setEmail( $str ) {
1807 $this->load();
1808 $this->mEmail = $str;
1809 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1813 * Get the user's real name
1814 * @return \type{\string} User's real name
1816 function getRealName() {
1817 $this->load();
1818 return $this->mRealName;
1822 * Set the user's real name
1823 * @param $str \type{\string} New real name
1825 function setRealName( $str ) {
1826 $this->load();
1827 $this->mRealName = $str;
1831 * Get the user's current setting for a given option.
1833 * @param $oname \type{\string} The option to check
1834 * @param $defaultOverride \type{\string} A default value returned if the option does not exist
1835 * @return \type{\string} User's current value for the option
1836 * @see getBoolOption()
1837 * @see getIntOption()
1839 function getOption( $oname, $defaultOverride = '' ) {
1840 $this->load();
1842 if ( is_null( $this->mOptions ) ) {
1843 if($defaultOverride != '') {
1844 return $defaultOverride;
1846 $this->mOptions = User::getDefaultOptions();
1849 if ( array_key_exists( $oname, $this->mOptions ) ) {
1850 return trim( $this->mOptions[$oname] );
1851 } else {
1852 return $defaultOverride;
1857 * Get the user's current setting for a given option, as a boolean value.
1859 * @param $oname \type{\string} The option to check
1860 * @return \type{\bool} User's current value for the option
1861 * @see getOption()
1863 function getBoolOption( $oname ) {
1864 return (bool)$this->getOption( $oname );
1869 * Get the user's current setting for a given option, as a boolean value.
1871 * @param $oname \type{\string} The option to check
1872 * @param $defaultOverride \type{\int} A default value returned if the option does not exist
1873 * @return \type{\int} User's current value for the option
1874 * @see getOption()
1876 function getIntOption( $oname, $defaultOverride=0 ) {
1877 $val = $this->getOption( $oname );
1878 if( $val == '' ) {
1879 $val = $defaultOverride;
1881 return intval( $val );
1885 * Set the given option for a user.
1887 * @param $oname \type{\string} The option to set
1888 * @param $val \type{\mixed} New value to set
1890 function setOption( $oname, $val ) {
1891 $this->load();
1892 if ( is_null( $this->mOptions ) ) {
1893 $this->mOptions = User::getDefaultOptions();
1895 if ( $oname == 'skin' ) {
1896 # Clear cached skin, so the new one displays immediately in Special:Preferences
1897 unset( $this->mSkin );
1899 // Filter out any newlines that may have passed through input validation.
1900 // Newlines are used to separate items in the options blob.
1901 if( $val ) {
1902 $val = str_replace( "\r\n", "\n", $val );
1903 $val = str_replace( "\r", "\n", $val );
1904 $val = str_replace( "\n", " ", $val );
1906 // Explicitly NULL values should refer to defaults
1907 global $wgDefaultUserOptions;
1908 if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
1909 $val = $wgDefaultUserOptions[$oname];
1911 $this->mOptions[$oname] = $val;
1915 * Get the user's preferred date format.
1916 * @return \type{\string} User's preferred date format
1918 function getDatePreference() {
1919 // Important migration for old data rows
1920 if ( is_null( $this->mDatePreference ) ) {
1921 global $wgLang;
1922 $value = $this->getOption( 'date' );
1923 $map = $wgLang->getDatePreferenceMigrationMap();
1924 if ( isset( $map[$value] ) ) {
1925 $value = $map[$value];
1927 $this->mDatePreference = $value;
1929 return $this->mDatePreference;
1933 * Get the permissions this user has.
1934 * @return \type{\arrayof{\string}} Array of permission names
1936 function getRights() {
1937 if ( is_null( $this->mRights ) ) {
1938 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1939 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
1940 // Force reindexation of rights when a hook has unset one of them
1941 $this->mRights = array_values( $this->mRights );
1943 return $this->mRights;
1947 * Get the list of explicit group memberships this user has.
1948 * The implicit * and user groups are not included.
1949 * @return \type{\arrayof{\string}} Array of internal group names
1951 function getGroups() {
1952 $this->load();
1953 return $this->mGroups;
1957 * Get the list of implicit group memberships this user has.
1958 * This includes all explicit groups, plus 'user' if logged in,
1959 * '*' for all accounts and autopromoted groups
1961 * @param $recache \type{\bool} Whether to avoid the cache
1962 * @return \type{\arrayof{\string}} Array of internal group names
1964 function getEffectiveGroups( $recache = false ) {
1965 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
1966 $this->mEffectiveGroups = $this->getGroups();
1967 $this->mEffectiveGroups[] = '*';
1968 if( $this->getId() ) {
1969 $this->mEffectiveGroups[] = 'user';
1971 $this->mEffectiveGroups = array_unique( array_merge(
1972 $this->mEffectiveGroups,
1973 Autopromote::getAutopromoteGroups( $this )
1974 ) );
1976 # Hook for additional groups
1977 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
1980 return $this->mEffectiveGroups;
1984 * Get the user's edit count.
1985 * @return \type{\int} User's edit count
1987 function getEditCount() {
1988 if ($this->mId) {
1989 if ( !isset( $this->mEditCount ) ) {
1990 /* Populate the count, if it has not been populated yet */
1991 $this->mEditCount = User::edits($this->mId);
1993 return $this->mEditCount;
1994 } else {
1995 /* nil */
1996 return null;
2001 * Add the user to the given group.
2002 * This takes immediate effect.
2003 * @param $group \type{\string} Name of the group to add
2005 function addGroup( $group ) {
2006 $dbw = wfGetDB( DB_MASTER );
2007 if( $this->getId() ) {
2008 $dbw->insert( 'user_groups',
2009 array(
2010 'ug_user' => $this->getID(),
2011 'ug_group' => $group,
2013 'User::addGroup',
2014 array( 'IGNORE' ) );
2017 $this->loadGroups();
2018 $this->mGroups[] = $group;
2019 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2021 $this->invalidateCache();
2025 * Remove the user from the given group.
2026 * This takes immediate effect.
2027 * @param $group \type{\string} Name of the group to remove
2029 function removeGroup( $group ) {
2030 $this->load();
2031 $dbw = wfGetDB( DB_MASTER );
2032 $dbw->delete( 'user_groups',
2033 array(
2034 'ug_user' => $this->getID(),
2035 'ug_group' => $group,
2037 'User::removeGroup' );
2039 $this->loadGroups();
2040 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2041 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2043 $this->invalidateCache();
2048 * Get whether the user is logged in
2049 * @return \type{\bool} True or false
2051 function isLoggedIn() {
2052 return $this->getID() != 0;
2056 * Get whether the user is anonymous
2057 * @return \type{\bool} True or false
2059 function isAnon() {
2060 return !$this->isLoggedIn();
2064 * Get whether the user is a bot
2065 * @return \type{\bool} True or false
2066 * @deprecated
2068 function isBot() {
2069 wfDeprecated( __METHOD__ );
2070 return $this->isAllowed( 'bot' );
2074 * Check if user is allowed to access a feature / make an action
2075 * @param $action \type{\string} action to be checked
2076 * @return \type{\bool} True if action is allowed, else false
2078 function isAllowed($action='') {
2079 if ( $action === '' )
2080 // In the spirit of DWIM
2081 return true;
2083 # Use strict parameter to avoid matching numeric 0 accidentally inserted
2084 # by misconfiguration: 0 == 'foo'
2085 return in_array( $action, $this->getRights(), true );
2089 * Check whether to enable recent changes patrol features for this user
2090 * @return \type{\bool} True or false
2092 public function useRCPatrol() {
2093 global $wgUseRCPatrol;
2094 return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2098 * Check whether to enable new pages patrol features for this user
2099 * @return \type{\bool} True or false
2101 public function useNPPatrol() {
2102 global $wgUseRCPatrol, $wgUseNPPatrol;
2103 return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2107 * Get the current skin, loading it if required
2108 * @return \type{Skin} Current skin
2109 * @todo FIXME : need to check the old failback system [AV]
2111 function &getSkin() {
2112 global $wgRequest;
2113 if ( ! isset( $this->mSkin ) ) {
2114 wfProfileIn( __METHOD__ );
2116 # get the user skin
2117 $userSkin = $this->getOption( 'skin' );
2118 $userSkin = $wgRequest->getVal('useskin', $userSkin);
2120 $this->mSkin =& Skin::newFromKey( $userSkin );
2121 wfProfileOut( __METHOD__ );
2123 return $this->mSkin;
2127 * Check the watched status of an article.
2128 * @param $title \type{Title} Title of the article to look at
2129 * @return \type{\bool} True if article is watched
2131 function isWatched( $title ) {
2132 $wl = WatchedItem::fromUserTitle( $this, $title );
2133 return $wl->isWatched();
2137 * Watch an article.
2138 * @param $title \type{Title} Title of the article to look at
2140 function addWatch( $title ) {
2141 $wl = WatchedItem::fromUserTitle( $this, $title );
2142 $wl->addWatch();
2143 $this->invalidateCache();
2147 * Stop watching an article.
2148 * @param $title \type{Title} Title of the article to look at
2150 function removeWatch( $title ) {
2151 $wl = WatchedItem::fromUserTitle( $this, $title );
2152 $wl->removeWatch();
2153 $this->invalidateCache();
2157 * Clear the user's notification timestamp for the given title.
2158 * If e-notif e-mails are on, they will receive notification mails on
2159 * the next change of the page if it's watched etc.
2160 * @param $title \type{Title} Title of the article to look at
2162 function clearNotification( &$title ) {
2163 global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2165 # Do nothing if the database is locked to writes
2166 if( wfReadOnly() ) {
2167 return;
2170 if ($title->getNamespace() == NS_USER_TALK &&
2171 $title->getText() == $this->getName() ) {
2172 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
2173 return;
2174 $this->setNewtalk( false );
2177 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2178 return;
2181 if( $this->isAnon() ) {
2182 // Nothing else to do...
2183 return;
2186 // Only update the timestamp if the page is being watched.
2187 // The query to find out if it is watched is cached both in memcached and per-invocation,
2188 // and when it does have to be executed, it can be on a slave
2189 // If this is the user's newtalk page, we always update the timestamp
2190 if ($title->getNamespace() == NS_USER_TALK &&
2191 $title->getText() == $wgUser->getName())
2193 $watched = true;
2194 } elseif ( $this->getId() == $wgUser->getId() ) {
2195 $watched = $title->userIsWatching();
2196 } else {
2197 $watched = true;
2200 // If the page is watched by the user (or may be watched), update the timestamp on any
2201 // any matching rows
2202 if ( $watched ) {
2203 $dbw = wfGetDB( DB_MASTER );
2204 $dbw->update( 'watchlist',
2205 array( /* SET */
2206 'wl_notificationtimestamp' => NULL
2207 ), array( /* WHERE */
2208 'wl_title' => $title->getDBkey(),
2209 'wl_namespace' => $title->getNamespace(),
2210 'wl_user' => $this->getID()
2211 ), __METHOD__
2217 * Resets all of the given user's page-change notification timestamps.
2218 * If e-notif e-mails are on, they will receive notification mails on
2219 * the next change of any watched page.
2221 * @param $currentUser \type{\int} %User ID
2223 function clearAllNotifications( $currentUser ) {
2224 global $wgUseEnotif, $wgShowUpdatedMarker;
2225 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2226 $this->setNewtalk( false );
2227 return;
2229 if( $currentUser != 0 ) {
2230 $dbw = wfGetDB( DB_MASTER );
2231 $dbw->update( 'watchlist',
2232 array( /* SET */
2233 'wl_notificationtimestamp' => NULL
2234 ), array( /* WHERE */
2235 'wl_user' => $currentUser
2236 ), __METHOD__
2238 # We also need to clear here the "you have new message" notification for the own user_talk page
2239 # This is cleared one page view later in Article::viewUpdates();
2244 * Encode this user's options as a string
2245 * @return \type{\string} Encoded options
2246 * @private
2248 function encodeOptions() {
2249 $this->load();
2250 if ( is_null( $this->mOptions ) ) {
2251 $this->mOptions = User::getDefaultOptions();
2253 $a = array();
2254 foreach ( $this->mOptions as $oname => $oval ) {
2255 array_push( $a, $oname.'='.$oval );
2257 $s = implode( "\n", $a );
2258 return $s;
2262 * Set this user's options from an encoded string
2263 * @param $str \type{\string} Encoded options to import
2264 * @private
2266 function decodeOptions( $str ) {
2267 $this->mOptions = array();
2268 $a = explode( "\n", $str );
2269 foreach ( $a as $s ) {
2270 $m = array();
2271 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2272 $this->mOptions[$m[1]] = $m[2];
2278 * Set a cookie on the user's client. Wrapper for
2279 * WebResponse::setCookie
2281 protected function setCookie( $name, $value, $exp=0 ) {
2282 global $wgRequest;
2283 $wgRequest->response()->setcookie( $name, $value, $exp );
2287 * Clear a cookie on the user's client
2288 * @param $name \type{\string} Name of the cookie to clear
2290 protected function clearCookie( $name ) {
2291 $this->setCookie( $name, '', time() - 86400 );
2295 * Set the default cookies for this session on the user's client.
2297 function setCookies() {
2298 $this->load();
2299 if ( 0 == $this->mId ) return;
2300 $session = array(
2301 'wsUserID' => $this->mId,
2302 'wsToken' => $this->mToken,
2303 'wsUserName' => $this->getName()
2305 $cookies = array(
2306 'UserID' => $this->mId,
2307 'UserName' => $this->getName(),
2309 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2310 $cookies['Token'] = $this->mToken;
2311 } else {
2312 $cookies['Token'] = false;
2315 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2316 $_SESSION = $session + $_SESSION;
2317 foreach ( $cookies as $name => $value ) {
2318 if ( $value === false ) {
2319 $this->clearCookie( $name );
2320 } else {
2321 $this->setCookie( $name, $value );
2327 * Log this user out.
2329 function logout() {
2330 global $wgUser;
2331 if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
2332 $this->doLogout();
2337 * Clear the user's cookies and session, and reset the instance cache.
2338 * @private
2339 * @see logout()
2341 function doLogout() {
2342 $this->clearInstanceCache( 'defaults' );
2344 $_SESSION['wsUserID'] = 0;
2346 $this->clearCookie( 'UserID' );
2347 $this->clearCookie( 'Token' );
2349 # Remember when user logged out, to prevent seeing cached pages
2350 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2354 * Save this user's settings into the database.
2355 * @todo Only rarely do all these fields need to be set!
2357 function saveSettings() {
2358 $this->load();
2359 if ( wfReadOnly() ) { return; }
2360 if ( 0 == $this->mId ) { return; }
2362 $this->mTouched = self::newTouchedTimestamp();
2364 $dbw = wfGetDB( DB_MASTER );
2365 $dbw->update( 'user',
2366 array( /* SET */
2367 'user_name' => $this->mName,
2368 'user_password' => $this->mPassword,
2369 'user_newpassword' => $this->mNewpassword,
2370 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2371 'user_real_name' => $this->mRealName,
2372 'user_email' => $this->mEmail,
2373 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2374 'user_options' => $this->encodeOptions(),
2375 'user_touched' => $dbw->timestamp($this->mTouched),
2376 'user_token' => $this->mToken,
2377 'user_email_token' => $this->mEmailToken,
2378 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2379 ), array( /* WHERE */
2380 'user_id' => $this->mId
2381 ), __METHOD__
2383 wfRunHooks( 'UserSaveSettings', array( $this ) );
2384 $this->clearSharedCache();
2388 * If only this user's username is known, and it exists, return the user ID.
2390 function idForName() {
2391 $s = trim( $this->getName() );
2392 if ( $s === '' ) return 0;
2394 $dbr = wfGetDB( DB_SLAVE );
2395 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2396 if ( $id === false ) {
2397 $id = 0;
2399 return $id;
2403 * Add a user to the database, return the user object
2405 * @param $name \type{\string} Username to add
2406 * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
2407 * - password The user's password. Password logins will be disabled if this is omitted.
2408 * - newpassword A temporary password mailed to the user
2409 * - email The user's email address
2410 * - email_authenticated The email authentication timestamp
2411 * - real_name The user's real name
2412 * - options An associative array of non-default options
2413 * - token Random authentication token. Do not set.
2414 * - registration Registration timestamp. Do not set.
2416 * @return \type{User} A new User object, or null if the username already exists
2418 static function createNew( $name, $params = array() ) {
2419 $user = new User;
2420 $user->load();
2421 if ( isset( $params['options'] ) ) {
2422 $user->mOptions = $params['options'] + $user->mOptions;
2423 unset( $params['options'] );
2425 $dbw = wfGetDB( DB_MASTER );
2426 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2427 $fields = array(
2428 'user_id' => $seqVal,
2429 'user_name' => $name,
2430 'user_password' => $user->mPassword,
2431 'user_newpassword' => $user->mNewpassword,
2432 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2433 'user_email' => $user->mEmail,
2434 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2435 'user_real_name' => $user->mRealName,
2436 'user_options' => $user->encodeOptions(),
2437 'user_token' => $user->mToken,
2438 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2439 'user_editcount' => 0,
2441 foreach ( $params as $name => $value ) {
2442 $fields["user_$name"] = $value;
2444 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2445 if ( $dbw->affectedRows() ) {
2446 $newUser = User::newFromId( $dbw->insertId() );
2447 } else {
2448 $newUser = null;
2450 return $newUser;
2454 * Add this existing user object to the database
2456 function addToDatabase() {
2457 $this->load();
2458 $dbw = wfGetDB( DB_MASTER );
2459 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2460 $dbw->insert( 'user',
2461 array(
2462 'user_id' => $seqVal,
2463 'user_name' => $this->mName,
2464 'user_password' => $this->mPassword,
2465 'user_newpassword' => $this->mNewpassword,
2466 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2467 'user_email' => $this->mEmail,
2468 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2469 'user_real_name' => $this->mRealName,
2470 'user_options' => $this->encodeOptions(),
2471 'user_token' => $this->mToken,
2472 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2473 'user_editcount' => 0,
2474 ), __METHOD__
2476 $this->mId = $dbw->insertId();
2478 // Clear instance cache other than user table data, which is already accurate
2479 $this->clearInstanceCache();
2483 * If this (non-anonymous) user is blocked, block any IP address
2484 * they've successfully logged in from.
2486 function spreadBlock() {
2487 wfDebug( __METHOD__."()\n" );
2488 $this->load();
2489 if ( $this->mId == 0 ) {
2490 return;
2493 $userblock = Block::newFromDB( '', $this->mId );
2494 if ( !$userblock ) {
2495 return;
2498 $userblock->doAutoblock( wfGetIp() );
2503 * Generate a string which will be different for any combination of
2504 * user options which would produce different parser output.
2505 * This will be used as part of the hash key for the parser cache,
2506 * so users will the same options can share the same cached data
2507 * safely.
2509 * Extensions which require it should install 'PageRenderingHash' hook,
2510 * which will give them a chance to modify this key based on their own
2511 * settings.
2513 * @return \type{\string} Page rendering hash
2515 function getPageRenderingHash() {
2516 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2517 if( $this->mHash ){
2518 return $this->mHash;
2521 // stubthreshold is only included below for completeness,
2522 // it will always be 0 when this function is called by parsercache.
2524 $confstr = $this->getOption( 'math' );
2525 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2526 if ( $wgUseDynamicDates ) {
2527 $confstr .= '!' . $this->getDatePreference();
2529 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2530 $confstr .= '!' . $wgLang->getCode();
2531 $confstr .= '!' . $this->getOption( 'thumbsize' );
2532 // add in language specific options, if any
2533 $extra = $wgContLang->getExtraHashOptions();
2534 $confstr .= $extra;
2536 $confstr .= $wgRenderHashAppend;
2538 // Give a chance for extensions to modify the hash, if they have
2539 // extra options or other effects on the parser cache.
2540 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2542 // Make it a valid memcached key fragment
2543 $confstr = str_replace( ' ', '_', $confstr );
2544 $this->mHash = $confstr;
2545 return $confstr;
2549 * Get whether the user is explicitly blocked from account creation.
2550 * @return \type{\bool} True if blocked
2552 function isBlockedFromCreateAccount() {
2553 $this->getBlockedStatus();
2554 return $this->mBlock && $this->mBlock->mCreateAccount;
2558 * Get whether the user is blocked from using Special:Emailuser.
2559 * @return \type{\bool} True if blocked
2561 function isBlockedFromEmailuser() {
2562 $this->getBlockedStatus();
2563 return $this->mBlock && $this->mBlock->mBlockEmail;
2567 * Get whether the user is allowed to create an account.
2568 * @return \type{\bool} True if allowed
2570 function isAllowedToCreateAccount() {
2571 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2575 * @deprecated
2577 function setLoaded( $loaded ) {
2578 wfDeprecated( __METHOD__ );
2582 * Get this user's personal page title.
2584 * @return \type{Title} User's personal page title
2586 function getUserPage() {
2587 return Title::makeTitle( NS_USER, $this->getName() );
2591 * Get this user's talk page title.
2593 * @return \type{Title} User's talk page title
2595 function getTalkPage() {
2596 $title = $this->getUserPage();
2597 return $title->getTalkPage();
2601 * Get the maximum valid user ID.
2602 * @return \type{\int} %User ID
2603 * @static
2605 function getMaxID() {
2606 static $res; // cache
2608 if ( isset( $res ) )
2609 return $res;
2610 else {
2611 $dbr = wfGetDB( DB_SLAVE );
2612 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2617 * Determine whether the user is a newbie. Newbies are either
2618 * anonymous IPs, or the most recently created accounts.
2619 * @return \type{\bool} True if the user is a newbie
2621 function isNewbie() {
2622 return !$this->isAllowed( 'autoconfirmed' );
2626 * Is the user active? We check to see if they've made at least
2627 * X number of edits in the last Y days.
2629 * @return \type{\bool} True if the user is active, false if not.
2631 public function isActiveEditor() {
2632 global $wgActiveUserEditCount, $wgActiveUserDays;
2633 $dbr = wfGetDB( DB_SLAVE );
2635 // Stolen without shame from RC
2636 $cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
2637 $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
2638 $oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
2640 $res = $dbr->select( 'revision', '1',
2641 array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
2642 __METHOD__,
2643 array('LIMIT' => $wgActiveUserEditCount ) );
2645 $count = $dbr->numRows($res);
2646 $dbr->freeResult($res);
2648 return $count == $wgActiveUserEditCount;
2652 * Check to see if the given clear-text password is one of the accepted passwords
2653 * @param $password \type{\string} user password.
2654 * @return \type{\bool} True if the given password is correct, otherwise False.
2656 function checkPassword( $password ) {
2657 global $wgAuth;
2658 $this->load();
2660 // Even though we stop people from creating passwords that
2661 // are shorter than this, doesn't mean people wont be able
2662 // to. Certain authentication plugins do NOT want to save
2663 // domain passwords in a mysql database, so we should
2664 // check this (incase $wgAuth->strict() is false).
2665 if( !$this->isValidPassword( $password ) ) {
2666 return false;
2669 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2670 return true;
2671 } elseif( $wgAuth->strict() ) {
2672 /* Auth plugin doesn't allow local authentication */
2673 return false;
2674 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2675 /* Auth plugin doesn't allow local authentication for this user name */
2676 return false;
2678 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2679 return true;
2680 } elseif ( function_exists( 'iconv' ) ) {
2681 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2682 # Check for this with iconv
2683 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2684 if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2685 return true;
2688 return false;
2692 * Check if the given clear-text password matches the temporary password
2693 * sent by e-mail for password reset operations.
2694 * @return \type{\bool} True if matches, false otherwise
2696 function checkTemporaryPassword( $plaintext ) {
2697 return self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() );
2701 * Initialize (if necessary) and return a session token value
2702 * which can be used in edit forms to show that the user's
2703 * login credentials aren't being hijacked with a foreign form
2704 * submission.
2706 * @param $salt \twotypes{\string,\arrayof{\string}} Optional function-specific data for hashing
2707 * @return \type{\string} The new edit token
2709 function editToken( $salt = '' ) {
2710 if ( $this->isAnon() ) {
2711 return EDIT_TOKEN_SUFFIX;
2712 } else {
2713 if( !isset( $_SESSION['wsEditToken'] ) ) {
2714 $token = $this->generateToken();
2715 $_SESSION['wsEditToken'] = $token;
2716 } else {
2717 $token = $_SESSION['wsEditToken'];
2719 if( is_array( $salt ) ) {
2720 $salt = implode( '|', $salt );
2722 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2727 * Generate a looking random token for various uses.
2729 * @param $salt \type{\string} Optional salt value
2730 * @return \type{\string} The new random token
2732 function generateToken( $salt = '' ) {
2733 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2734 return md5( $token . $salt );
2738 * Check given value against the token value stored in the session.
2739 * A match should confirm that the form was submitted from the
2740 * user's own login session, not a form submission from a third-party
2741 * site.
2743 * @param $val \type{\string} Input value to compare
2744 * @param $salt \type{\string} Optional function-specific data for hashing
2745 * @return \type{\bool} Whether the token matches
2747 function matchEditToken( $val, $salt = '' ) {
2748 $sessionToken = $this->editToken( $salt );
2749 if ( $val != $sessionToken ) {
2750 wfDebug( "User::matchEditToken: broken session data\n" );
2752 return $val == $sessionToken;
2756 * Check given value against the token value stored in the session,
2757 * ignoring the suffix.
2759 * @param $val \type{\string} Input value to compare
2760 * @param $salt \type{\string} Optional function-specific data for hashing
2761 * @return \type{\bool} Whether the token matches
2763 function matchEditTokenNoSuffix( $val, $salt = '' ) {
2764 $sessionToken = $this->editToken( $salt );
2765 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2769 * Generate a new e-mail confirmation token and send a confirmation/invalidation
2770 * mail to the user's given address.
2772 * @return \twotypes{\bool,WikiError} True on success, a WikiError object on failure.
2774 function sendConfirmationMail() {
2775 global $wgLang;
2776 $expiration = null; // gets passed-by-ref and defined in next line.
2777 $token = $this->confirmationToken( $expiration );
2778 $url = $this->confirmationTokenUrl( $token );
2779 $invalidateURL = $this->invalidationTokenUrl( $token );
2780 $this->saveSettings();
2782 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2783 wfMsg( 'confirmemail_body',
2784 wfGetIP(),
2785 $this->getName(),
2786 $url,
2787 $wgLang->timeanddate( $expiration, false ),
2788 $invalidateURL ) );
2792 * Send an e-mail to this user's account. Does not check for
2793 * confirmed status or validity.
2795 * @param $subject \type{\string} Message subject
2796 * @param $body \type{\string} Message body
2797 * @param $from \type{\string} Optional From address; if unspecified, default $wgPasswordSender will be used
2798 * @param $replyto \type{\string} Reply-to address
2799 * @return \twotypes{\bool,WikiError} True on success, a WikiError object on failure
2801 function sendMail( $subject, $body, $from = null, $replyto = null ) {
2802 if( is_null( $from ) ) {
2803 global $wgPasswordSender;
2804 $from = $wgPasswordSender;
2807 $to = new MailAddress( $this );
2808 $sender = new MailAddress( $from );
2809 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2813 * Generate, store, and return a new e-mail confirmation code.
2814 * A hash (unsalted, since it's used as a key) is stored.
2816 * @note Call saveSettings() after calling this function to commit
2817 * this change to the database.
2819 * @param[out] &$expiration \type{\mixed} Accepts the expiration time
2820 * @return \type{\string} New token
2821 * @private
2823 function confirmationToken( &$expiration ) {
2824 $now = time();
2825 $expires = $now + 7 * 24 * 60 * 60;
2826 $expiration = wfTimestamp( TS_MW, $expires );
2827 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2828 $hash = md5( $token );
2829 $this->load();
2830 $this->mEmailToken = $hash;
2831 $this->mEmailTokenExpires = $expiration;
2832 return $token;
2836 * Return a URL the user can use to confirm their email address.
2837 * @param $token \type{\string} Accepts the email confirmation token
2838 * @return \type{\string} New token URL
2839 * @private
2841 function confirmationTokenUrl( $token ) {
2842 return $this->getTokenUrl( 'ConfirmEmail', $token );
2845 * Return a URL the user can use to invalidate their email address.
2847 * @param $token \type{\string} Accepts the email confirmation token
2848 * @return \type{\string} New token URL
2849 * @private
2851 function invalidationTokenUrl( $token ) {
2852 return $this->getTokenUrl( 'Invalidateemail', $token );
2856 * Internal function to format the e-mail validation/invalidation URLs.
2857 * This uses $wgArticlePath directly as a quickie hack to use the
2858 * hardcoded English names of the Special: pages, for ASCII safety.
2860 * @note Since these URLs get dropped directly into emails, using the
2861 * short English names avoids insanely long URL-encoded links, which
2862 * also sometimes can get corrupted in some browsers/mailers
2863 * (bug 6957 with Gmail and Internet Explorer).
2865 * @param $page \type{\string} Special page
2866 * @param $token \type{\string} Token
2867 * @return \type{\string} Formatted URL
2869 protected function getTokenUrl( $page, $token ) {
2870 global $wgArticlePath;
2871 return wfExpandUrl(
2872 str_replace(
2873 '$1',
2874 "Special:$page/$token",
2875 $wgArticlePath ) );
2879 * Mark the e-mail address confirmed.
2881 * @note Call saveSettings() after calling this function to commit the change.
2883 function confirmEmail() {
2884 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2885 return true;
2889 * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2890 * address if it was already confirmed.
2892 * @note Call saveSettings() after calling this function to commit the change.
2894 function invalidateEmail() {
2895 $this->load();
2896 $this->mEmailToken = null;
2897 $this->mEmailTokenExpires = null;
2898 $this->setEmailAuthenticationTimestamp( null );
2899 return true;
2903 * Set the e-mail authentication timestamp.
2904 * @param $timestamp \type{\string} TS_MW timestamp
2906 function setEmailAuthenticationTimestamp( $timestamp ) {
2907 $this->load();
2908 $this->mEmailAuthenticated = $timestamp;
2909 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2913 * Is this user allowed to send e-mails within limits of current
2914 * site configuration?
2915 * @return \type{\bool} True if allowed
2917 function canSendEmail() {
2918 $canSend = $this->isEmailConfirmed();
2919 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2920 return $canSend;
2924 * Is this user allowed to receive e-mails within limits of current
2925 * site configuration?
2926 * @return \type{\bool} True if allowed
2928 function canReceiveEmail() {
2929 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
2933 * Is this user's e-mail address valid-looking and confirmed within
2934 * limits of the current site configuration?
2936 * @note If $wgEmailAuthentication is on, this may require the user to have
2937 * confirmed their address by returning a code or using a password
2938 * sent to the address from the wiki.
2940 * @return \type{\bool} True if confirmed
2942 function isEmailConfirmed() {
2943 global $wgEmailAuthentication;
2944 $this->load();
2945 $confirmed = true;
2946 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2947 if( $this->isAnon() )
2948 return false;
2949 if( !self::isValidEmailAddr( $this->mEmail ) )
2950 return false;
2951 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2952 return false;
2953 return true;
2954 } else {
2955 return $confirmed;
2960 * Check whether there is an outstanding request for e-mail confirmation.
2961 * @return \type{\bool} True if pending
2963 function isEmailConfirmationPending() {
2964 global $wgEmailAuthentication;
2965 return $wgEmailAuthentication &&
2966 !$this->isEmailConfirmed() &&
2967 $this->mEmailToken &&
2968 $this->mEmailTokenExpires > wfTimestamp();
2972 * Get the timestamp of account creation.
2974 * @return \twotypes{\string,\bool} string Timestamp of account creation, or false for
2975 * non-existent/anonymous user accounts.
2977 public function getRegistration() {
2978 return $this->mId > 0
2979 ? $this->mRegistration
2980 : false;
2984 * Get the permissions associated with a given list of groups
2986 * @param $groups \type{\arrayof{\string}} List of internal group names
2987 * @return \type{\arrayof{\string}} List of permission key names for given groups combined
2989 static function getGroupPermissions( $groups ) {
2990 global $wgGroupPermissions;
2991 $rights = array();
2992 foreach( $groups as $group ) {
2993 if( isset( $wgGroupPermissions[$group] ) ) {
2994 $rights = array_merge( $rights,
2995 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2998 return $rights;
3002 * Get all the groups who have a given permission
3004 * @param $role \type{\string} Role to check
3005 * @return \type{\arrayof{\string}} List of internal group names with the given permission
3007 static function getGroupsWithPermission( $role ) {
3008 global $wgGroupPermissions;
3009 $allowedGroups = array();
3010 foreach ( $wgGroupPermissions as $group => $rights ) {
3011 if ( isset( $rights[$role] ) && $rights[$role] ) {
3012 $allowedGroups[] = $group;
3015 return $allowedGroups;
3019 * Get the localized descriptive name for a group, if it exists
3021 * @param $group \type{\string} Internal group name
3022 * @return \type{\string} Localized descriptive group name
3024 static function getGroupName( $group ) {
3025 global $wgMessageCache;
3026 $wgMessageCache->loadAllMessages();
3027 $key = "group-$group";
3028 $name = wfMsg( $key );
3029 return $name == '' || wfEmptyMsg( $key, $name )
3030 ? $group
3031 : $name;
3035 * Get the localized descriptive name for a member of a group, if it exists
3037 * @param $group \type{\string} Internal group name
3038 * @return \type{\string} Localized name for group member
3040 static function getGroupMember( $group ) {
3041 global $wgMessageCache;
3042 $wgMessageCache->loadAllMessages();
3043 $key = "group-$group-member";
3044 $name = wfMsg( $key );
3045 return $name == '' || wfEmptyMsg( $key, $name )
3046 ? $group
3047 : $name;
3051 * Return the set of defined explicit groups.
3052 * The implicit groups (by default *, 'user' and 'autoconfirmed')
3053 * are not included, as they are defined automatically, not in the database.
3054 * @return \type{\arrayof{\string}} Array of internal group names
3056 static function getAllGroups() {
3057 global $wgGroupPermissions;
3058 return array_diff(
3059 array_keys( $wgGroupPermissions ),
3060 self::getImplicitGroups()
3065 * Get a list of all available permissions.
3066 * @return \type{\arrayof{\string}} Array of permission names
3068 static function getAllRights() {
3069 if ( self::$mAllRights === false ) {
3070 global $wgAvailableRights;
3071 if ( count( $wgAvailableRights ) ) {
3072 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3073 } else {
3074 self::$mAllRights = self::$mCoreRights;
3076 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3078 return self::$mAllRights;
3082 * Get a list of implicit groups
3083 * @return \type{\arrayof{\string}} Array of internal group names
3085 public static function getImplicitGroups() {
3086 global $wgImplicitGroups;
3087 $groups = $wgImplicitGroups;
3088 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead
3089 return $groups;
3093 * Get the title of a page describing a particular group
3095 * @param $group \type{\string} Internal group name
3096 * @return \twotypes{Title,\bool} Title of the page if it exists, false otherwise
3098 static function getGroupPage( $group ) {
3099 global $wgMessageCache;
3100 $wgMessageCache->loadAllMessages();
3101 $page = wfMsgForContent( 'grouppage-' . $group );
3102 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3103 $title = Title::newFromText( $page );
3104 if( is_object( $title ) )
3105 return $title;
3107 return false;
3111 * Create a link to the group in HTML, if available;
3112 * else return the group name.
3114 * @param $group \type{\string} Internal name of the group
3115 * @param $text \type{\string} The text of the link
3116 * @return \type{\string} HTML link to the group
3118 static function makeGroupLinkHTML( $group, $text = '' ) {
3119 if( $text == '' ) {
3120 $text = self::getGroupName( $group );
3122 $title = self::getGroupPage( $group );
3123 if( $title ) {
3124 global $wgUser;
3125 $sk = $wgUser->getSkin();
3126 return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
3127 } else {
3128 return $text;
3133 * Create a link to the group in Wikitext, if available;
3134 * else return the group name.
3136 * @param $group \type{\string} Internal name of the group
3137 * @param $text \type{\string} The text of the link
3138 * @return \type{\string} Wikilink to the group
3140 static function makeGroupLinkWiki( $group, $text = '' ) {
3141 if( $text == '' ) {
3142 $text = self::getGroupName( $group );
3144 $title = self::getGroupPage( $group );
3145 if( $title ) {
3146 $page = $title->getPrefixedText();
3147 return "[[$page|$text]]";
3148 } else {
3149 return $text;
3154 * Increment the user's edit-count field.
3155 * Will have no effect for anonymous users.
3157 function incEditCount() {
3158 if( !$this->isAnon() ) {
3159 $dbw = wfGetDB( DB_MASTER );
3160 $dbw->update( 'user',
3161 array( 'user_editcount=user_editcount+1' ),
3162 array( 'user_id' => $this->getId() ),
3163 __METHOD__ );
3165 // Lazy initialization check...
3166 if( $dbw->affectedRows() == 0 ) {
3167 // Pull from a slave to be less cruel to servers
3168 // Accuracy isn't the point anyway here
3169 $dbr = wfGetDB( DB_SLAVE );
3170 $count = $dbr->selectField( 'revision',
3171 'COUNT(rev_user)',
3172 array( 'rev_user' => $this->getId() ),
3173 __METHOD__ );
3175 // Now here's a goddamn hack...
3176 if( $dbr !== $dbw ) {
3177 // If we actually have a slave server, the count is
3178 // at least one behind because the current transaction
3179 // has not been committed and replicated.
3180 $count++;
3181 } else {
3182 // But if DB_SLAVE is selecting the master, then the
3183 // count we just read includes the revision that was
3184 // just added in the working transaction.
3187 $dbw->update( 'user',
3188 array( 'user_editcount' => $count ),
3189 array( 'user_id' => $this->getId() ),
3190 __METHOD__ );
3193 // edit count in user cache too
3194 $this->invalidateCache();
3198 * Get the description of a given right
3200 * @param $right \type{\string} Right to query
3201 * @return \type{\string} Localized description of the right
3203 static function getRightDescription( $right ) {
3204 global $wgMessageCache;
3205 $wgMessageCache->loadAllMessages();
3206 $key = "right-$right";
3207 $name = wfMsg( $key );
3208 return $name == '' || wfEmptyMsg( $key, $name )
3209 ? $right
3210 : $name;
3214 * Make an old-style password hash
3216 * @param $password \type{\string} Plain-text password
3217 * @param $userId \type{\string} %User ID
3218 * @return \type{\string} Password hash
3220 static function oldCrypt( $password, $userId ) {
3221 global $wgPasswordSalt;
3222 if ( $wgPasswordSalt ) {
3223 return md5( $userId . '-' . md5( $password ) );
3224 } else {
3225 return md5( $password );
3230 * Make a new-style password hash
3232 * @param $password \type{\string} Plain-text password
3233 * @param $salt \type{\string} Optional salt, may be random or the user ID.
3234 * If unspecified or false, will generate one automatically
3235 * @return \type{\string} Password hash
3237 static function crypt( $password, $salt = false ) {
3238 global $wgPasswordSalt;
3240 if($wgPasswordSalt) {
3241 if ( $salt === false ) {
3242 $salt = substr( wfGenerateToken(), 0, 8 );
3244 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3245 } else {
3246 return ':A:' . md5( $password);
3251 * Compare a password hash with a plain-text password. Requires the user
3252 * ID if there's a chance that the hash is an old-style hash.
3254 * @param $hash \type{\string} Password hash
3255 * @param $password \type{\string} Plain-text password to compare
3256 * @param $userId \type{\string} %User ID for old-style password salt
3257 * @return \type{\bool} True if matches, false otherwise
3259 static function comparePasswords( $hash, $password, $userId = false ) {
3260 $m = false;
3261 $type = substr( $hash, 0, 3 );
3262 if ( $type == ':A:' ) {
3263 # Unsalted
3264 return md5( $password ) === substr( $hash, 3 );
3265 } elseif ( $type == ':B:' ) {
3266 # Salted
3267 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3268 return md5( $salt.'-'.md5( $password ) ) == $realHash;
3269 } else {
3270 # Old-style
3271 return self::oldCrypt( $password, $userId ) === $hash;
3276 * Add a newuser log entry for this user
3277 * @param bool $byEmail, account made by email?
3279 public function addNewUserLogEntry( $byEmail = false ) {
3280 global $wgUser, $wgContLang, $wgNewUserLog;
3281 if( empty($wgNewUserLog) ) {
3282 return true; // disabled
3284 $talk = $wgContLang->getFormattedNsText( NS_TALK );
3285 if( $this->getName() == $wgUser->getName() ) {
3286 $action = 'create';
3287 $message = '';
3288 } else {
3289 $action = 'create2';
3290 $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
3292 $log = new LogPage( 'newusers' );
3293 $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
3294 return true;
3298 * Add an autocreate newuser log entry for this user
3299 * Used by things like CentralAuth and perhaps other authplugins.
3301 public function addNewUserLogEntryAutoCreate() {
3302 global $wgNewUserLog;
3303 if( empty($wgNewUserLog) ) {
3304 return true; // disabled
3306 $log = new LogPage( 'newusers', false );
3307 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3308 return true;