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