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