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