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