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