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