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