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