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