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