]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/User.php
Mediawiki 1.15.3-scripts
[autoinstalls/mediawiki.git] / includes / User.php
1 <?php
2 /**
3  * Implements the User class for the %MediaWiki software.
4  * @file
5  */
6
7 /**
8  * \int Number of characters in user_token field.
9  * @ingroup Constants
10  */
11 define( 'USER_TOKEN_LENGTH', 32 );
12
13 /**
14  * \int Serialized record version.
15  * @ingroup Constants
16  */
17 define( 'MW_USER_VERSION', 6 );
18
19 /**
20  * \string Some punctuation to prevent editing from broken text-mangling proxies.
21  * @ingroup Constants
22  */
23 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
24
25 /**
26  * Thrown by User::setPassword() on error.
27  * @ingroup Exception
28  */
29 class PasswordError extends MWException {
30         // NOP
31 }
32
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.
42  */
43 class User {
44
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
51          */
52         public static $mToggles = array(
53                 'highlightbroken',
54                 'justify',
55                 'hideminor',
56                 'extendwatchlist',
57                 'usenewrc',
58                 'numberheadings',
59                 'showtoolbar',
60                 'editondblclick',
61                 'editsection',
62                 'editsectiononrightclick',
63                 'showtoc',
64                 'rememberpassword',
65                 'editwidth',
66                 'watchcreations',
67                 'watchdefault',
68                 'watchmoves',
69                 'watchdeletion',
70                 'minordefault',
71                 'previewontop',
72                 'previewonfirst',
73                 'nocache',
74                 'enotifwatchlistpages',
75                 'enotifusertalkpages',
76                 'enotifminoredits',
77                 'enotifrevealaddr',
78                 'shownumberswatching',
79                 'fancysig',
80                 'externaleditor',
81                 'externaldiff',
82                 'showjumplinks',
83                 'uselivepreview',
84                 'forceeditsummary',
85                 'watchlisthideminor',
86                 'watchlisthidebots',
87                 'watchlisthideown',
88                 'watchlisthideanons',
89                 'watchlisthideliu',
90                 'ccmeonemails',
91                 'diffonly',
92                 'showhiddencats',
93                 'noconvertlink',
94                 'norollbackdiff',
95         );
96
97         /**
98          * \type{\arrayof{\string}} List of member variables which are saved to the 
99          * shared cache (memcached). Any operation which changes the 
100          * corresponding database fields must call a cache-clearing function.
101          * @showinitializer
102          */
103         static $mCacheVars = array(
104                 // user table
105                 'mId',
106                 'mName',
107                 'mRealName',
108                 'mPassword',
109                 'mNewpassword',
110                 'mNewpassTime',
111                 'mEmail',
112                 'mOptions',
113                 'mTouched',
114                 'mToken',
115                 'mEmailAuthenticated',
116                 'mEmailToken',
117                 'mEmailTokenExpires',
118                 'mRegistration',
119                 'mEditCount',
120                 // user_group table
121                 'mGroups',
122         );
123
124         /**
125          * \type{\arrayof{\string}} Core rights.
126          * Each of these should have a corresponding message of the form 
127          * "right-$right".
128          * @showinitializer
129          */
130         static $mCoreRights = array(
131                 'apihighlimits',
132                 'autoconfirmed',
133                 'autopatrol',
134                 'bigdelete',
135                 'block',
136                 'blockemail',
137                 'bot',
138                 'browsearchive',
139                 'createaccount',
140                 'createpage',
141                 'createtalk',
142                 'delete',
143                 'deletedhistory',
144                 'deleterevision',
145                 'edit',
146                 'editinterface',
147                 'editusercssjs',
148                 'hideuser',
149                 'import',
150                 'importupload',
151                 'ipblock-exempt',
152                 'markbotedits',
153                 'minoredit',
154                 'move',
155                 'movefile',
156                 'move-rootuserpages',
157                 'move-subpages',
158                 'nominornewtalk',
159                 'noratelimit',
160                 'override-export-depth',
161                 'patrol',
162                 'protect',
163                 'proxyunbannable',
164                 'purge',
165                 'read',
166                 'reupload',
167                 'reupload-shared',
168                 'rollback',
169                 'siteadmin',
170                 'suppressionlog',
171                 'suppressredirect',
172                 'suppressrevision',
173                 'trackback',
174                 'undelete',
175                 'unwatchedpages',
176                 'upload',
177                 'upload_by_url',
178                 'userrights',
179                 'userrights-interwiki',
180                 'writeapi',
181         );
182         /**
183          * \string Cached results of getAllRights()
184          */
185         static $mAllRights = false;
186
187         /** @name Cache variables */
188         //@{
189         var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
190                 $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
191                 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
192         //@}
193
194         /**
195          * \bool Whether the cache variables have been loaded.
196          */
197         var $mDataLoaded, $mAuthLoaded;
198
199         /**
200          * \string Initialization data source if mDataLoaded==false. May be one of:
201          *  - 'defaults'   anonymous user initialised from class defaults
202          *  - 'name'       initialise from mName
203          *  - 'id'         initialise from mId
204          *  - 'session'    log in from cookies or session if possible
205          *
206          * Use the User::newFrom*() family of functions to set this.
207          */
208         var $mFrom;
209
210         /** @name Lazy-initialized variables, invalidated with clearInstanceCache */
211         //@{
212         var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
213                 $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally, 
214                 $mLocked, $mHideName;
215         //@}
216
217         /**
218          * Lightweight constructor for an anonymous user.
219          * Use the User::newFrom* factory functions for other kinds of users.
220          * 
221          * @see newFromName()
222          * @see newFromId()
223          * @see newFromConfirmationCode()
224          * @see newFromSession()
225          * @see newFromRow()
226          */
227         function User() {
228                 $this->clearInstanceCache( 'defaults' );
229         }
230
231         /**
232          * Load the user table data for this object from the source given by mFrom.
233          */
234         function load() {
235                 if ( $this->mDataLoaded ) {
236                         return;
237                 }
238                 wfProfileIn( __METHOD__ );
239
240                 # Set it now to avoid infinite recursion in accessors
241                 $this->mDataLoaded = true;
242
243                 switch ( $this->mFrom ) {
244                         case 'defaults':
245                                 $this->loadDefaults();
246                                 break;
247                         case 'name':
248                                 $this->mId = self::idFromName( $this->mName );
249                                 if ( !$this->mId ) {
250                                         # Nonexistent user placeholder object
251                                         $this->loadDefaults( $this->mName );
252                                 } else {
253                                         $this->loadFromId();
254                                 }
255                                 break;
256                         case 'id':
257                                 $this->loadFromId();
258                                 break;
259                         case 'session':
260                                 $this->loadFromSession();
261                                 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
262                                 break;
263                         default:
264                                 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
265                 }
266                 wfProfileOut( __METHOD__ );
267         }
268
269         /**
270          * Load user table data, given mId has already been set.
271          * @return \bool false if the ID does not exist, true otherwise
272          * @private
273          */
274         function loadFromId() {
275                 global $wgMemc;
276                 if ( $this->mId == 0 ) {
277                         $this->loadDefaults();
278                         return false;
279                 }
280
281                 # Try cache
282                 $key = wfMemcKey( 'user', 'id', $this->mId );
283                 $data = $wgMemc->get( $key );
284                 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
285                         # Object is expired, load from DB
286                         $data = false;
287                 }
288
289                 if ( !$data ) {
290                         wfDebug( "Cache miss for user {$this->mId}\n" );
291                         # Load from DB
292                         if ( !$this->loadFromDatabase() ) {
293                                 # Can't load from ID, user is anonymous
294                                 return false;
295                         }
296                         $this->saveToCache();
297                 } else {
298                         wfDebug( "Got user {$this->mId} from cache\n" );
299                         # Restore from cache
300                         foreach ( self::$mCacheVars as $name ) {
301                                 $this->$name = $data[$name];
302                         }
303                 }
304                 return true;
305         }
306
307         /**
308          * Save user data to the shared cache
309          */
310         function saveToCache() {
311                 $this->load();
312                 $this->loadGroups();
313                 if ( $this->isAnon() ) {
314                         // Anonymous users are uncached
315                         return;
316                 }
317                 $data = array();
318                 foreach ( self::$mCacheVars as $name ) {
319                         $data[$name] = $this->$name;
320                 }
321                 $data['mVersion'] = MW_USER_VERSION;
322                 $key = wfMemcKey( 'user', 'id', $this->mId );
323                 global $wgMemc;
324                 $wgMemc->set( $key, $data );
325         }
326         
327         
328         /** @name newFrom*() static factory methods */
329         //@{
330
331         /**
332          * Static factory method for creation from username.
333          *
334          * This is slightly less efficient than newFromId(), so use newFromId() if
335          * you have both an ID and a name handy.
336          *
337          * @param $name \string Username, validated by Title::newFromText()
338          * @param $validate \mixed Validate username. Takes the same parameters as
339          *    User::getCanonicalName(), except that true is accepted as an alias
340          *    for 'valid', for BC.
341          *
342          * @return \type{User} The User object, or null if the username is invalid. If the 
343          *    username is not present in the database, the result will be a user object 
344          *    with a name, zero user ID and default settings.
345          */
346         static function newFromName( $name, $validate = 'valid' ) {
347                 if ( $validate === true ) {
348                         $validate = 'valid';
349                 }
350                 $name = self::getCanonicalName( $name, $validate );
351                 if ( $name === false ) {
352                         return null;
353                 } else {
354                         # Create unloaded user object
355                         $u = new User;
356                         $u->mName = $name;
357                         $u->mFrom = 'name';
358                         return $u;
359                 }
360         }
361
362         /**
363          * Static factory method for creation from a given user ID.
364          *
365          * @param $id \int Valid user ID
366          * @return \type{User} The corresponding User object
367          */
368         static function newFromId( $id ) {
369                 $u = new User;
370                 $u->mId = $id;
371                 $u->mFrom = 'id';
372                 return $u;
373         }
374
375         /**
376          * Factory method to fetch whichever user has a given email confirmation code.
377          * This code is generated when an account is created or its e-mail address
378          * has changed.
379          *
380          * If the code is invalid or has expired, returns NULL.
381          *
382          * @param $code \string Confirmation code
383          * @return \type{User}
384          */
385         static function newFromConfirmationCode( $code ) {
386                 $dbr = wfGetDB( DB_SLAVE );
387                 $id = $dbr->selectField( 'user', 'user_id', array(
388                         'user_email_token' => md5( $code ),
389                         'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
390                         ) );
391                 if( $id !== false ) {
392                         return User::newFromId( $id );
393                 } else {
394                         return null;
395                 }
396         }
397
398         /**
399          * Create a new user object using data from session or cookies. If the
400          * login credentials are invalid, the result is an anonymous user.
401          *
402          * @return \type{User}
403          */
404         static function newFromSession() {
405                 $user = new User;
406                 $user->mFrom = 'session';
407                 return $user;
408         }
409
410         /**
411          * Create a new user object from a user row.
412          * The row should have all fields from the user table in it.
413          * @param $row array A row from the user table
414          * @return \type{User}
415          */
416         static function newFromRow( $row ) {
417                 $user = new User;
418                 $user->loadFromRow( $row );
419                 return $user;
420         }
421         
422         //@}
423         
424
425         /**
426          * Get the username corresponding to a given user ID
427          * @param $id \int User ID
428          * @return \string The corresponding username
429          */
430         static function whoIs( $id ) {
431                 $dbr = wfGetDB( DB_SLAVE );
432                 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
433         }
434
435         /**
436          * Get the real name of a user given their user ID
437          *
438          * @param $id \int User ID
439          * @return \string The corresponding user's real name
440          */
441         static function whoIsReal( $id ) {
442                 $dbr = wfGetDB( DB_SLAVE );
443                 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ );
444         }
445
446         /**
447          * Get database id given a user name
448          * @param $name \string Username
449          * @return \types{\int,\null} The corresponding user's ID, or null if user is nonexistent
450          */
451         static function idFromName( $name ) {
452                 $nt = Title::makeTitleSafe( NS_USER, $name );
453                 if( is_null( $nt ) ) {
454                         # Illegal name
455                         return null;
456                 }
457                 $dbr = wfGetDB( DB_SLAVE );
458                 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
459
460                 if ( $s === false ) {
461                         return 0;
462                 } else {
463                         return $s->user_id;
464                 }
465         }
466
467         /**
468          * Does the string match an anonymous IPv4 address?
469          *
470          * This function exists for username validation, in order to reject
471          * usernames which are similar in form to IP addresses. Strings such
472          * as 300.300.300.300 will return true because it looks like an IP
473          * address, despite not being strictly valid.
474          *
475          * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
476          * address because the usemod software would "cloak" anonymous IP
477          * addresses like this, if we allowed accounts like this to be created
478          * new users could get the old edits of these anonymous users.
479          *
480          * @param $name \string String to match
481          * @return \bool True or false
482          */
483         static function isIP( $name ) {
484                 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
485         }
486
487         /**
488          * Is the input a valid username?
489          *
490          * Checks if the input is a valid username, we don't want an empty string,
491          * an IP address, anything that containins slashes (would mess up subpages),
492          * is longer than the maximum allowed username size or doesn't begin with
493          * a capital letter.
494          *
495          * @param $name \string String to match
496          * @return \bool True or false
497          */
498         static function isValidUserName( $name ) {
499                 global $wgContLang, $wgMaxNameChars;
500
501                 if ( $name == ''
502                 || User::isIP( $name )
503                 || strpos( $name, '/' ) !== false
504                 || strlen( $name ) > $wgMaxNameChars
505                 || $name != $wgContLang->ucfirst( $name ) ) {
506                         wfDebugLog( 'username', __METHOD__ .
507                                 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
508                         return false;
509                 }
510
511                 // Ensure that the name can't be misresolved as a different title,
512                 // such as with extra namespace keys at the start.
513                 $parsed = Title::newFromText( $name );
514                 if( is_null( $parsed )
515                         || $parsed->getNamespace()
516                         || strcmp( $name, $parsed->getPrefixedText() ) ) {
517                         wfDebugLog( 'username', __METHOD__ .
518                                 ": '$name' invalid due to ambiguous prefixes" );
519                         return false;
520                 }
521
522                 // Check an additional blacklist of troublemaker characters.
523                 // Should these be merged into the title char list?
524                 $unicodeBlacklist = '/[' .
525                         '\x{0080}-\x{009f}' . # iso-8859-1 control chars
526                         '\x{00a0}' .          # non-breaking space
527                         '\x{2000}-\x{200f}' . # various whitespace
528                         '\x{2028}-\x{202f}' . # breaks and control chars
529                         '\x{3000}' .          # ideographic space
530                         '\x{e000}-\x{f8ff}' . # private use
531                         ']/u';
532                 if( preg_match( $unicodeBlacklist, $name ) ) {
533                         wfDebugLog( 'username', __METHOD__ .
534                                 ": '$name' invalid due to blacklisted characters" );
535                         return false;
536                 }
537
538                 return true;
539         }
540
541         /**
542          * Usernames which fail to pass this function will be blocked
543          * from user login and new account registrations, but may be used
544          * internally by batch processes.
545          *
546          * If an account already exists in this form, login will be blocked
547          * by a failure to pass this function.
548          *
549          * @param $name \string String to match
550          * @return \bool True or false
551          */
552         static function isUsableName( $name ) {
553                 global $wgReservedUsernames;
554                 // Must be a valid username, obviously ;)
555                 if ( !self::isValidUserName( $name ) ) {
556                         return false;
557                 }
558
559                 static $reservedUsernames = false;
560                 if ( !$reservedUsernames ) {
561                         $reservedUsernames = $wgReservedUsernames;
562                         wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
563                 }
564
565                 // Certain names may be reserved for batch processes.
566                 foreach ( $reservedUsernames as $reserved ) {
567                         if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
568                                 $reserved = wfMsgForContent( substr( $reserved, 4 ) );
569                         }
570                         if ( $reserved == $name ) {
571                                 return false;
572                         }
573                 }
574                 return true;
575         }
576
577         /**
578          * Usernames which fail to pass this function will be blocked
579          * from new account registrations, but may be used internally
580          * either by batch processes or by user accounts which have
581          * already been created.
582          *
583          * Additional character blacklisting may be added here
584          * rather than in isValidUserName() to avoid disrupting
585          * existing accounts.
586          *
587          * @param $name \string String to match
588          * @return \bool True or false
589          */
590         static function isCreatableName( $name ) {
591                 global $wgInvalidUsernameCharacters;
592                 return
593                         self::isUsableName( $name ) &&
594
595                         // Registration-time character blacklisting...
596                         !preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name );
597         }
598
599         /**
600          * Is the input a valid password for this user?
601          *
602          * @param $password \string Desired password
603          * @return \bool True or false
604          */
605         function isValidPassword( $password ) {
606                 global $wgMinimalPasswordLength, $wgContLang;
607
608                 $result = null;
609                 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
610                         return $result;
611                 if( $result === false )
612                         return false;
613
614                 // Password needs to be long enough, and can't be the same as the username
615                 return strlen( $password ) >= $wgMinimalPasswordLength
616                         && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
617         }
618
619         /**
620          * Does a string look like an e-mail address?
621          *
622          * There used to be a regular expression here, it got removed because it
623          * rejected valid addresses. Actually just check if there is '@' somewhere
624          * in the given address.
625          *
626          * @todo Check for RFC 2822 compilance (bug 959)
627          *
628          * @param $addr \string E-mail address
629          * @return \bool True or false
630          */
631         public static function isValidEmailAddr( $addr ) {
632                 $result = null;
633                 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
634                         return $result;
635                 }
636
637                 return strpos( $addr, '@' ) !== false;
638         }
639
640         /**
641          * Given unvalidated user input, return a canonical username, or false if
642          * the username is invalid.
643          * @param $name \string User input
644          * @param $validate \types{\string,\bool} Type of validation to use:
645          *                - false        No validation
646          *                - 'valid'      Valid for batch processes
647          *                - 'usable'     Valid for batch processes and login
648          *                - 'creatable'  Valid for batch processes, login and account creation
649          */
650         static function getCanonicalName( $name, $validate = 'valid' ) {
651                 # Force usernames to capital
652                 global $wgContLang;
653                 $name = $wgContLang->ucfirst( $name );
654
655                 # Reject names containing '#'; these will be cleaned up
656                 # with title normalisation, but then it's too late to
657                 # check elsewhere
658                 if( strpos( $name, '#' ) !== false )
659                         return false;
660
661                 # Clean up name according to title rules
662                 $t = ($validate === 'valid') ? 
663                         Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
664                 # Check for invalid titles
665                 if( is_null( $t ) ) {
666                         return false;
667                 }
668
669                 # Reject various classes of invalid names
670                 $name = $t->getText();
671                 global $wgAuth;
672                 $name = $wgAuth->getCanonicalName( $t->getText() );
673
674                 switch ( $validate ) {
675                         case false:
676                                 break;
677                         case 'valid':
678                                 if ( !User::isValidUserName( $name ) ) {
679                                         $name = false;
680                                 }
681                                 break;
682                         case 'usable':
683                                 if ( !User::isUsableName( $name ) ) {
684                                         $name = false;
685                                 }
686                                 break;
687                         case 'creatable':
688                                 if ( !User::isCreatableName( $name ) ) {
689                                         $name = false;
690                                 }
691                                 break;
692                         default:
693                                 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
694                 }
695                 return $name;
696         }
697
698         /**
699          * Count the number of edits of a user
700          * @todo It should not be static and some day should be merged as proper member function / deprecated -- domas
701          *
702          * @param $uid \int User ID to check
703          * @return \int The user's edit count
704          */
705         static function edits( $uid ) {
706                 wfProfileIn( __METHOD__ );
707                 $dbr = wfGetDB( DB_SLAVE );
708                 // check if the user_editcount field has been initialized
709                 $field = $dbr->selectField(
710                         'user', 'user_editcount',
711                         array( 'user_id' => $uid ),
712                         __METHOD__
713                 );
714
715                 if( $field === null ) { // it has not been initialized. do so.
716                         $dbw = wfGetDB( DB_MASTER );
717                         $count = $dbr->selectField(
718                                 'revision', 'count(*)',
719                                 array( 'rev_user' => $uid ),
720                                 __METHOD__
721                         );
722                         $dbw->update(
723                                 'user',
724                                 array( 'user_editcount' => $count ),
725                                 array( 'user_id' => $uid ),
726                                 __METHOD__
727                         );
728                 } else {
729                         $count = $field;
730                 }
731                 wfProfileOut( __METHOD__ );
732                 return $count;
733         }
734
735         /**
736          * Return a random password. Sourced from mt_rand, so it's not particularly secure.
737          * @todo hash random numbers to improve security, like generateToken()
738          *
739          * @return \string New random password
740          */
741         static function randomPassword() {
742                 global $wgMinimalPasswordLength;
743                 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
744                 $l = strlen( $pwchars ) - 1;
745
746                 $pwlength = max( 7, $wgMinimalPasswordLength );
747                 $digit = mt_rand(0, $pwlength - 1);
748                 $np = '';
749                 for ( $i = 0; $i < $pwlength; $i++ ) {
750                         $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
751                 }
752                 return $np;
753         }
754
755         /**
756          * Set cached properties to default. 
757          *
758          * @note This no longer clears uncached lazy-initialised properties; 
759          *       the constructor does that instead.
760          * @private
761          */
762         function loadDefaults( $name = false ) {
763                 wfProfileIn( __METHOD__ );
764
765                 global $wgCookiePrefix;
766
767                 $this->mId = 0;
768                 $this->mName = $name;
769                 $this->mRealName = '';
770                 $this->mPassword = $this->mNewpassword = '';
771                 $this->mNewpassTime = null;
772                 $this->mEmail = '';
773                 $this->mOptions = null; # Defer init
774
775                 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
776                         $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
777                 } else {
778                         $this->mTouched = '0'; # Allow any pages to be cached
779                 }
780
781                 $this->setToken(); # Random
782                 $this->mEmailAuthenticated = null;
783                 $this->mEmailToken = '';
784                 $this->mEmailTokenExpires = null;
785                 $this->mRegistration = wfTimestamp( TS_MW );
786                 $this->mGroups = array();
787
788                 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
789
790                 wfProfileOut( __METHOD__ );
791         }
792
793         /**
794          * @deprecated Use wfSetupSession().
795          */
796         function SetupSession() {
797                 wfDeprecated( __METHOD__ );
798                 wfSetupSession();
799         }
800
801         /**
802          * Load user data from the session or login cookie. If there are no valid
803          * credentials, initialises the user as an anonymous user.
804          * @return \bool True if the user is logged in, false otherwise.
805          */
806         private function loadFromSession() {
807                 global $wgMemc, $wgCookiePrefix;
808
809                 $result = null;
810                 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
811                 if ( $result !== null ) {
812                         return $result;
813                 }
814
815                 if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
816                         $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
817                         if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
818                                 $this->loadDefaults(); // Possible collision!
819                                 wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and 
820                                         cookie user ID ($sId) don't match!" );
821                                 return false;
822                         }
823                         $_SESSION['wsUserID'] = $sId;
824                 } else if ( isset( $_SESSION['wsUserID'] ) ) {
825                         if ( $_SESSION['wsUserID'] != 0 ) {
826                                 $sId = $_SESSION['wsUserID'];
827                         } else {
828                                 $this->loadDefaults();
829                                 return false;
830                         }
831                 } else {
832                         $this->loadDefaults();
833                         return false;
834                 }
835
836                 if ( isset( $_SESSION['wsUserName'] ) ) {
837                         $sName = $_SESSION['wsUserName'];
838                 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
839                         $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
840                         $_SESSION['wsUserName'] = $sName;
841                 } else {
842                         $this->loadDefaults();
843                         return false;
844                 }
845
846                 $passwordCorrect = FALSE;
847                 $this->mId = $sId;
848                 if ( !$this->loadFromId() ) {
849                         # Not a valid ID, loadFromId has switched the object to anon for us
850                         return false;
851                 }
852
853                 if ( isset( $_SESSION['wsToken'] ) ) {
854                         $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
855                         $from = 'session';
856                 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
857                         $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
858                         $from = 'cookie';
859                 } else {
860                         # No session or persistent login cookie
861                         $this->loadDefaults();
862                         return false;
863                 }
864
865                 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
866                         $_SESSION['wsToken'] = $this->mToken;
867                         wfDebug( "Logged in from $from\n" );
868                         return true;
869                 } else {
870                         # Invalid credentials
871                         wfDebug( "Can't log in from $from, invalid credentials\n" );
872                         $this->loadDefaults();
873                         return false;
874                 }
875         }
876
877         /**
878          * Load user and user_group data from the database.
879          * $this::mId must be set, this is how the user is identified.
880          *
881          * @return \bool True if the user exists, false if the user is anonymous
882          * @private
883          */
884         function loadFromDatabase() {
885                 # Paranoia
886                 $this->mId = intval( $this->mId );
887
888                 /** Anonymous user */
889                 if( !$this->mId ) {
890                         $this->loadDefaults();
891                         return false;
892                 }
893
894                 $dbr = wfGetDB( DB_MASTER );
895                 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
896
897                 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
898
899                 if ( $s !== false ) {
900                         # Initialise user table data
901                         $this->loadFromRow( $s );
902                         $this->mGroups = null; // deferred
903                         $this->getEditCount(); // revalidation for nulls
904                         return true;
905                 } else {
906                         # Invalid user_id
907                         $this->mId = 0;
908                         $this->loadDefaults();
909                         return false;
910                 }
911         }
912
913         /**
914          * Initialize this object from a row from the user table.
915          *
916          * @param $row \type{\arrayof{\mixed}} Row from the user table to load.
917          */
918         function loadFromRow( $row ) {
919                 $this->mDataLoaded = true;
920
921                 if ( isset( $row->user_id ) ) {
922                         $this->mId = intval( $row->user_id );
923                 }
924                 $this->mName = $row->user_name;
925                 $this->mRealName = $row->user_real_name;
926                 $this->mPassword = $row->user_password;
927                 $this->mNewpassword = $row->user_newpassword;
928                 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
929                 $this->mEmail = $row->user_email;
930                 $this->decodeOptions( $row->user_options );
931                 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
932                 $this->mToken = $row->user_token;
933                 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
934                 $this->mEmailToken = $row->user_email_token;
935                 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
936                 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
937                 $this->mEditCount = $row->user_editcount; 
938         }
939
940         /**
941          * Load the groups from the database if they aren't already loaded.
942          * @private
943          */
944         function loadGroups() {
945                 if ( is_null( $this->mGroups ) ) {
946                         $dbr = wfGetDB( DB_MASTER );
947                         $res = $dbr->select( 'user_groups',
948                                 array( 'ug_group' ),
949                                 array( 'ug_user' => $this->mId ),
950                                 __METHOD__ );
951                         $this->mGroups = array();
952                         while( $row = $dbr->fetchObject( $res ) ) {
953                                 $this->mGroups[] = $row->ug_group;
954                         }
955                 }
956         }
957
958         /**
959          * Clear various cached data stored in this object.
960          * @param $reloadFrom \string Reload user and user_groups table data from a
961          *   given source. May be "name", "id", "defaults", "session", or false for
962          *   no reload.
963          */
964         function clearInstanceCache( $reloadFrom = false ) {
965                 $this->mNewtalk = -1;
966                 $this->mDatePreference = null;
967                 $this->mBlockedby = -1; # Unset
968                 $this->mHash = false;
969                 $this->mSkin = null;
970                 $this->mRights = null;
971                 $this->mEffectiveGroups = null;
972
973                 if ( $reloadFrom ) {
974                         $this->mDataLoaded = false;
975                         $this->mFrom = $reloadFrom;
976                 }
977         }
978
979         /**
980          * Combine the language default options with any site-specific options
981          * and add the default language variants.
982          *
983          * @return \type{\arrayof{\string}} Array of options
984          */
985         static function getDefaultOptions() {
986                 global $wgNamespacesToBeSearchedDefault;
987                 /**
988                  * Site defaults will override the global/language defaults
989                  */
990                 global $wgDefaultUserOptions, $wgContLang;
991                 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
992
993                 /**
994                  * default language setting
995                  */
996                 $variant = $wgContLang->getPreferredVariant( false );
997                 $defOpt['variant'] = $variant;
998                 $defOpt['language'] = $variant;
999
1000                 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
1001                         $defOpt['searchNs'.$nsnum] = $val;
1002                 }
1003                 return $defOpt;
1004         }
1005
1006         /**
1007          * Get a given default option value.
1008          *
1009          * @param $opt \string Name of option to retrieve
1010          * @return \string Default option value
1011          */
1012         public static function getDefaultOption( $opt ) {
1013                 $defOpts = self::getDefaultOptions();
1014                 if( isset( $defOpts[$opt] ) ) {
1015                         return $defOpts[$opt];
1016                 } else {
1017                         return '';
1018                 }
1019         }
1020
1021         /**
1022          * Get a list of user toggle names
1023          * @return \type{\arrayof{\string}} Array of user toggle names
1024          */
1025         static function getToggles() {
1026                 global $wgContLang, $wgUseRCPatrol;
1027                 $extraToggles = array();
1028                 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
1029                 if( $wgUseRCPatrol ) {
1030                         $extraToggles[] = 'hidepatrolled';
1031                         $extraToggles[] = 'newpageshidepatrolled';
1032                         $extraToggles[] = 'watchlisthidepatrolled';
1033                 }
1034                 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
1035         }
1036
1037
1038         /**
1039          * Get blocking information
1040          * @private
1041          * @param $bFromSlave \bool Whether to check the slave database first. To
1042          *                    improve performance, non-critical checks are done
1043          *                    against slaves. Check when actually saving should be
1044          *                    done against master.
1045          */
1046         function getBlockedStatus( $bFromSlave = true ) {
1047                 global $wgEnableSorbs, $wgProxyWhitelist;
1048
1049                 if ( -1 != $this->mBlockedby ) {
1050                         wfDebug( "User::getBlockedStatus: already loaded.\n" );
1051                         return;
1052                 }
1053
1054                 wfProfileIn( __METHOD__ );
1055                 wfDebug( __METHOD__.": checking...\n" );
1056
1057                 // Initialize data...
1058                 // Otherwise something ends up stomping on $this->mBlockedby when
1059                 // things get lazy-loaded later, causing false positive block hits
1060                 // due to -1 !== 0. Probably session-related... Nothing should be
1061                 // overwriting mBlockedby, surely?
1062                 $this->load();
1063                 
1064                 $this->mBlockedby = 0;
1065                 $this->mHideName = 0;
1066                 $this->mAllowUsertalk = 0;
1067                 $ip = wfGetIP();
1068
1069                 if ($this->isAllowed( 'ipblock-exempt' ) ) {
1070                         # Exempt from all types of IP-block
1071                         $ip = '';
1072                 }
1073
1074                 # User/IP blocking
1075                 $this->mBlock = new Block();
1076                 $this->mBlock->fromMaster( !$bFromSlave );
1077                 if ( $this->mBlock->load( $ip , $this->mId ) ) {
1078                         wfDebug( __METHOD__.": Found block.\n" );
1079                         $this->mBlockedby = $this->mBlock->mBy;
1080                         $this->mBlockreason = $this->mBlock->mReason;
1081                         $this->mHideName = $this->mBlock->mHideName;
1082                         $this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
1083                         if ( $this->isLoggedIn() ) {
1084                                 $this->spreadBlock();
1085                         }
1086                 } else {
1087                         // Bug 13611: don't remove mBlock here, to allow account creation blocks to 
1088                         // apply to users. Note that the existence of $this->mBlock is not used to 
1089                         // check for edit blocks, $this->mBlockedby is instead.
1090                 }
1091
1092                 # Proxy blocking
1093                 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
1094                         # Local list
1095                         if ( wfIsLocallyBlockedProxy( $ip ) ) {
1096                                 $this->mBlockedby = wfMsg( 'proxyblocker' );
1097                                 $this->mBlockreason = wfMsg( 'proxyblockreason' );
1098                         }
1099
1100                         # DNSBL
1101                         if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
1102                                 if ( $this->inSorbsBlacklist( $ip ) ) {
1103                                         $this->mBlockedby = wfMsg( 'sorbs' );
1104                                         $this->mBlockreason = wfMsg( 'sorbsreason' );
1105                                 }
1106                         }
1107                 }
1108
1109                 # Extensions
1110                 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
1111
1112                 wfProfileOut( __METHOD__ );
1113         }
1114
1115         /**
1116          * Whether the given IP is in the SORBS blacklist.
1117          *
1118          * @param $ip \string IP to check
1119          * @return \bool True if blacklisted.
1120          */
1121         function inSorbsBlacklist( $ip ) {
1122                 global $wgEnableSorbs, $wgSorbsUrl;
1123
1124                 return $wgEnableSorbs &&
1125                         $this->inDnsBlacklist( $ip, $wgSorbsUrl );
1126         }
1127
1128         /**
1129          * Whether the given IP is in a given DNS blacklist.
1130          *
1131          * @param $ip \string IP to check
1132          * @param $base \string URL of the DNS blacklist
1133          * @return \bool True if blacklisted.
1134          */
1135         function inDnsBlacklist( $ip, $base ) {
1136                 wfProfileIn( __METHOD__ );
1137
1138                 $found = false;
1139                 $host = '';
1140                 // FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
1141                 if( IP::isIPv4($ip) ) {
1142                         # Make hostname
1143                         $host = "$ip.$base";
1144
1145                         # Send query
1146                         $ipList = gethostbynamel( $host );
1147
1148                         if( $ipList ) {
1149                                 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
1150                                 $found = true;
1151                         } else {
1152                                 wfDebug( "Requested $host, not found in $base.\n" );
1153                         }
1154                 }
1155
1156                 wfProfileOut( __METHOD__ );
1157                 return $found;
1158         }
1159
1160         /**
1161          * Is this user subject to rate limiting?
1162          *
1163          * @return \bool True if rate limited
1164          */
1165         public function isPingLimitable() {
1166                 global $wgRateLimitsExcludedGroups;
1167                 global $wgRateLimitsExcludedIPs;
1168                 if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) {
1169                         // Deprecated, but kept for backwards-compatibility config
1170                         return false;
1171                 }
1172                 if( in_array( wfGetIP(), $wgRateLimitsExcludedIPs ) ) {
1173                         // No other good way currently to disable rate limits
1174                         // for specific IPs. :P
1175                         // But this is a crappy hack and should die.
1176                         return false;
1177                 }
1178                 return !$this->isAllowed('noratelimit');
1179         }
1180
1181         /**
1182          * Primitive rate limits: enforce maximum actions per time period
1183          * to put a brake on flooding.
1184          *
1185          * @note When using a shared cache like memcached, IP-address
1186          * last-hit counters will be shared across wikis.
1187          *
1188          * @param $action \string Action to enforce; 'edit' if unspecified
1189          * @return \bool True if a rate limiter was tripped
1190          */
1191         function pingLimiter( $action='edit' ) {
1192
1193                 # Call the 'PingLimiter' hook
1194                 $result = false;
1195                 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
1196                         return $result;
1197                 }
1198
1199                 global $wgRateLimits;
1200                 if( !isset( $wgRateLimits[$action] ) ) {
1201                         return false;
1202                 }
1203
1204                 # Some groups shouldn't trigger the ping limiter, ever
1205                 if( !$this->isPingLimitable() )
1206                         return false;
1207
1208                 global $wgMemc, $wgRateLimitLog;
1209                 wfProfileIn( __METHOD__ );
1210
1211                 $limits = $wgRateLimits[$action];
1212                 $keys = array();
1213                 $id = $this->getId();
1214                 $ip = wfGetIP();
1215                 $userLimit = false;
1216
1217                 if( isset( $limits['anon'] ) && $id == 0 ) {
1218                         $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1219                 }
1220
1221                 if( isset( $limits['user'] ) && $id != 0 ) {
1222                         $userLimit = $limits['user'];
1223                 }
1224                 if( $this->isNewbie() ) {
1225                         if( isset( $limits['newbie'] ) && $id != 0 ) {
1226                                 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1227                         }
1228                         if( isset( $limits['ip'] ) ) {
1229                                 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1230                         }
1231                         $matches = array();
1232                         if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1233                                 $subnet = $matches[1];
1234                                 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1235                         }
1236                 }
1237                 // Check for group-specific permissions
1238                 // If more than one group applies, use the group with the highest limit
1239                 foreach ( $this->getGroups() as $group ) {
1240                         if ( isset( $limits[$group] ) ) {
1241                                 if ( $userLimit === false || $limits[$group] > $userLimit ) {
1242                                         $userLimit = $limits[$group];
1243                                 }
1244                         }
1245                 }
1246                 // Set the user limit key
1247                 if ( $userLimit !== false ) {
1248                         wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
1249                         $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
1250                 }
1251
1252                 $triggered = false;
1253                 foreach( $keys as $key => $limit ) {
1254                         list( $max, $period ) = $limit;
1255                         $summary = "(limit $max in {$period}s)";
1256                         $count = $wgMemc->get( $key );
1257                         if( $count ) {
1258                                 if( $count > $max ) {
1259                                         wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1260                                         if( $wgRateLimitLog ) {
1261                                                 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1262                                         }
1263                                         $triggered = true;
1264                                 } else {
1265                                         wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1266                                 }
1267                         } else {
1268                                 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1269                                 $wgMemc->add( $key, 1, intval( $period ) );
1270                         }
1271                         $wgMemc->incr( $key );
1272                 }
1273
1274                 wfProfileOut( __METHOD__ );
1275                 return $triggered;
1276         }
1277
1278         /**
1279          * Check if user is blocked
1280          * 
1281          * @param $bFromSlave \bool Whether to check the slave database instead of the master
1282          * @return \bool True if blocked, false otherwise
1283          */
1284         function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1285                 wfDebug( "User::isBlocked: enter\n" );
1286                 $this->getBlockedStatus( $bFromSlave );
1287                 return $this->mBlockedby !== 0;
1288         }
1289
1290         /**
1291          * Check if user is blocked from editing a particular article
1292          * 
1293          * @param $title      \string Title to check
1294          * @param $bFromSlave \bool   Whether to check the slave database instead of the master
1295          * @return \bool True if blocked, false otherwise
1296          */
1297         function isBlockedFrom( $title, $bFromSlave = false ) {
1298                 global $wgBlockAllowsUTEdit;
1299                 wfProfileIn( __METHOD__ );
1300                 wfDebug( __METHOD__.": enter\n" );
1301
1302                 wfDebug( __METHOD__.": asking isBlocked()\n" );
1303                 $blocked = $this->isBlocked( $bFromSlave );
1304                 $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
1305                 # If a user's name is suppressed, they cannot make edits anywhere
1306                 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
1307                   $title->getNamespace() == NS_USER_TALK ) {
1308                         $blocked = false;
1309                         wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1310                 }
1311                 wfProfileOut( __METHOD__ );
1312                 return $blocked;
1313         }
1314
1315         /**
1316          * If user is blocked, return the name of the user who placed the block
1317          * @return \string name of blocker
1318          */
1319         function blockedBy() {
1320                 $this->getBlockedStatus();
1321                 return $this->mBlockedby;
1322         }
1323
1324         /**
1325          * If user is blocked, return the specified reason for the block
1326          * @return \string Blocking reason
1327          */
1328         function blockedFor() {
1329                 $this->getBlockedStatus();
1330                 return $this->mBlockreason;
1331         }
1332         
1333         /**
1334          * If user is blocked, return the ID for the block
1335          * @return \int Block ID
1336          */
1337         function getBlockId() {
1338                 $this->getBlockedStatus();
1339                 return ($this->mBlock ? $this->mBlock->mId : false);
1340         }
1341         
1342         /**
1343          * Check if user is blocked on all wikis.
1344          * Do not use for actual edit permission checks!
1345          * This is intented for quick UI checks.
1346          * 
1347          * @param $ip \type{\string} IP address, uses current client if none given
1348          * @return \type{\bool} True if blocked, false otherwise
1349          */
1350         function isBlockedGlobally( $ip = '' ) {
1351                 if( $this->mBlockedGlobally !== null ) {
1352                         return $this->mBlockedGlobally;
1353                 }
1354                 // User is already an IP?
1355                 if( IP::isIPAddress( $this->getName() ) ) {
1356                         $ip = $this->getName();
1357                 } else if( !$ip ) {
1358                         $ip = wfGetIP();
1359                 }
1360                 $blocked = false;
1361                 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
1362                 $this->mBlockedGlobally = (bool)$blocked;
1363                 return $this->mBlockedGlobally;
1364         }
1365         
1366         /**
1367          * Check if user account is locked
1368          * 
1369          * @return \type{\bool} True if locked, false otherwise
1370          */
1371         function isLocked() {
1372                 if( $this->mLocked !== null ) {
1373                         return $this->mLocked;
1374                 }
1375                 global $wgAuth;
1376                 $authUser = $wgAuth->getUserInstance( $this );
1377                 $this->mLocked = (bool)$authUser->isLocked();
1378                 return $this->mLocked;
1379         }
1380         
1381         /**
1382          * Check if user account is hidden
1383          * 
1384          * @return \type{\bool} True if hidden, false otherwise
1385          */
1386         function isHidden() {
1387                 if( $this->mHideName !== null ) {
1388                         return $this->mHideName;
1389                 }
1390                 $this->getBlockedStatus();
1391                 if( !$this->mHideName ) {
1392                         global $wgAuth;
1393                         $authUser = $wgAuth->getUserInstance( $this );
1394                         $this->mHideName = (bool)$authUser->isHidden();
1395                 }
1396                 return $this->mHideName;
1397         }
1398
1399         /**
1400          * Get the user's ID.
1401          * @return \int The user's ID; 0 if the user is anonymous or nonexistent
1402          */
1403         function getId() {
1404                 if( $this->mId === null and $this->mName !== null
1405                 and User::isIP( $this->mName ) ) {
1406                         // Special case, we know the user is anonymous
1407                         return 0;
1408                 } elseif( $this->mId === null ) {
1409                         // Don't load if this was initialized from an ID
1410                         $this->load();
1411                 }
1412                 return $this->mId;
1413         }
1414
1415         /**
1416          * Set the user and reload all fields according to a given ID
1417          * @param $v \int User ID to reload
1418          */
1419         function setId( $v ) {
1420                 $this->mId = $v;
1421                 $this->clearInstanceCache( 'id' );
1422         }
1423
1424         /**
1425          * Get the user name, or the IP of an anonymous user
1426          * @return \string User's name or IP address
1427          */
1428         function getName() {
1429                 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1430                         # Special case optimisation
1431                         return $this->mName;
1432                 } else {
1433                         $this->load();
1434                         if ( $this->mName === false ) {
1435                                 # Clean up IPs
1436                                 $this->mName = IP::sanitizeIP( wfGetIP() );
1437                         }
1438                         return $this->mName;
1439                 }
1440         }
1441
1442         /**
1443          * Set the user name.
1444          *
1445          * This does not reload fields from the database according to the given
1446          * name. Rather, it is used to create a temporary "nonexistent user" for
1447          * later addition to the database. It can also be used to set the IP
1448          * address for an anonymous user to something other than the current
1449          * remote IP.
1450          *
1451          * @note User::newFromName() has rougly the same function, when the named user
1452          * does not exist.
1453          * @param $str \string New user name to set
1454          */
1455         function setName( $str ) {
1456                 $this->load();
1457                 $this->mName = $str;
1458         }
1459
1460         /**
1461          * Get the user's name escaped by underscores.
1462          * @return \string Username escaped by underscores.
1463          */
1464         function getTitleKey() {
1465                 return str_replace( ' ', '_', $this->getName() );
1466         }
1467
1468         /**
1469          * Check if the user has new messages.
1470          * @return \bool True if the user has new messages
1471          */
1472         function getNewtalk() {
1473                 $this->load();
1474
1475                 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1476                 if( $this->mNewtalk === -1 ) {
1477                         $this->mNewtalk = false; # reset talk page status
1478
1479                         # Check memcached separately for anons, who have no
1480                         # entire User object stored in there.
1481                         if( !$this->mId ) {
1482                                 global $wgMemc;
1483                                 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1484                                 $newtalk = $wgMemc->get( $key );
1485                                 if( strval( $newtalk ) !== '' ) {
1486                                         $this->mNewtalk = (bool)$newtalk;
1487                                 } else {
1488                                         // Since we are caching this, make sure it is up to date by getting it
1489                                         // from the master
1490                                         $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
1491                                         $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
1492                                 }
1493                         } else {
1494                                 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1495                         }
1496                 }
1497
1498                 return (bool)$this->mNewtalk;
1499         }
1500
1501         /**
1502          * Return the talk page(s) this user has new messages on.
1503          * @return \type{\arrayof{\string}} Array of page URLs
1504          */
1505         function getNewMessageLinks() {
1506                 $talks = array();
1507                 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1508                         return $talks;
1509
1510                 if (!$this->getNewtalk())
1511                         return array();
1512                 $up = $this->getUserPage();
1513                 $utp = $up->getTalkPage();
1514                 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1515         }
1516
1517
1518         /**
1519          * Internal uncached check for new messages
1520          *
1521          * @see getNewtalk()
1522          * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1523          * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1524          * @param $fromMaster \bool true to fetch from the master, false for a slave
1525          * @return \bool True if the user has new messages
1526          * @private
1527          */
1528         function checkNewtalk( $field, $id, $fromMaster = false ) {
1529                 if ( $fromMaster ) {
1530                         $db = wfGetDB( DB_MASTER );
1531                 } else {
1532                         $db = wfGetDB( DB_SLAVE );
1533                 }
1534                 $ok = $db->selectField( 'user_newtalk', $field,
1535                         array( $field => $id ), __METHOD__ );
1536                 return $ok !== false;
1537         }
1538
1539         /**
1540          * Add or update the new messages flag
1541          * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1542          * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1543          * @return \bool True if successful, false otherwise
1544          * @private
1545          */
1546         function updateNewtalk( $field, $id ) {
1547                 $dbw = wfGetDB( DB_MASTER );
1548                 $dbw->insert( 'user_newtalk',
1549                         array( $field => $id ),
1550                         __METHOD__,
1551                         'IGNORE' );
1552                 if ( $dbw->affectedRows() ) {
1553                         wfDebug( __METHOD__.": set on ($field, $id)\n" );
1554                         return true;
1555                 } else {
1556                         wfDebug( __METHOD__." already set ($field, $id)\n" );
1557                         return false;
1558                 }
1559         }
1560
1561         /**
1562          * Clear the new messages flag for the given user
1563          * @param $field \string 'user_ip' for anonymous users, 'user_id' otherwise
1564          * @param $id \types{\string,\int} User's IP address for anonymous users, User ID otherwise
1565          * @return \bool True if successful, false otherwise
1566          * @private
1567          */
1568         function deleteNewtalk( $field, $id ) {
1569                 $dbw = wfGetDB( DB_MASTER );
1570                 $dbw->delete( 'user_newtalk',
1571                         array( $field => $id ),
1572                         __METHOD__ );
1573                 if ( $dbw->affectedRows() ) {
1574                         wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1575                         return true;
1576                 } else {
1577                         wfDebug( __METHOD__.": already gone ($field, $id)\n" );
1578                         return false;
1579                 }
1580         }
1581
1582         /**
1583          * Update the 'You have new messages!' status.
1584          * @param $val \bool Whether the user has new messages
1585          */
1586         function setNewtalk( $val ) {
1587                 if( wfReadOnly() ) {
1588                         return;
1589                 }
1590
1591                 $this->load();
1592                 $this->mNewtalk = $val;
1593
1594                 if( $this->isAnon() ) {
1595                         $field = 'user_ip';
1596                         $id = $this->getName();
1597                 } else {
1598                         $field = 'user_id';
1599                         $id = $this->getId();
1600                 }
1601                 global $wgMemc;
1602
1603                 if( $val ) {
1604                         $changed = $this->updateNewtalk( $field, $id );
1605                 } else {
1606                         $changed = $this->deleteNewtalk( $field, $id );
1607                 }
1608
1609                 if( $this->isAnon() ) {
1610                         // Anons have a separate memcached space, since
1611                         // user records aren't kept for them.
1612                         $key = wfMemcKey( 'newtalk', 'ip', $id );
1613                         $wgMemc->set( $key, $val ? 1 : 0, 1800 );
1614                 }
1615                 if ( $changed ) {
1616                         $this->invalidateCache();
1617                 }
1618         }
1619
1620         /**
1621          * Generate a current or new-future timestamp to be stored in the
1622          * user_touched field when we update things.
1623          * @return \string Timestamp in TS_MW format
1624          */
1625         private static function newTouchedTimestamp() {
1626                 global $wgClockSkewFudge;
1627                 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1628         }
1629
1630         /**
1631          * Clear user data from memcached.
1632          * Use after applying fun updates to the database; caller's
1633          * responsibility to update user_touched if appropriate.
1634          *
1635          * Called implicitly from invalidateCache() and saveSettings().
1636          */
1637         private function clearSharedCache() {
1638                 $this->load();
1639                 if( $this->mId ) {
1640                         global $wgMemc;
1641                         $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1642                 }
1643         }
1644
1645         /**
1646          * Immediately touch the user data cache for this account.
1647          * Updates user_touched field, and removes account data from memcached
1648          * for reload on the next hit.
1649          */
1650         function invalidateCache() {
1651                 $this->load();
1652                 if( $this->mId ) {
1653                         $this->mTouched = self::newTouchedTimestamp();
1654
1655                         $dbw = wfGetDB( DB_MASTER );
1656                         $dbw->update( 'user',
1657                                 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1658                                 array( 'user_id' => $this->mId ),
1659                                 __METHOD__ );
1660
1661                         $this->clearSharedCache();
1662                 }
1663         }
1664
1665         /**
1666          * Validate the cache for this account.
1667          * @param $timestamp \string A timestamp in TS_MW format
1668          */
1669         function validateCache( $timestamp ) {
1670                 $this->load();
1671                 return ($timestamp >= $this->mTouched);
1672         }
1673
1674         /**
1675          * Get the user touched timestamp
1676          */
1677         function getTouched() {
1678                 $this->load();
1679                 return $this->mTouched;
1680         }
1681
1682         /**
1683          * Set the password and reset the random token.
1684          * Calls through to authentication plugin if necessary;
1685          * will have no effect if the auth plugin refuses to
1686          * pass the change through or if the legal password
1687          * checks fail.
1688          *
1689          * As a special case, setting the password to null
1690          * wipes it, so the account cannot be logged in until
1691          * a new password is set, for instance via e-mail.
1692          *
1693          * @param $str \string New password to set
1694          * @throws PasswordError on failure
1695          */
1696         function setPassword( $str ) {
1697                 global $wgAuth;
1698
1699                 if( $str !== null ) {
1700                         if( !$wgAuth->allowPasswordChange() ) {
1701                                 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1702                         }
1703
1704                         if( !$this->isValidPassword( $str ) ) {
1705                                 global $wgMinimalPasswordLength;
1706                                 throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
1707                                         $wgMinimalPasswordLength ) );
1708                         }
1709                 }
1710
1711                 if( !$wgAuth->setPassword( $this, $str ) ) {
1712                         throw new PasswordError( wfMsg( 'externaldberror' ) );
1713                 }
1714
1715                 $this->setInternalPassword( $str );
1716
1717                 return true;
1718         }
1719
1720         /**
1721          * Set the password and reset the random token unconditionally.
1722          *
1723          * @param $str \string New password to set
1724          */
1725         function setInternalPassword( $str ) {
1726                 $this->load();
1727                 $this->setToken();
1728
1729                 if( $str === null ) {
1730                         // Save an invalid hash...
1731                         $this->mPassword = '';
1732                 } else {
1733                         $this->mPassword = self::crypt( $str );
1734                 }
1735                 $this->mNewpassword = '';
1736                 $this->mNewpassTime = null;
1737         }
1738         
1739         /**
1740          * Get the user's current token.
1741          * @return \string Token
1742          */
1743         function getToken() {
1744                 $this->load();
1745                 return $this->mToken;
1746         }
1747         
1748         /**
1749          * Set the random token (used for persistent authentication)
1750          * Called from loadDefaults() among other places.
1751          *
1752          * @param $token \string If specified, set the token to this value
1753          * @private
1754          */
1755         function setToken( $token = false ) {
1756                 global $wgSecretKey, $wgProxyKey;
1757                 $this->load();
1758                 if ( !$token ) {
1759                         if ( $wgSecretKey ) {
1760                                 $key = $wgSecretKey;
1761                         } elseif ( $wgProxyKey ) {
1762                                 $key = $wgProxyKey;
1763                         } else {
1764                                 $key = microtime();
1765                         }
1766                         $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1767                 } else {
1768                         $this->mToken = $token;
1769                 }
1770         }
1771         
1772         /**
1773          * Set the cookie password
1774          *
1775          * @param $str \string New cookie password
1776          * @private
1777          */
1778         function setCookiePassword( $str ) {
1779                 $this->load();
1780                 $this->mCookiePassword = md5( $str );
1781         }
1782
1783         /**
1784          * Set the password for a password reminder or new account email
1785          *
1786          * @param $str \string New password to set
1787          * @param $throttle \bool If true, reset the throttle timestamp to the present
1788          */
1789         function setNewpassword( $str, $throttle = true ) {
1790                 $this->load();
1791                 $this->mNewpassword = self::crypt( $str );
1792                 if ( $throttle ) {
1793                         $this->mNewpassTime = wfTimestampNow();
1794                 }
1795         }
1796
1797         /**
1798          * Has password reminder email been sent within the last 
1799          * $wgPasswordReminderResendTime hours?
1800          * @return \bool True or false
1801          */
1802         function isPasswordReminderThrottled() {
1803                 global $wgPasswordReminderResendTime;
1804                 $this->load();
1805                 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1806                         return false;
1807                 }
1808                 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1809                 return time() < $expiry;
1810         }
1811
1812         /**
1813          * Get the user's e-mail address
1814          * @return \string User's email address
1815          */
1816         function getEmail() {
1817                 $this->load();
1818                 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
1819                 return $this->mEmail;
1820         }
1821
1822         /**
1823          * Get the timestamp of the user's e-mail authentication
1824          * @return \string TS_MW timestamp
1825          */
1826         function getEmailAuthenticationTimestamp() {
1827                 $this->load();
1828                 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
1829                 return $this->mEmailAuthenticated;
1830         }
1831
1832         /**
1833          * Set the user's e-mail address
1834          * @param $str \string New e-mail address
1835          */
1836         function setEmail( $str ) {
1837                 $this->load();
1838                 $this->mEmail = $str;
1839                 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
1840         }
1841
1842         /**
1843          * Get the user's real name
1844          * @return \string User's real name
1845          */
1846         function getRealName() {
1847                 $this->load();
1848                 return $this->mRealName;
1849         }
1850
1851         /**
1852          * Set the user's real name
1853          * @param $str \string New real name
1854          */
1855         function setRealName( $str ) {
1856                 $this->load();
1857                 $this->mRealName = $str;
1858         }
1859
1860         /**
1861          * Get the user's current setting for a given option.
1862          *
1863          * @param $oname \string The option to check
1864          * @param $defaultOverride \string A default value returned if the option does not exist
1865          * @return \string User's current value for the option
1866          * @see getBoolOption()
1867          * @see getIntOption()
1868          */
1869         function getOption( $oname, $defaultOverride = '' ) {
1870                 $this->load();
1871
1872                 if ( is_null( $this->mOptions ) ) {
1873                         if($defaultOverride != '') {
1874                                 return $defaultOverride;
1875                         }
1876                         $this->mOptions = User::getDefaultOptions();
1877                 }
1878
1879                 if ( array_key_exists( $oname, $this->mOptions ) ) {
1880                         return trim( $this->mOptions[$oname] );
1881                 } else {
1882                         return $defaultOverride;
1883                 }
1884         }
1885         
1886         /**
1887          * Get the user's current setting for a given option, as a boolean value.
1888          *
1889          * @param $oname \string The option to check
1890          * @return \bool User's current value for the option
1891          * @see getOption()
1892          */
1893         function getBoolOption( $oname ) {
1894                 return (bool)$this->getOption( $oname );
1895         }
1896
1897         
1898         /**
1899          * Get the user's current setting for a given option, as a boolean value.
1900          *
1901          * @param $oname \string The option to check
1902          * @param $defaultOverride \int A default value returned if the option does not exist
1903          * @return \int User's current value for the option
1904          * @see getOption()
1905          */
1906         function getIntOption( $oname, $defaultOverride=0 ) {
1907                 $val = $this->getOption( $oname );
1908                 if( $val == '' ) {
1909                         $val = $defaultOverride;
1910                 }
1911                 return intval( $val );
1912         }
1913
1914         /**
1915          * Set the given option for a user.
1916          *
1917          * @param $oname \string The option to set
1918          * @param $val \mixed New value to set
1919          */
1920         function setOption( $oname, $val ) {
1921                 $this->load();
1922                 if ( is_null( $this->mOptions ) ) {
1923                         $this->mOptions = User::getDefaultOptions();
1924                 }
1925                 if ( $oname == 'skin' ) {
1926                         # Clear cached skin, so the new one displays immediately in Special:Preferences
1927                         unset( $this->mSkin );
1928                 }
1929                 // Filter out any newlines that may have passed through input validation.
1930                 // Newlines are used to separate items in the options blob.
1931                 if( $val ) {
1932                         $val = str_replace( "\r\n", "\n", $val );
1933                         $val = str_replace( "\r", "\n", $val );
1934                         $val = str_replace( "\n", " ", $val );
1935                 }
1936                 // Explicitly NULL values should refer to defaults
1937                 global $wgDefaultUserOptions;
1938                 if( is_null($val) && isset($wgDefaultUserOptions[$oname]) ) {
1939                         $val = $wgDefaultUserOptions[$oname];
1940                 }
1941                 $this->mOptions[$oname] = $val;
1942         }
1943         
1944         /**
1945          * Reset all options to the site defaults
1946          */     
1947         function restoreOptions() {
1948                 $this->mOptions = User::getDefaultOptions();
1949         }
1950
1951         /**
1952          * Get the user's preferred date format.
1953          * @return \string User's preferred date format
1954          */
1955         function getDatePreference() {
1956                 // Important migration for old data rows
1957                 if ( is_null( $this->mDatePreference ) ) {
1958                         global $wgLang;
1959                         $value = $this->getOption( 'date' );
1960                         $map = $wgLang->getDatePreferenceMigrationMap();
1961                         if ( isset( $map[$value] ) ) {
1962                                 $value = $map[$value];
1963                         }
1964                         $this->mDatePreference = $value;
1965                 }
1966                 return $this->mDatePreference;
1967         }
1968
1969         /**
1970          * Get the permissions this user has.
1971          * @return \type{\arrayof{\string}} Array of permission names
1972          */
1973         function getRights() {
1974                 if ( is_null( $this->mRights ) ) {
1975                         $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1976                         wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
1977                         // Force reindexation of rights when a hook has unset one of them
1978                         $this->mRights = array_values( $this->mRights );
1979                 }
1980                 return $this->mRights;
1981         }
1982
1983         /**
1984          * Get the list of explicit group memberships this user has.
1985          * The implicit * and user groups are not included.
1986          * @return \type{\arrayof{\string}} Array of internal group names
1987          */
1988         function getGroups() {
1989                 $this->load();
1990                 return $this->mGroups;
1991         }
1992
1993         /**
1994          * Get the list of implicit group memberships this user has.
1995          * This includes all explicit groups, plus 'user' if logged in,
1996          * '*' for all accounts and autopromoted groups
1997          * @param $recache \bool Whether to avoid the cache
1998          * @return \type{\arrayof{\string}} Array of internal group names
1999          */
2000         function getEffectiveGroups( $recache = false ) {
2001                 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
2002                         $this->mEffectiveGroups = $this->getGroups();
2003                         $this->mEffectiveGroups[] = '*';
2004                         if( $this->getId() ) {
2005                                 $this->mEffectiveGroups[] = 'user';
2006
2007                                 $this->mEffectiveGroups = array_unique( array_merge(
2008                                         $this->mEffectiveGroups,
2009                                         Autopromote::getAutopromoteGroups( $this )
2010                                 ) );
2011
2012                                 # Hook for additional groups
2013                                 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
2014                         }
2015                 }
2016                 return $this->mEffectiveGroups;
2017         }
2018
2019         /**
2020          * Get the user's edit count.
2021          * @return \int User'e edit count
2022          */
2023         function getEditCount() {
2024                 if ($this->getId()) {
2025                         if ( !isset( $this->mEditCount ) ) {
2026                                 /* Populate the count, if it has not been populated yet */
2027                                 $this->mEditCount = User::edits($this->mId);
2028                         }
2029                         return $this->mEditCount;
2030                 } else {
2031                         /* nil */
2032                         return null;
2033                 }
2034         }
2035
2036         /**
2037          * Add the user to the given group.
2038          * This takes immediate effect.
2039          * @param $group \string Name of the group to add
2040          */
2041         function addGroup( $group ) {
2042                 $dbw = wfGetDB( DB_MASTER );
2043                 if( $this->getId() ) {
2044                         $dbw->insert( 'user_groups',
2045                                 array(
2046                                         'ug_user'  => $this->getID(),
2047                                         'ug_group' => $group,
2048                                 ),
2049                                 'User::addGroup',
2050                                 array( 'IGNORE' ) );
2051                 }
2052
2053                 $this->loadGroups();
2054                 $this->mGroups[] = $group;
2055                 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2056
2057                 $this->invalidateCache();
2058         }
2059
2060         /**
2061          * Remove the user from the given group.
2062          * This takes immediate effect.
2063          * @param $group \string Name of the group to remove
2064          */
2065         function removeGroup( $group ) {
2066                 $this->load();
2067                 $dbw = wfGetDB( DB_MASTER );
2068                 $dbw->delete( 'user_groups',
2069                         array(
2070                                 'ug_user'  => $this->getID(),
2071                                 'ug_group' => $group,
2072                         ),
2073                         'User::removeGroup' );
2074
2075                 $this->loadGroups();
2076                 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
2077                 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
2078
2079                 $this->invalidateCache();
2080         }
2081
2082
2083         /**
2084          * Get whether the user is logged in
2085          * @return \bool True or false
2086          */
2087         function isLoggedIn() {
2088                 return $this->getID() != 0;
2089         }
2090
2091         /**
2092          * Get whether the user is anonymous
2093          * @return \bool True or false
2094          */
2095         function isAnon() {
2096                 return !$this->isLoggedIn();
2097         }
2098
2099         /**
2100          * Get whether the user is a bot
2101          * @return \bool True or false
2102          * @deprecated
2103          */
2104         function isBot() {
2105                 wfDeprecated( __METHOD__ );
2106                 return $this->isAllowed( 'bot' );
2107         }
2108
2109         /**
2110          * Check if user is allowed to access a feature / make an action
2111          * @param $action \string action to be checked
2112          * @return \bool True if action is allowed, else false
2113          */
2114         function isAllowed( $action = '' ) {
2115                 if ( $action === '' )
2116                         return true; // In the spirit of DWIM
2117                 # Patrolling may not be enabled
2118                 if( $action === 'patrol' || $action === 'autopatrol' ) {
2119                         global $wgUseRCPatrol, $wgUseNPPatrol;
2120                         if( !$wgUseRCPatrol && !$wgUseNPPatrol )
2121                                 return false;
2122                 }
2123                 # Use strict parameter to avoid matching numeric 0 accidentally inserted 
2124                 # by misconfiguration: 0 == 'foo'
2125                 return in_array( $action, $this->getRights(), true );
2126         }
2127
2128         /**
2129         * Check whether to enable recent changes patrol features for this user
2130         * @return \bool True or false
2131         */
2132         public function useRCPatrol() {
2133                 global $wgUseRCPatrol;
2134                 return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2135         }
2136
2137         /**
2138         * Check whether to enable new pages patrol features for this user
2139         * @return \bool True or false
2140         */
2141         public function useNPPatrol() {
2142                 global $wgUseRCPatrol, $wgUseNPPatrol;
2143                 return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
2144         }
2145
2146         /**
2147          * Get the current skin, loading it if required
2148          * @return \type{Skin} Current skin
2149          * @todo FIXME : need to check the old failback system [AV]
2150          */
2151         function &getSkin() {
2152                 global $wgRequest, $wgAllowUserSkin, $wgDefaultSkin;
2153                 if ( ! isset( $this->mSkin ) ) {
2154                         wfProfileIn( __METHOD__ );
2155
2156                         if( $wgAllowUserSkin ) {
2157                                 # get the user skin
2158                                 $userSkin = $this->getOption( 'skin' );
2159                                 $userSkin = $wgRequest->getVal('useskin', $userSkin);
2160                         } else {
2161                                 # if we're not allowing users to override, then use the default
2162                                 $userSkin = $wgDefaultSkin;
2163                         }
2164                         
2165                         $this->mSkin =& Skin::newFromKey( $userSkin );
2166                         wfProfileOut( __METHOD__ );
2167                 }
2168                 return $this->mSkin;
2169         }
2170
2171         /**
2172          * Check the watched status of an article.
2173          * @param $title \type{Title} Title of the article to look at
2174          * @return \bool True if article is watched
2175          */
2176         function isWatched( $title ) {
2177                 $wl = WatchedItem::fromUserTitle( $this, $title );
2178                 return $wl->isWatched();
2179         }
2180
2181         /**
2182          * Watch an article.
2183          * @param $title \type{Title} Title of the article to look at
2184          */
2185         function addWatch( $title ) {
2186                 $wl = WatchedItem::fromUserTitle( $this, $title );
2187                 $wl->addWatch();
2188                 $this->invalidateCache();
2189         }
2190
2191         /**
2192          * Stop watching an article.
2193          * @param $title \type{Title} Title of the article to look at
2194          */
2195         function removeWatch( $title ) {
2196                 $wl = WatchedItem::fromUserTitle( $this, $title );
2197                 $wl->removeWatch();
2198                 $this->invalidateCache();
2199         }
2200
2201         /**
2202          * Clear the user's notification timestamp for the given title.
2203          * If e-notif e-mails are on, they will receive notification mails on
2204          * the next change of the page if it's watched etc.
2205          * @param $title \type{Title} Title of the article to look at
2206          */
2207         function clearNotification( &$title ) {
2208                 global $wgUser, $wgUseEnotif, $wgShowUpdatedMarker;
2209
2210                 # Do nothing if the database is locked to writes
2211                 if( wfReadOnly() ) {
2212                         return;
2213                 }
2214
2215                 if ($title->getNamespace() == NS_USER_TALK &&
2216                         $title->getText() == $this->getName() ) {
2217                         if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
2218                                 return;
2219                         $this->setNewtalk( false );
2220                 }
2221
2222                 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2223                         return;
2224                 }
2225
2226                 if( $this->isAnon() ) {
2227                         // Nothing else to do...
2228                         return;
2229                 }
2230
2231                 // Only update the timestamp if the page is being watched.
2232                 // The query to find out if it is watched is cached both in memcached and per-invocation,
2233                 // and when it does have to be executed, it can be on a slave
2234                 // If this is the user's newtalk page, we always update the timestamp
2235                 if ($title->getNamespace() == NS_USER_TALK &&
2236                         $title->getText() == $wgUser->getName())
2237                 {
2238                         $watched = true;
2239                 } elseif ( $this->getId() == $wgUser->getId() ) {
2240                         $watched = $title->userIsWatching();
2241                 } else {
2242                         $watched = true;
2243                 }
2244
2245                 // If the page is watched by the user (or may be watched), update the timestamp on any
2246                 // any matching rows
2247                 if ( $watched ) {
2248                         $dbw = wfGetDB( DB_MASTER );
2249                         $dbw->update( 'watchlist',
2250                                         array( /* SET */
2251                                                 'wl_notificationtimestamp' => NULL
2252                                         ), array( /* WHERE */
2253                                                 'wl_title' => $title->getDBkey(),
2254                                                 'wl_namespace' => $title->getNamespace(),
2255                                                 'wl_user' => $this->getID()
2256                                         ), __METHOD__
2257                         );
2258                 }
2259         }
2260
2261         /**
2262          * Resets all of the given user's page-change notification timestamps.
2263          * If e-notif e-mails are on, they will receive notification mails on
2264          * the next change of any watched page.
2265          *
2266          * @param $currentUser \int User ID
2267          */
2268         function clearAllNotifications( $currentUser ) {
2269                 global $wgUseEnotif, $wgShowUpdatedMarker;
2270                 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
2271                         $this->setNewtalk( false );
2272                         return;
2273                 }
2274                 if( $currentUser != 0 )  {
2275                         $dbw = wfGetDB( DB_MASTER );
2276                         $dbw->update( 'watchlist',
2277                                 array( /* SET */
2278                                         'wl_notificationtimestamp' => NULL
2279                                 ), array( /* WHERE */
2280                                         'wl_user' => $currentUser
2281                                 ), __METHOD__
2282                         );
2283                 #       We also need to clear here the "you have new message" notification for the own user_talk page
2284                 #       This is cleared one page view later in Article::viewUpdates();
2285                 }
2286         }
2287
2288         /**
2289          * Encode this user's options as a string
2290          * @return \string Encoded options
2291          * @private
2292          */
2293         function encodeOptions() {
2294                 $this->load();
2295                 if ( is_null( $this->mOptions ) ) {
2296                         $this->mOptions = User::getDefaultOptions();
2297                 }
2298                 $a = array();
2299                 foreach ( $this->mOptions as $oname => $oval ) {
2300                         array_push( $a, $oname.'='.$oval );
2301                 }
2302                 $s = implode( "\n", $a );
2303                 return $s;
2304         }
2305
2306         /**
2307          * Set this user's options from an encoded string
2308          * @param $str \string Encoded options to import
2309          * @private
2310          */
2311         function decodeOptions( $str ) {
2312                 $this->mOptions = array();
2313                 $a = explode( "\n", $str );
2314                 foreach ( $a as $s ) {
2315                         $m = array();
2316                         if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
2317                                 $this->mOptions[$m[1]] = $m[2];
2318                         }
2319                 }
2320         }
2321         
2322         /**
2323          * Set a cookie on the user's client. Wrapper for 
2324          * WebResponse::setCookie
2325          * @param $name \string Name of the cookie to set
2326          * @param $value \string Value to set
2327          * @param $exp \int Expiration time, as a UNIX time value; 
2328          *                   if 0 or not specified, use the default $wgCookieExpiration
2329          */
2330         protected function setCookie( $name, $value, $exp=0 ) {
2331                 global $wgRequest;
2332                 $wgRequest->response()->setcookie( $name, $value, $exp );
2333         }
2334         
2335         /**
2336          * Clear a cookie on the user's client
2337          * @param $name \string Name of the cookie to clear
2338          */
2339         protected function clearCookie( $name ) {
2340                 $this->setCookie( $name, '', time() - 86400 );
2341         }
2342
2343         /**
2344          * Set the default cookies for this session on the user's client.
2345          */
2346         function setCookies() {
2347                 $this->load();
2348                 if ( 0 == $this->mId ) return;
2349                 $session = array( 
2350                         'wsUserID' => $this->mId,
2351                         'wsToken' => $this->mToken,
2352                         'wsUserName' => $this->getName()
2353                 );
2354                 $cookies = array(
2355                         'UserID' => $this->mId,
2356                         'UserName' => $this->getName(),
2357                 );
2358                 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
2359                         $cookies['Token'] = $this->mToken;
2360                 } else {
2361                         $cookies['Token'] = false;
2362                 }
2363                 
2364                 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
2365                 #check for null, since the hook could cause a null value 
2366                 if ( !is_null( $session ) && isset( $_SESSION ) ){
2367                         $_SESSION = $session + $_SESSION;
2368                 }
2369                 foreach ( $cookies as $name => $value ) {
2370                         if ( $value === false ) {
2371                                 $this->clearCookie( $name );
2372                         } else {
2373                                 $this->setCookie( $name, $value );
2374                         }
2375                 }
2376         }
2377
2378         /**
2379          * Log this user out.
2380          */
2381         function logout() {
2382                 global $wgUser;
2383                 if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
2384                         $this->doLogout();
2385                 }
2386         }
2387
2388         /**
2389          * Clear the user's cookies and session, and reset the instance cache.
2390          * @private
2391          * @see logout()
2392          */
2393         function doLogout() {
2394                 $this->clearInstanceCache( 'defaults' );
2395
2396                 $_SESSION['wsUserID'] = 0;
2397
2398                 $this->clearCookie( 'UserID' );
2399                 $this->clearCookie( 'Token' );
2400
2401                 # Remember when user logged out, to prevent seeing cached pages
2402                 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
2403         }
2404
2405         /**
2406          * Save this user's settings into the database.
2407          * @todo Only rarely do all these fields need to be set!
2408          */
2409         function saveSettings() {
2410                 $this->load();
2411                 if ( wfReadOnly() ) { return; }
2412                 if ( 0 == $this->mId ) { return; }
2413
2414                 $this->mTouched = self::newTouchedTimestamp();
2415
2416                 $dbw = wfGetDB( DB_MASTER );
2417                 $dbw->update( 'user',
2418                         array( /* SET */
2419                                 'user_name' => $this->mName,
2420                                 'user_password' => $this->mPassword,
2421                                 'user_newpassword' => $this->mNewpassword,
2422                                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
2423                                 'user_real_name' => $this->mRealName,
2424                                 'user_email' => $this->mEmail,
2425                                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2426                                 'user_options' => $this->encodeOptions(),
2427                                 'user_touched' => $dbw->timestamp($this->mTouched),
2428                                 'user_token' => $this->mToken,
2429                                 'user_email_token' => $this->mEmailToken,
2430                                 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
2431                         ), array( /* WHERE */
2432                                 'user_id' => $this->mId
2433                         ), __METHOD__
2434                 );
2435                 wfRunHooks( 'UserSaveSettings', array( $this ) );
2436                 $this->clearSharedCache();
2437                 $this->getUserPage()->invalidateCache();
2438         }
2439
2440         /**
2441          * If only this user's username is known, and it exists, return the user ID.
2442          */
2443         function idForName() {
2444                 $s = trim( $this->getName() );
2445                 if ( $s === '' ) return 0;
2446
2447                 $dbr = wfGetDB( DB_SLAVE );
2448                 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
2449                 if ( $id === false ) {
2450                         $id = 0;
2451                 }
2452                 return $id;
2453         }
2454
2455         /**
2456          * Add a user to the database, return the user object
2457          *
2458          * @param $name \string Username to add
2459          * @param $params \type{\arrayof{\string}} Non-default parameters to save to the database:
2460          *   - password             The user's password. Password logins will be disabled if this is omitted.
2461          *   - newpassword          A temporary password mailed to the user
2462          *   - email                The user's email address
2463          *   - email_authenticated  The email authentication timestamp
2464          *   - real_name            The user's real name
2465          *   - options              An associative array of non-default options
2466          *   - token                Random authentication token. Do not set.
2467          *   - registration         Registration timestamp. Do not set.
2468          *
2469          * @return \type{User} A new User object, or null if the username already exists
2470          */
2471         static function createNew( $name, $params = array() ) {
2472                 $user = new User;
2473                 $user->load();
2474                 if ( isset( $params['options'] ) ) {
2475                         $user->mOptions = $params['options'] + $user->mOptions;
2476                         unset( $params['options'] );
2477                 }
2478                 $dbw = wfGetDB( DB_MASTER );
2479                 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2480                 $fields = array(
2481                         'user_id' => $seqVal,
2482                         'user_name' => $name,
2483                         'user_password' => $user->mPassword,
2484                         'user_newpassword' => $user->mNewpassword,
2485                         'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2486                         'user_email' => $user->mEmail,
2487                         'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2488                         'user_real_name' => $user->mRealName,
2489                         'user_options' => $user->encodeOptions(),
2490                         'user_token' => $user->mToken,
2491                         'user_registration' => $dbw->timestamp( $user->mRegistration ),
2492                         'user_editcount' => 0,
2493                 );
2494                 foreach ( $params as $name => $value ) {
2495                         $fields["user_$name"] = $value;
2496                 }
2497                 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2498                 if ( $dbw->affectedRows() ) {
2499                         $newUser = User::newFromId( $dbw->insertId() );
2500                 } else {
2501                         $newUser = null;
2502                 }
2503                 return $newUser;
2504         }
2505
2506         /**
2507          * Add this existing user object to the database
2508          */
2509         function addToDatabase() {
2510                 $this->load();
2511                 $dbw = wfGetDB( DB_MASTER );
2512                 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2513                 $dbw->insert( 'user',
2514                         array(
2515                                 'user_id' => $seqVal,
2516                                 'user_name' => $this->mName,
2517                                 'user_password' => $this->mPassword,
2518                                 'user_newpassword' => $this->mNewpassword,
2519                                 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2520                                 'user_email' => $this->mEmail,
2521                                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2522                                 'user_real_name' => $this->mRealName,
2523                                 'user_options' => $this->encodeOptions(),
2524                                 'user_token' => $this->mToken,
2525                                 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2526                                 'user_editcount' => 0,
2527                         ), __METHOD__
2528                 );
2529                 $this->mId = $dbw->insertId();
2530
2531                 // Clear instance cache other than user table data, which is already accurate
2532                 $this->clearInstanceCache();
2533         }
2534
2535         /**
2536          * If this (non-anonymous) user is blocked, block any IP address
2537          * they've successfully logged in from.
2538          */
2539         function spreadBlock() {
2540                 wfDebug( __METHOD__."()\n" );
2541                 $this->load();
2542                 if ( $this->mId == 0 ) {
2543                         return;
2544                 }
2545
2546                 $userblock = Block::newFromDB( '', $this->mId );
2547                 if ( !$userblock ) {
2548                         return;
2549                 }
2550
2551                 $userblock->doAutoblock( wfGetIp() );
2552
2553         }
2554
2555         /**
2556          * Generate a string which will be different for any combination of
2557          * user options which would produce different parser output.
2558          * This will be used as part of the hash key for the parser cache,
2559          * so users will the same options can share the same cached data
2560          * safely.
2561          *
2562          * Extensions which require it should install 'PageRenderingHash' hook,
2563          * which will give them a chance to modify this key based on their own
2564          * settings.
2565          *
2566          * @return \string Page rendering hash
2567          */
2568         function getPageRenderingHash() {
2569                 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
2570                 if( $this->mHash ){
2571                         return $this->mHash;
2572                 }
2573
2574                 // stubthreshold is only included below for completeness,
2575                 // it will always be 0 when this function is called by parsercache.
2576
2577                 $confstr =        $this->getOption( 'math' );
2578                 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2579                 if ( $wgUseDynamicDates ) {
2580                         $confstr .= '!' . $this->getDatePreference();
2581                 }
2582                 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2583                 $confstr .= '!' . $wgLang->getCode();
2584                 $confstr .= '!' . $this->getOption( 'thumbsize' );
2585                 // add in language specific options, if any
2586                 $extra = $wgContLang->getExtraHashOptions();
2587                 $confstr .= $extra;
2588
2589                 $confstr .= $wgRenderHashAppend;
2590
2591                 // Give a chance for extensions to modify the hash, if they have
2592                 // extra options or other effects on the parser cache.
2593                 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2594
2595                 // Make it a valid memcached key fragment
2596                 $confstr = str_replace( ' ', '_', $confstr );
2597                 $this->mHash = $confstr;
2598                 return $confstr;
2599         }
2600
2601         /**
2602          * Get whether the user is explicitly blocked from account creation.
2603          * @return \bool True if blocked
2604          */
2605         function isBlockedFromCreateAccount() {
2606                 $this->getBlockedStatus();
2607                 return $this->mBlock && $this->mBlock->mCreateAccount;
2608         }
2609
2610         /**
2611          * Get whether the user is blocked from using Special:Emailuser.
2612          * @return \bool True if blocked
2613          */
2614         function isBlockedFromEmailuser() {
2615                 $this->getBlockedStatus();
2616                 return $this->mBlock && $this->mBlock->mBlockEmail;
2617         }
2618
2619         /**
2620          * Get whether the user is allowed to create an account.
2621          * @return \bool True if allowed
2622          */
2623         function isAllowedToCreateAccount() {
2624                 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2625         }
2626
2627         /**
2628          * @deprecated
2629          */
2630         function setLoaded( $loaded ) {
2631                 wfDeprecated( __METHOD__ );
2632         }
2633
2634         /**
2635          * Get this user's personal page title.
2636          *
2637          * @return \type{Title} User's personal page title
2638          */
2639         function getUserPage() {
2640                 return Title::makeTitle( NS_USER, $this->getName() );
2641         }
2642
2643         /**
2644          * Get this user's talk page title.
2645          *
2646          * @return \type{Title} User's talk page title
2647          */
2648         function getTalkPage() {
2649                 $title = $this->getUserPage();
2650                 return $title->getTalkPage();
2651         }
2652
2653         /**
2654          * Get the maximum valid user ID.
2655          * @return \int User ID
2656          * @static
2657          */
2658         function getMaxID() {
2659                 static $res; // cache
2660
2661                 if ( isset( $res ) )
2662                         return $res;
2663                 else {
2664                         $dbr = wfGetDB( DB_SLAVE );
2665                         return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2666                 }
2667         }
2668
2669         /**
2670          * Determine whether the user is a newbie. Newbies are either
2671          * anonymous IPs, or the most recently created accounts.
2672          * @return \bool True if the user is a newbie
2673          */
2674         function isNewbie() {
2675                 return !$this->isAllowed( 'autoconfirmed' );
2676         }
2677         
2678         /**
2679          * Is the user active? We check to see if they've made at least
2680          * X number of edits in the last Y days.
2681          * 
2682          * @return \bool True if the user is active, false if not.
2683          */
2684         public function isActiveEditor() {
2685                 global $wgActiveUserEditCount, $wgActiveUserDays;
2686                 $dbr = wfGetDB( DB_SLAVE );
2687                 
2688                 // Stolen without shame from RC
2689                 $cutoff_unixtime = time() - ( $wgActiveUserDays * 86400 );
2690                 $cutoff_unixtime = $cutoff_unixtime - ( $cutoff_unixtime % 86400 );
2691                 $oldTime = $dbr->addQuotes( $dbr->timestamp( $cutoff_unixtime ) );
2692                 
2693                 $res = $dbr->select( 'revision', '1',
2694                                 array( 'rev_user_text' => $this->getName(), "rev_timestamp > $oldTime"),
2695                                 __METHOD__,
2696                                 array('LIMIT' => $wgActiveUserEditCount ) );
2697                 
2698                 $count = $dbr->numRows($res);
2699                 $dbr->freeResult($res);
2700
2701                 return $count == $wgActiveUserEditCount;
2702         }
2703
2704         /**
2705          * Check to see if the given clear-text password is one of the accepted passwords
2706          * @param $password \string user password.
2707          * @return \bool True if the given password is correct, otherwise False.
2708          */
2709         function checkPassword( $password ) {
2710                 global $wgAuth;
2711                 $this->load();
2712
2713                 // Even though we stop people from creating passwords that
2714                 // are shorter than this, doesn't mean people wont be able
2715                 // to. Certain authentication plugins do NOT want to save
2716                 // domain passwords in a mysql database, so we should
2717                 // check this (incase $wgAuth->strict() is false).
2718                 if( !$this->isValidPassword( $password ) ) {
2719                         return false;
2720                 }
2721
2722                 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2723                         return true;
2724                 } elseif( $wgAuth->strict() ) {
2725                         /* Auth plugin doesn't allow local authentication */
2726                         return false;
2727                 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
2728                         /* Auth plugin doesn't allow local authentication for this user name */
2729                         return false;
2730                 }
2731                 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
2732                         return true;
2733                 } elseif ( function_exists( 'iconv' ) ) {
2734                         # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2735                         # Check for this with iconv
2736                         $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
2737                         if ( self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) {
2738                                 return true;
2739                         }
2740                 }
2741                 return false;
2742         }
2743
2744         /**
2745          * Check if the given clear-text password matches the temporary password
2746          * sent by e-mail for password reset operations.
2747          * @return \bool True if matches, false otherwise
2748          */
2749         function checkTemporaryPassword( $plaintext ) {
2750                 global $wgNewPasswordExpiry;
2751                 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
2752                         $this->load();
2753                         $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
2754                         return ( time() < $expiry );
2755                 } else {
2756                         return false;
2757                 }
2758         }
2759
2760         /**
2761          * Initialize (if necessary) and return a session token value
2762          * which can be used in edit forms to show that the user's
2763          * login credentials aren't being hijacked with a foreign form
2764          * submission.
2765          *
2766          * @param $salt \types{\string,\arrayof{\string}} Optional function-specific data for hashing
2767          * @return \string The new edit token
2768          */
2769         function editToken( $salt = '' ) {
2770                 if ( $this->isAnon() ) {
2771                         return EDIT_TOKEN_SUFFIX;
2772                 } else {
2773                         if( !isset( $_SESSION['wsEditToken'] ) ) {
2774                                 $token = self::generateToken();
2775                                 $_SESSION['wsEditToken'] = $token;
2776                         } else {
2777                                 $token = $_SESSION['wsEditToken'];
2778                         }
2779                         if( is_array( $salt ) ) {
2780                                 $salt = implode( '|', $salt );
2781                         }
2782                         return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2783                 }
2784         }
2785
2786         /**
2787          * Generate a looking random token for various uses.
2788          *
2789          * @param $salt \string Optional salt value
2790          * @return \string The new random token
2791          */
2792         public static function generateToken( $salt = '' ) {
2793                 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2794                 return md5( $token . $salt );
2795         }
2796
2797         /**
2798          * Check given value against the token value stored in the session.
2799          * A match should confirm that the form was submitted from the
2800          * user's own login session, not a form submission from a third-party
2801          * site.
2802          *
2803          * @param $val \string Input value to compare
2804          * @param $salt \string Optional function-specific data for hashing
2805          * @return \bool Whether the token matches
2806          */
2807         function matchEditToken( $val, $salt = '' ) {
2808                 $sessionToken = $this->editToken( $salt );
2809                 if ( $val != $sessionToken ) {
2810                         wfDebug( "User::matchEditToken: broken session data\n" );
2811                 }
2812                 return $val == $sessionToken;
2813         }
2814
2815         /**
2816          * Check given value against the token value stored in the session,
2817          * ignoring the suffix.
2818          *
2819          * @param $val \string Input value to compare
2820          * @param $salt \string Optional function-specific data for hashing
2821          * @return \bool Whether the token matches
2822          */
2823         function matchEditTokenNoSuffix( $val, $salt = '' ) {
2824                 $sessionToken = $this->editToken( $salt );
2825                 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
2826         }
2827
2828         /**
2829          * Generate a new e-mail confirmation token and send a confirmation/invalidation
2830          * mail to the user's given address.
2831          *
2832          * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure.
2833          */
2834         function sendConfirmationMail() {
2835                 global $wgLang;
2836                 $expiration = null; // gets passed-by-ref and defined in next line.
2837                 $token = $this->confirmationToken( $expiration );
2838                 $url = $this->confirmationTokenUrl( $token );
2839                 $invalidateURL = $this->invalidationTokenUrl( $token );
2840                 $this->saveSettings();
2841                 
2842                 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2843                         wfMsg( 'confirmemail_body',
2844                                 wfGetIP(),
2845                                 $this->getName(),
2846                                 $url,
2847                                 $wgLang->timeanddate( $expiration, false ),
2848                                 $invalidateURL ) );
2849         }
2850
2851         /**
2852          * Send an e-mail to this user's account. Does not check for
2853          * confirmed status or validity.
2854          *
2855          * @param $subject \string Message subject
2856          * @param $body \string Message body
2857          * @param $from \string Optional From address; if unspecified, default $wgPasswordSender will be used
2858          * @param $replyto \string Reply-To address
2859          * @return \types{\bool,\type{WikiError}} True on success, a WikiError object on failure
2860          */
2861         function sendMail( $subject, $body, $from = null, $replyto = null ) {
2862                 if( is_null( $from ) ) {
2863                         global $wgPasswordSender;
2864                         $from = $wgPasswordSender;
2865                 }
2866
2867                 $to = new MailAddress( $this );
2868                 $sender = new MailAddress( $from );
2869                 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
2870         }
2871
2872         /**
2873          * Generate, store, and return a new e-mail confirmation code.
2874          * A hash (unsalted, since it's used as a key) is stored.
2875          *
2876          * @note Call saveSettings() after calling this function to commit
2877          * this change to the database.
2878          *
2879          * @param[out] &$expiration \mixed Accepts the expiration time
2880          * @return \string New token
2881          * @private
2882          */
2883         function confirmationToken( &$expiration ) {
2884                 $now = time();
2885                 $expires = $now + 7 * 24 * 60 * 60;
2886                 $expiration = wfTimestamp( TS_MW, $expires );
2887                 $token = self::generateToken( $this->mId . $this->mEmail . $expires );
2888                 $hash = md5( $token );
2889                 $this->load();
2890                 $this->mEmailToken = $hash;
2891                 $this->mEmailTokenExpires = $expiration;
2892                 return $token;
2893         }
2894
2895         /**
2896         * Return a URL the user can use to confirm their email address.
2897          * @param $token \string Accepts the email confirmation token
2898          * @return \string New token URL
2899          * @private
2900          */
2901         function confirmationTokenUrl( $token ) {
2902                 return $this->getTokenUrl( 'ConfirmEmail', $token );
2903         }
2904         /**
2905          * Return a URL the user can use to invalidate their email address.
2906          * @param $token \string Accepts the email confirmation token
2907          * @return \string New token URL
2908          * @private
2909          */
2910         function invalidationTokenUrl( $token ) {
2911                 return $this->getTokenUrl( 'Invalidateemail', $token );
2912         }
2913         
2914         /**
2915          * Internal function to format the e-mail validation/invalidation URLs.
2916          * This uses $wgArticlePath directly as a quickie hack to use the
2917          * hardcoded English names of the Special: pages, for ASCII safety.
2918          *
2919          * @note Since these URLs get dropped directly into emails, using the
2920          * short English names avoids insanely long URL-encoded links, which
2921          * also sometimes can get corrupted in some browsers/mailers
2922          * (bug 6957 with Gmail and Internet Explorer).
2923          *
2924          * @param $page \string Special page
2925          * @param $token \string Token
2926          * @return \string Formatted URL
2927          */
2928         protected function getTokenUrl( $page, $token ) {
2929                 global $wgArticlePath;
2930                 return wfExpandUrl(
2931                         str_replace(
2932                                 '$1',
2933                                 "Special:$page/$token",
2934                                 $wgArticlePath ) );
2935         }
2936
2937         /**
2938          * Mark the e-mail address confirmed.
2939          *
2940          * @note Call saveSettings() after calling this function to commit the change.
2941          */
2942         function confirmEmail() {
2943                 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
2944                 return true;
2945         }
2946
2947         /**
2948          * Invalidate the user's e-mail confirmation, and unauthenticate the e-mail
2949          * address if it was already confirmed.
2950          *
2951          * @note Call saveSettings() after calling this function to commit the change.
2952          */
2953         function invalidateEmail() {
2954                 $this->load();
2955                 $this->mEmailToken = null;
2956                 $this->mEmailTokenExpires = null;
2957                 $this->setEmailAuthenticationTimestamp( null );
2958                 return true;
2959         }
2960
2961         /**
2962          * Set the e-mail authentication timestamp.
2963          * @param $timestamp \string TS_MW timestamp
2964          */
2965         function setEmailAuthenticationTimestamp( $timestamp ) {
2966                 $this->load();
2967                 $this->mEmailAuthenticated = $timestamp;
2968                 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
2969         }
2970
2971         /**
2972          * Is this user allowed to send e-mails within limits of current
2973          * site configuration?
2974          * @return \bool True if allowed
2975          */
2976         function canSendEmail() {
2977                 global $wgEnableEmail, $wgEnableUserEmail;
2978                 if( !$wgEnableEmail || !$wgEnableUserEmail ) {
2979                         return false;
2980                 }
2981                 $canSend = $this->isEmailConfirmed();
2982                 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
2983                 return $canSend;
2984         }
2985
2986         /**
2987          * Is this user allowed to receive e-mails within limits of current
2988          * site configuration?
2989          * @return \bool True if allowed
2990          */
2991         function canReceiveEmail() {
2992                 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
2993         }
2994
2995         /**
2996          * Is this user's e-mail address valid-looking and confirmed within
2997          * limits of the current site configuration?
2998          *
2999          * @note If $wgEmailAuthentication is on, this may require the user to have
3000          * confirmed their address by returning a code or using a password
3001          * sent to the address from the wiki.
3002          *
3003          * @return \bool True if confirmed
3004          */
3005         function isEmailConfirmed() {
3006                 global $wgEmailAuthentication;
3007                 $this->load();
3008                 $confirmed = true;
3009                 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
3010                         if( $this->isAnon() )
3011                                 return false;
3012                         if( !self::isValidEmailAddr( $this->mEmail ) )
3013                                 return false;
3014                         if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
3015                                 return false;
3016                         return true;
3017                 } else {
3018                         return $confirmed;
3019                 }
3020         }
3021
3022         /**
3023          * Check whether there is an outstanding request for e-mail confirmation.
3024          * @return \bool True if pending
3025          */
3026         function isEmailConfirmationPending() {
3027                 global $wgEmailAuthentication;
3028                 return $wgEmailAuthentication &&
3029                         !$this->isEmailConfirmed() &&
3030                         $this->mEmailToken &&
3031                         $this->mEmailTokenExpires > wfTimestamp();
3032         }
3033
3034         /**
3035          * Get the timestamp of account creation.
3036          *
3037          * @return \types{\string,\bool} string Timestamp of account creation, or false for
3038          *                                non-existent/anonymous user accounts.
3039          */
3040         public function getRegistration() {
3041                 return $this->getId() > 0
3042                         ? $this->mRegistration
3043                         : false;
3044         }
3045         
3046         /**
3047          * Get the timestamp of the first edit
3048          *
3049          * @return \types{\string,\bool} string Timestamp of first edit, or false for
3050          *                                non-existent/anonymous user accounts.
3051          */
3052         public function getFirstEditTimestamp() {
3053                 if( $this->getId() == 0 ) return false; // anons
3054                 $dbr = wfGetDB( DB_SLAVE );
3055                 $time = $dbr->selectField( 'revision', 'rev_timestamp',
3056                         array( 'rev_user' => $this->getId() ),
3057                         __METHOD__,
3058                         array( 'ORDER BY' => 'rev_timestamp ASC' )
3059                 );
3060                 if( !$time ) return false; // no edits
3061                 return wfTimestamp( TS_MW, $time );
3062         }       
3063
3064         /**
3065          * Get the permissions associated with a given list of groups
3066          *
3067          * @param $groups \type{\arrayof{\string}} List of internal group names
3068          * @return \type{\arrayof{\string}} List of permission key names for given groups combined
3069          */
3070         static function getGroupPermissions( $groups ) {
3071                 global $wgGroupPermissions;
3072                 $rights = array();
3073                 foreach( $groups as $group ) {
3074                         if( isset( $wgGroupPermissions[$group] ) ) {
3075                                 $rights = array_merge( $rights,
3076                                         // array_filter removes empty items
3077                                         array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
3078                         }
3079                 }
3080                 return array_unique($rights);
3081         }
3082         
3083         /**
3084          * Get all the groups who have a given permission
3085          * 
3086          * @param $role \string Role to check
3087          * @return \type{\arrayof{\string}} List of internal group names with the given permission
3088          */
3089         static function getGroupsWithPermission( $role ) {
3090                 global $wgGroupPermissions;
3091                 $allowedGroups = array();
3092                 foreach ( $wgGroupPermissions as $group => $rights ) {
3093                         if ( isset( $rights[$role] ) && $rights[$role] ) {
3094                                 $allowedGroups[] = $group;
3095                         }
3096                 }
3097                 return $allowedGroups;
3098         }
3099
3100         /**
3101          * Get the localized descriptive name for a group, if it exists
3102          *
3103          * @param $group \string Internal group name
3104          * @return \string Localized descriptive group name
3105          */
3106         static function getGroupName( $group ) {
3107                 global $wgMessageCache;
3108                 $wgMessageCache->loadAllMessages();
3109                 $key = "group-$group";
3110                 $name = wfMsg( $key );
3111                 return $name == '' || wfEmptyMsg( $key, $name )
3112                         ? $group
3113                         : $name;
3114         }
3115
3116         /**
3117          * Get the localized descriptive name for a member of a group, if it exists
3118          *
3119          * @param $group \string Internal group name
3120          * @return \string Localized name for group member
3121          */
3122         static function getGroupMember( $group ) {
3123                 global $wgMessageCache;
3124                 $wgMessageCache->loadAllMessages();
3125                 $key = "group-$group-member";
3126                 $name = wfMsg( $key );
3127                 return $name == '' || wfEmptyMsg( $key, $name )
3128                         ? $group
3129                         : $name;
3130         }
3131
3132         /**
3133          * Return the set of defined explicit groups.
3134          * The implicit groups (by default *, 'user' and 'autoconfirmed')
3135          * are not included, as they are defined automatically, not in the database.
3136          * @return \type{\arrayof{\string}} Array of internal group names
3137          */
3138         static function getAllGroups() {
3139                 global $wgGroupPermissions;
3140                 return array_diff(
3141                         array_keys( $wgGroupPermissions ),
3142                         self::getImplicitGroups()
3143                 );
3144         }
3145
3146         /**
3147          * Get a list of all available permissions.
3148          * @return \type{\arrayof{\string}} Array of permission names
3149          */
3150         static function getAllRights() {
3151                 if ( self::$mAllRights === false ) {
3152                         global $wgAvailableRights;
3153                         if ( count( $wgAvailableRights ) ) {
3154                                 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
3155                         } else {
3156                                 self::$mAllRights = self::$mCoreRights;
3157                         }
3158                         wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
3159                 }
3160                 return self::$mAllRights;
3161         }
3162
3163         /**
3164          * Get a list of implicit groups
3165          * @return \type{\arrayof{\string}} Array of internal group names
3166          */
3167         public static function getImplicitGroups() {
3168                 global $wgImplicitGroups;
3169                 $groups = $wgImplicitGroups;
3170                 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );       #deprecated, use $wgImplictGroups instead
3171                 return $groups;
3172         }
3173
3174         /**
3175          * Get the title of a page describing a particular group
3176          *
3177          * @param $group \string Internal group name
3178          * @return \types{\type{Title},\bool} Title of the page if it exists, false otherwise
3179          */
3180         static function getGroupPage( $group ) {
3181                 global $wgMessageCache;
3182                 $wgMessageCache->loadAllMessages();
3183                 $page = wfMsgForContent( 'grouppage-' . $group );
3184                 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
3185                         $title = Title::newFromText( $page );
3186                         if( is_object( $title ) )
3187                                 return $title;
3188                 }
3189                 return false;
3190         }
3191
3192         /**
3193          * Create a link to the group in HTML, if available; 
3194          * else return the group name.
3195          *
3196          * @param $group \string Internal name of the group
3197          * @param $text \string The text of the link
3198          * @return \string HTML link to the group
3199          */
3200         static function makeGroupLinkHTML( $group, $text = '' ) {
3201                 if( $text == '' ) {
3202                         $text = self::getGroupName( $group );
3203                 }
3204                 $title = self::getGroupPage( $group );
3205                 if( $title ) {
3206                         global $wgUser;
3207                         $sk = $wgUser->getSkin();
3208                         return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
3209                 } else {
3210                         return $text;
3211                 }
3212         }
3213
3214         /**
3215          * Create a link to the group in Wikitext, if available; 
3216          * else return the group name.
3217          *
3218          * @param $group \string Internal name of the group
3219          * @param $text \string The text of the link
3220          * @return \string Wikilink to the group
3221          */
3222         static function makeGroupLinkWiki( $group, $text = '' ) {
3223                 if( $text == '' ) {
3224                         $text = self::getGroupName( $group );
3225                 }
3226                 $title = self::getGroupPage( $group );
3227                 if( $title ) {
3228                         $page = $title->getPrefixedText();
3229                         return "[[$page|$text]]";
3230                 } else {
3231                         return $text;
3232                 }
3233         }
3234
3235         /**
3236          * Increment the user's edit-count field.
3237          * Will have no effect for anonymous users.
3238          */
3239         function incEditCount() {
3240                 if( !$this->isAnon() ) {
3241                         $dbw = wfGetDB( DB_MASTER );
3242                         $dbw->update( 'user',
3243                                 array( 'user_editcount=user_editcount+1' ),
3244                                 array( 'user_id' => $this->getId() ),
3245                                 __METHOD__ );
3246
3247                         // Lazy initialization check...
3248                         if( $dbw->affectedRows() == 0 ) {
3249                                 // Pull from a slave to be less cruel to servers
3250                                 // Accuracy isn't the point anyway here
3251                                 $dbr = wfGetDB( DB_SLAVE );
3252                                 $count = $dbr->selectField( 'revision',
3253                                         'COUNT(rev_user)',
3254                                         array( 'rev_user' => $this->getId() ),
3255                                         __METHOD__ );
3256
3257                                 // Now here's a goddamn hack...
3258                                 if( $dbr !== $dbw ) {
3259                                         // If we actually have a slave server, the count is
3260                                         // at least one behind because the current transaction
3261                                         // has not been committed and replicated.
3262                                         $count++;
3263                                 } else {
3264                                         // But if DB_SLAVE is selecting the master, then the
3265                                         // count we just read includes the revision that was
3266                                         // just added in the working transaction.
3267                                 }
3268
3269                                 $dbw->update( 'user',
3270                                         array( 'user_editcount' => $count ),
3271                                         array( 'user_id' => $this->getId() ),
3272                                         __METHOD__ );
3273                         }
3274                 }
3275                 // edit count in user cache too
3276                 $this->invalidateCache();
3277         }
3278         
3279         /**
3280          * Get the description of a given right
3281          *
3282          * @param $right \string Right to query
3283          * @return \string Localized description of the right
3284          */
3285         static function getRightDescription( $right ) {
3286                 global $wgMessageCache;
3287                 $wgMessageCache->loadAllMessages();
3288                 $key = "right-$right";
3289                 $name = wfMsg( $key );
3290                 return $name == '' || wfEmptyMsg( $key, $name )
3291                         ? $right
3292                         : $name;
3293         }
3294
3295         /**
3296          * Make an old-style password hash
3297          *
3298          * @param $password \string Plain-text password
3299          * @param $userId \string User ID
3300          * @return \string Password hash
3301          */
3302         static function oldCrypt( $password, $userId ) {
3303                 global $wgPasswordSalt;
3304                 if ( $wgPasswordSalt ) {
3305                         return md5( $userId . '-' . md5( $password ) );
3306                 } else {
3307                         return md5( $password );
3308                 }
3309         }
3310
3311         /**
3312          * Make a new-style password hash
3313          *
3314          * @param $password \string Plain-text password
3315          * @param $salt \string Optional salt, may be random or the user ID. 
3316          *                     If unspecified or false, will generate one automatically
3317          * @return \string Password hash
3318          */
3319         static function crypt( $password, $salt = false ) {
3320                 global $wgPasswordSalt;
3321
3322                 $hash = '';
3323                 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
3324                         return $hash;
3325                 }
3326                 
3327                 if( $wgPasswordSalt ) {
3328                         if ( $salt === false ) {
3329                                 $salt = substr( wfGenerateToken(), 0, 8 );
3330                         }
3331                         return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
3332                 } else {
3333                         return ':A:' . md5( $password );
3334                 }
3335         }
3336
3337         /**
3338          * Compare a password hash with a plain-text password. Requires the user
3339          * ID if there's a chance that the hash is an old-style hash.
3340          *
3341          * @param $hash \string Password hash
3342          * @param $password \string Plain-text password to compare
3343          * @param $userId \string User ID for old-style password salt
3344          * @return \bool
3345          */
3346         static function comparePasswords( $hash, $password, $userId = false ) {
3347                 $m = false;
3348                 $type = substr( $hash, 0, 3 );
3349                 
3350                 $result = false;
3351                 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
3352                         return $result;
3353                 }
3354                 
3355                 if ( $type == ':A:' ) {
3356                         # Unsalted
3357                         return md5( $password ) === substr( $hash, 3 );
3358                 } elseif ( $type == ':B:' ) {
3359                         # Salted
3360                         list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
3361                         return md5( $salt.'-'.md5( $password ) ) == $realHash;
3362                 } else {
3363                         # Old-style
3364                         return self::oldCrypt( $password, $userId ) === $hash;
3365                 }
3366         }
3367         
3368         /**
3369          * Add a newuser log entry for this user
3370          * @param $byEmail Boolean: account made by email?
3371          */
3372         public function addNewUserLogEntry( $byEmail = false ) {
3373                 global $wgUser, $wgContLang, $wgNewUserLog;
3374                 if( empty($wgNewUserLog) ) {
3375                         return true; // disabled
3376                 }
3377                 $talk = $wgContLang->getFormattedNsText( NS_TALK );
3378                 if( $this->getName() == $wgUser->getName() ) {
3379                         $action = 'create';
3380                         $message = '';
3381                 } else {
3382                         $action = 'create2';
3383                         $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
3384                 }
3385                 $log = new LogPage( 'newusers' );
3386                 $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
3387                 return true;
3388         }
3389
3390         /**
3391          * Add an autocreate newuser log entry for this user
3392          * Used by things like CentralAuth and perhaps other authplugins.
3393          */
3394         public function addNewUserLogEntryAutoCreate() {
3395                 global $wgNewUserLog;
3396                 if( empty($wgNewUserLog) ) {
3397                         return true; // disabled
3398                 }
3399                 $log = new LogPage( 'newusers', false );
3400                 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) );
3401                 return true;
3402         }
3403
3404 }