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