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