d0c2db68a390907467671bc21e7cb0667e284b59
[lhc/web/wiklou.git] / includes / User.php
1 <?php
2 /**
3 * See user.txt
4 *
5 */
6
7 # Number of characters in user_token field
8 define( 'USER_TOKEN_LENGTH', 32 );
9
10 # Serialized record version
11 define( 'MW_USER_VERSION', 5 );
12
13 # Some punctuation to prevent editing from broken text-mangling proxies.
14 # FIXME: this is embedded unescaped into HTML attributes in various
15 # places, so we can't safely include ' or " even though we really should.
16 define( 'EDIT_TOKEN_SUFFIX', '\\' );
17
18 /**
19 * Thrown by User::setPassword() on error
20 * @addtogroup Exception
21 */
22 class PasswordError extends MWException {
23 // NOP
24 }
25
26 /**
27 *
28 */
29 class User {
30
31 /**
32 * A list of default user toggles, i.e. boolean user preferences that are
33 * displayed by Special:Preferences as checkboxes. This list can be
34 * extended via the UserToggles hook or $wgContLang->getExtraUserToggles().
35 */
36 static public $mToggles = array(
37 'highlightbroken',
38 'justify',
39 'hideminor',
40 'extendwatchlist',
41 'usenewrc',
42 'numberheadings',
43 'showtoolbar',
44 'editondblclick',
45 'editsection',
46 'editsectiononrightclick',
47 'showtoc',
48 'rememberpassword',
49 'editwidth',
50 'watchcreations',
51 'watchdefault',
52 'watchmoves',
53 'watchdeletion',
54 'minordefault',
55 'previewontop',
56 'previewonfirst',
57 'nocache',
58 'enotifwatchlistpages',
59 'enotifusertalkpages',
60 'enotifminoredits',
61 'enotifrevealaddr',
62 'shownumberswatching',
63 'fancysig',
64 'externaleditor',
65 'externaldiff',
66 'showjumplinks',
67 'uselivepreview',
68 'forceeditsummary',
69 'watchlisthideown',
70 'watchlisthidebots',
71 'watchlisthideminor',
72 'ccmeonemails',
73 'diffonly',
74 );
75
76 /**
77 * List of member variables which are saved to the shared cache (memcached).
78 * Any operation which changes the corresponding database fields must
79 * call a cache-clearing function.
80 */
81 static $mCacheVars = array(
82 # user table
83 'mId',
84 'mName',
85 'mRealName',
86 'mPassword',
87 'mNewpassword',
88 'mNewpassTime',
89 'mEmail',
90 'mOptions',
91 'mTouched',
92 'mToken',
93 'mEmailAuthenticated',
94 'mEmailToken',
95 'mEmailTokenExpires',
96 'mRegistration',
97 'mEditCount',
98 # user_group table
99 'mGroups',
100 );
101
102 /**
103 * The cache variable declarations
104 */
105 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
106 $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
107 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
108
109 /**
110 * Whether the cache variables have been loaded
111 */
112 var $mDataLoaded;
113
114 /**
115 * Initialisation data source if mDataLoaded==false. May be one of:
116 * defaults anonymous user initialised from class defaults
117 * name initialise from mName
118 * id initialise from mId
119 * session log in from cookies or session if possible
120 *
121 * Use the User::newFrom*() family of functions to set this.
122 */
123 var $mFrom;
124
125 /**
126 * Lazy-initialised variables, invalidated with clearInstanceCache
127 */
128 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
129 $mBlockreason, $mBlock, $mEffectiveGroups;
130
131 /**
132 * Lightweight constructor for anonymous user
133 * Use the User::newFrom* factory functions for other kinds of users
134 */
135 function User() {
136 $this->clearInstanceCache( 'defaults' );
137 }
138
139 /**
140 * Load the user table data for this object from the source given by mFrom
141 */
142 function load() {
143 if ( $this->mDataLoaded ) {
144 return;
145 }
146 wfProfileIn( __METHOD__ );
147
148 # Set it now to avoid infinite recursion in accessors
149 $this->mDataLoaded = true;
150
151 switch ( $this->mFrom ) {
152 case 'defaults':
153 $this->loadDefaults();
154 break;
155 case 'name':
156 $this->mId = self::idFromName( $this->mName );
157 if ( !$this->mId ) {
158 # Nonexistent user placeholder object
159 $this->loadDefaults( $this->mName );
160 } else {
161 $this->loadFromId();
162 }
163 break;
164 case 'id':
165 $this->loadFromId();
166 break;
167 case 'session':
168 $this->loadFromSession();
169 break;
170 default:
171 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
172 }
173 wfProfileOut( __METHOD__ );
174 }
175
176 /**
177 * Load user table data given mId
178 * @return false if the ID does not exist, true otherwise
179 * @private
180 */
181 function loadFromId() {
182 global $wgMemc;
183 if ( $this->mId == 0 ) {
184 $this->loadDefaults();
185 return false;
186 }
187
188 # Try cache
189 $key = wfMemcKey( 'user', 'id', $this->mId );
190 $data = $wgMemc->get( $key );
191 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
192 # Object is expired, load from DB
193 $data = false;
194 }
195
196 if ( !$data ) {
197 wfDebug( "Cache miss for user {$this->mId}\n" );
198 # Load from DB
199 if ( !$this->loadFromDatabase() ) {
200 # Can't load from ID, user is anonymous
201 return false;
202 }
203
204 # Save to cache
205 $data = array();
206 foreach ( self::$mCacheVars as $name ) {
207 $data[$name] = $this->$name;
208 }
209 $data['mVersion'] = MW_USER_VERSION;
210 $wgMemc->set( $key, $data );
211 } else {
212 wfDebug( "Got user {$this->mId} from cache\n" );
213 # Restore from cache
214 foreach ( self::$mCacheVars as $name ) {
215 $this->$name = $data[$name];
216 }
217 }
218 return true;
219 }
220
221 /**
222 * Static factory method for creation from username.
223 *
224 * This is slightly less efficient than newFromId(), so use newFromId() if
225 * you have both an ID and a name handy.
226 *
227 * @param string $name Username, validated by Title:newFromText()
228 * @param mixed $validate Validate username. Takes the same parameters as
229 * User::getCanonicalName(), except that true is accepted as an alias
230 * for 'valid', for BC.
231 *
232 * @return User object, or null if the username is invalid. If the username
233 * is not present in the database, the result will be a user object with
234 * a name, zero user ID and default settings.
235 * @static
236 */
237 static function newFromName( $name, $validate = 'valid' ) {
238 if ( $validate === true ) {
239 $validate = 'valid';
240 }
241 $name = self::getCanonicalName( $name, $validate );
242 if ( $name === false ) {
243 return null;
244 } else {
245 # Create unloaded user object
246 $u = new User;
247 $u->mName = $name;
248 $u->mFrom = 'name';
249 return $u;
250 }
251 }
252
253 static function newFromId( $id ) {
254 $u = new User;
255 $u->mId = $id;
256 $u->mFrom = 'id';
257 return $u;
258 }
259
260 /**
261 * Factory method to fetch whichever user has a given email confirmation code.
262 * This code is generated when an account is created or its e-mail address
263 * has changed.
264 *
265 * If the code is invalid or has expired, returns NULL.
266 *
267 * @param string $code
268 * @return User
269 * @static
270 */
271 static function newFromConfirmationCode( $code ) {
272 $dbr = wfGetDB( DB_SLAVE );
273 $id = $dbr->selectField( 'user', 'user_id', array(
274 'user_email_token' => md5( $code ),
275 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
276 ) );
277 if( $id !== false ) {
278 return User::newFromId( $id );
279 } else {
280 return null;
281 }
282 }
283
284 /**
285 * Create a new user object using data from session or cookies. If the
286 * login credentials are invalid, the result is an anonymous user.
287 *
288 * @return User
289 * @static
290 */
291 static function newFromSession() {
292 $user = new User;
293 $user->mFrom = 'session';
294 return $user;
295 }
296
297 /**
298 * Get username given an id.
299 * @param integer $id Database user id
300 * @return string Nickname of a user
301 * @static
302 */
303 static function whoIs( $id ) {
304 $dbr = wfGetDB( DB_SLAVE );
305 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
306 }
307
308 /**
309 * Get real username given an id.
310 * @param integer $id Database user id
311 * @return string Realname of a user
312 * @static
313 */
314 static function whoIsReal( $id ) {
315 $dbr = wfGetDB( DB_SLAVE );
316 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
317 }
318
319 /**
320 * Get database id given a user name
321 * @param string $name Nickname of a user
322 * @return integer|null Database user id (null: if non existent
323 * @static
324 */
325 static function idFromName( $name ) {
326 $nt = Title::newFromText( $name );
327 if( is_null( $nt ) ) {
328 # Illegal name
329 return null;
330 }
331 $dbr = wfGetDB( DB_SLAVE );
332 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
333
334 if ( $s === false ) {
335 return 0;
336 } else {
337 return $s->user_id;
338 }
339 }
340
341 /**
342 * Does the string match an anonymous IPv4 address?
343 *
344 * This function exists for username validation, in order to reject
345 * usernames which are similar in form to IP addresses. Strings such
346 * as 300.300.300.300 will return true because it looks like an IP
347 * address, despite not being strictly valid.
348 *
349 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
350 * address because the usemod software would "cloak" anonymous IP
351 * addresses like this, if we allowed accounts like this to be created
352 * new users could get the old edits of these anonymous users.
353 *
354 * @static
355 * @param string $name Nickname of a user
356 * @return bool
357 */
358 static function isIP( $name ) {
359 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || User::isIPv6($name);
360 /*return preg_match("/^
361 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
362 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
363 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
364 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
365 $/x", $name);*/
366 }
367
368 /**
369 * Check if $name is an IPv6 IP.
370 */
371 static function isIPv6($name) {
372 /*
373 * if it has any non-valid characters, it can't be a valid IPv6
374 * address.
375 */
376 if (preg_match("/[^:a-fA-F0-9]/", $name))
377 return false;
378
379 $parts = explode(":", $name);
380 if (count($parts) < 3)
381 return false;
382 foreach ($parts as $part) {
383 if (!preg_match("/^[0-9a-fA-F]{0,4}$/", $part))
384 return false;
385 }
386 return true;
387 }
388
389 /**
390 * Is the input a valid username?
391 *
392 * Checks if the input is a valid username, we don't want an empty string,
393 * an IP address, anything that containins slashes (would mess up subpages),
394 * is longer than the maximum allowed username size or doesn't begin with
395 * a capital letter.
396 *
397 * @param string $name
398 * @return bool
399 * @static
400 */
401 static function isValidUserName( $name ) {
402 global $wgContLang, $wgMaxNameChars;
403
404 if ( $name == ''
405 || User::isIP( $name )
406 || strpos( $name, '/' ) !== false
407 || strlen( $name ) > $wgMaxNameChars
408 || $name != $wgContLang->ucfirst( $name ) )
409 return false;
410
411 // Ensure that the name can't be misresolved as a different title,
412 // such as with extra namespace keys at the start.
413 $parsed = Title::newFromText( $name );
414 if( is_null( $parsed )
415 || $parsed->getNamespace()
416 || strcmp( $name, $parsed->getPrefixedText() ) )
417 return false;
418
419 // Check an additional blacklist of troublemaker characters.
420 // Should these be merged into the title char list?
421 $unicodeBlacklist = '/[' .
422 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
423 '\x{00a0}' . # non-breaking space
424 '\x{2000}-\x{200f}' . # various whitespace
425 '\x{2028}-\x{202f}' . # breaks and control chars
426 '\x{3000}' . # ideographic space
427 '\x{e000}-\x{f8ff}' . # private use
428 ']/u';
429 if( preg_match( $unicodeBlacklist, $name ) ) {
430 return false;
431 }
432
433 return true;
434 }
435
436 /**
437 * Usernames which fail to pass this function will be blocked
438 * from user login and new account registrations, but may be used
439 * internally by batch processes.
440 *
441 * If an account already exists in this form, login will be blocked
442 * by a failure to pass this function.
443 *
444 * @param string $name
445 * @return bool
446 */
447 static function isUsableName( $name ) {
448 global $wgReservedUsernames;
449 return
450 // Must be a usable username, obviously ;)
451 self::isValidUserName( $name ) &&
452
453 // Certain names may be reserved for batch processes.
454 !in_array( $name, $wgReservedUsernames );
455 }
456
457 /**
458 * Usernames which fail to pass this function will be blocked
459 * from new account registrations, but may be used internally
460 * either by batch processes or by user accounts which have
461 * already been created.
462 *
463 * Additional character blacklisting may be added here
464 * rather than in isValidUserName() to avoid disrupting
465 * existing accounts.
466 *
467 * @param string $name
468 * @return bool
469 */
470 static function isCreatableName( $name ) {
471 return
472 self::isUsableName( $name ) &&
473
474 // Registration-time character blacklisting...
475 strpos( $name, '@' ) === false;
476 }
477
478 /**
479 * Is the input a valid password?
480 *
481 * @param string $password
482 * @return bool
483 * @static
484 */
485 static function isValidPassword( $password ) {
486 global $wgMinimalPasswordLength;
487
488 $result = null;
489 if( !wfRunHooks( 'isValidPassword', array( $password, &$result ) ) ) return $result;
490 if ($result === false) return false;
491 return (strlen( $password ) >= $wgMinimalPasswordLength);
492 }
493
494 /**
495 * Does the string match roughly an email address ?
496 *
497 * There used to be a regular expression here, it got removed because it
498 * rejected valid addresses. Actually just check if there is '@' somewhere
499 * in the given address.
500 *
501 * @todo Check for RFC 2822 compilance (bug 959)
502 *
503 * @param string $addr email address
504 * @static
505 * @return bool
506 */
507 static function isValidEmailAddr ( $addr ) {
508 return ( trim( $addr ) != '' ) &&
509 (false !== strpos( $addr, '@' ) );
510 }
511
512 /**
513 * Given unvalidated user input, return a canonical username, or false if
514 * the username is invalid.
515 * @param string $name
516 * @param mixed $validate Type of validation to use:
517 * false No validation
518 * 'valid' Valid for batch processes
519 * 'usable' Valid for batch processes and login
520 * 'creatable' Valid for batch processes, login and account creation
521 */
522 static function getCanonicalName( $name, $validate = 'valid' ) {
523 # Force usernames to capital
524 global $wgContLang;
525 $name = $wgContLang->ucfirst( $name );
526
527 # Clean up name according to title rules
528 $t = Title::newFromText( $name );
529 if( is_null( $t ) ) {
530 return false;
531 }
532
533 # Reject various classes of invalid names
534 $name = $t->getText();
535 global $wgAuth;
536 $name = $wgAuth->getCanonicalName( $t->getText() );
537
538 switch ( $validate ) {
539 case false:
540 break;
541 case 'valid':
542 if ( !User::isValidUserName( $name ) ) {
543 $name = false;
544 }
545 break;
546 case 'usable':
547 if ( !User::isUsableName( $name ) ) {
548 $name = false;
549 }
550 break;
551 case 'creatable':
552 if ( !User::isCreatableName( $name ) ) {
553 $name = false;
554 }
555 break;
556 default:
557 throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
558 }
559 return $name;
560 }
561
562 /**
563 * Count the number of edits of a user
564 *
565 * It should not be static and some day should be merged as proper member function / deprecated -- domas
566 *
567 * @param int $uid The user ID to check
568 * @return int
569 * @static
570 */
571 static function edits( $uid ) {
572 wfProfileIn( __METHOD__ );
573 $dbr = wfGetDB( DB_SLAVE );
574 // check if the user_editcount field has been initialized
575 $field = $dbr->selectField(
576 'user', 'user_editcount',
577 array( 'user_id' => $uid ),
578 __METHOD__
579 );
580
581 if( $field === null ) { // it has not been initialized. do so.
582 $dbw = wfGetDb( DB_MASTER );
583 $count = $dbr->selectField(
584 'revision', 'count(*)',
585 array( 'rev_user' => $uid ),
586 __METHOD__
587 );
588 $dbw->update(
589 'user',
590 array( 'user_editcount' => $count ),
591 array( 'user_id' => $uid ),
592 __METHOD__
593 );
594 } else {
595 $count = $field;
596 }
597 wfProfileOut( __METHOD__ );
598 return $count;
599 }
600
601 /**
602 * Return a random password. Sourced from mt_rand, so it's not particularly secure.
603 * @todo hash random numbers to improve security, like generateToken()
604 *
605 * @return string
606 * @static
607 */
608 static function randomPassword() {
609 global $wgMinimalPasswordLength;
610 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
611 $l = strlen( $pwchars ) - 1;
612
613 $pwlength = max( 7, $wgMinimalPasswordLength );
614 $digit = mt_rand(0, $pwlength - 1);
615 $np = '';
616 for ( $i = 0; $i < $pwlength; $i++ ) {
617 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
618 }
619 return $np;
620 }
621
622 /**
623 * Set cached properties to default. Note: this no longer clears
624 * uncached lazy-initialised properties. The constructor does that instead.
625 *
626 * @private
627 */
628 function loadDefaults( $name = false ) {
629 wfProfileIn( __METHOD__ );
630
631 global $wgCookiePrefix;
632
633 $this->mId = 0;
634 $this->mName = $name;
635 $this->mRealName = '';
636 $this->mPassword = $this->mNewpassword = '';
637 $this->mNewpassTime = null;
638 $this->mEmail = '';
639 $this->mOptions = null; # Defer init
640
641 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
642 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
643 } else {
644 $this->mTouched = '0'; # Allow any pages to be cached
645 }
646
647 $this->setToken(); # Random
648 $this->mEmailAuthenticated = null;
649 $this->mEmailToken = '';
650 $this->mEmailTokenExpires = null;
651 $this->mRegistration = wfTimestamp( TS_MW );
652 $this->mGroups = array();
653
654 wfProfileOut( __METHOD__ );
655 }
656
657 /**
658 * Initialise php session
659 * @deprecated use wfSetupSession()
660 */
661 function SetupSession() {
662 wfSetupSession();
663 }
664
665 /**
666 * Load user data from the session or login cookie. If there are no valid
667 * credentials, initialises the user as an anon.
668 * @return true if the user is logged in, false otherwise
669 *
670 * @private
671 */
672 function loadFromSession() {
673 global $wgMemc, $wgCookiePrefix;
674
675 if ( isset( $_SESSION['wsUserID'] ) ) {
676 if ( 0 != $_SESSION['wsUserID'] ) {
677 $sId = $_SESSION['wsUserID'];
678 } else {
679 $this->loadDefaults();
680 return false;
681 }
682 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
683 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
684 $_SESSION['wsUserID'] = $sId;
685 } else {
686 $this->loadDefaults();
687 return false;
688 }
689 if ( isset( $_SESSION['wsUserName'] ) ) {
690 $sName = $_SESSION['wsUserName'];
691 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
692 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
693 $_SESSION['wsUserName'] = $sName;
694 } else {
695 $this->loadDefaults();
696 return false;
697 }
698
699 $passwordCorrect = FALSE;
700 $this->mId = $sId;
701 if ( !$this->loadFromId() ) {
702 # Not a valid ID, loadFromId has switched the object to anon for us
703 return false;
704 }
705
706 if ( isset( $_SESSION['wsToken'] ) ) {
707 $passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
708 $from = 'session';
709 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
710 $passwordCorrect = $this->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
711 $from = 'cookie';
712 } else {
713 # No session or persistent login cookie
714 $this->loadDefaults();
715 return false;
716 }
717
718 if ( ( $sName == $this->mName ) && $passwordCorrect ) {
719 wfDebug( "Logged in from $from\n" );
720 return true;
721 } else {
722 # Invalid credentials
723 wfDebug( "Can't log in from $from, invalid credentials\n" );
724 $this->loadDefaults();
725 return false;
726 }
727 }
728
729 /**
730 * Load user and user_group data from the database
731 * $this->mId must be set, this is how the user is identified.
732 *
733 * @return true if the user exists, false if the user is anonymous
734 * @private
735 */
736 function loadFromDatabase() {
737 # Paranoia
738 $this->mId = intval( $this->mId );
739
740 /** Anonymous user */
741 if( !$this->mId ) {
742 $this->loadDefaults();
743 return false;
744 }
745
746 $dbr = wfGetDB( DB_MASTER );
747 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ );
748
749 if ( $s !== false ) {
750 # Initialise user table data
751 $this->mName = $s->user_name;
752 $this->mRealName = $s->user_real_name;
753 $this->mPassword = $s->user_password;
754 $this->mNewpassword = $s->user_newpassword;
755 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $s->user_newpass_time );
756 $this->mEmail = $s->user_email;
757 $this->decodeOptions( $s->user_options );
758 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
759 $this->mToken = $s->user_token;
760 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
761 $this->mEmailToken = $s->user_email_token;
762 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires );
763 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
764 $this->mEditCount = $s->user_editcount;
765 $this->getEditCount(); // revalidation for nulls
766
767 # Load group data
768 $res = $dbr->select( 'user_groups',
769 array( 'ug_group' ),
770 array( 'ug_user' => $this->mId ),
771 __METHOD__ );
772 $this->mGroups = array();
773 while( $row = $dbr->fetchObject( $res ) ) {
774 $this->mGroups[] = $row->ug_group;
775 }
776 return true;
777 } else {
778 # Invalid user_id
779 $this->mId = 0;
780 $this->loadDefaults();
781 return false;
782 }
783 }
784
785 /**
786 * Clear various cached data stored in this object.
787 * @param string $reloadFrom Reload user and user_groups table data from a
788 * given source. May be "name", "id", "defaults", "session" or false for
789 * no reload.
790 */
791 function clearInstanceCache( $reloadFrom = false ) {
792 $this->mNewtalk = -1;
793 $this->mDatePreference = null;
794 $this->mBlockedby = -1; # Unset
795 $this->mHash = false;
796 $this->mSkin = null;
797 $this->mRights = null;
798 $this->mEffectiveGroups = null;
799
800 if ( $reloadFrom ) {
801 $this->mDataLoaded = false;
802 $this->mFrom = $reloadFrom;
803 }
804 }
805
806 /**
807 * Combine the language default options with any site-specific options
808 * and add the default language variants.
809 * Not really private cause it's called by Language class
810 * @return array
811 * @static
812 * @private
813 */
814 static function getDefaultOptions() {
815 global $wgNamespacesToBeSearchedDefault;
816 /**
817 * Site defaults will override the global/language defaults
818 */
819 global $wgDefaultUserOptions, $wgContLang;
820 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
821
822 /**
823 * default language setting
824 */
825 $variant = $wgContLang->getPreferredVariant( false );
826 $defOpt['variant'] = $variant;
827 $defOpt['language'] = $variant;
828
829 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
830 $defOpt['searchNs'.$nsnum] = $val;
831 }
832 return $defOpt;
833 }
834
835 /**
836 * Get a given default option value.
837 *
838 * @param string $opt
839 * @return string
840 * @static
841 * @public
842 */
843 function getDefaultOption( $opt ) {
844 $defOpts = User::getDefaultOptions();
845 if( isset( $defOpts[$opt] ) ) {
846 return $defOpts[$opt];
847 } else {
848 return '';
849 }
850 }
851
852 /**
853 * Get a list of user toggle names
854 * @return array
855 */
856 static function getToggles() {
857 global $wgContLang;
858 $extraToggles = array();
859 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
860 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
861 }
862
863
864 /**
865 * Get blocking information
866 * @private
867 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
868 * non-critical checks are done against slaves. Check when actually saving should be done against
869 * master.
870 */
871 function getBlockedStatus( $bFromSlave = true ) {
872 global $wgEnableSorbs, $wgProxyWhitelist;
873
874 if ( -1 != $this->mBlockedby ) {
875 wfDebug( "User::getBlockedStatus: already loaded.\n" );
876 return;
877 }
878
879 wfProfileIn( __METHOD__ );
880 wfDebug( __METHOD__.": checking...\n" );
881
882 $this->mBlockedby = 0;
883 $this->mHideName = 0;
884 $ip = wfGetIP();
885
886 if ($this->isAllowed( 'ipblock-exempt' ) ) {
887 # Exempt from all types of IP-block
888 $ip = '';
889 }
890
891 # User/IP blocking
892 $this->mBlock = new Block();
893 $this->mBlock->fromMaster( !$bFromSlave );
894 if ( $this->mBlock->load( $ip , $this->mId ) ) {
895 wfDebug( __METHOD__.": Found block.\n" );
896 $this->mBlockedby = $this->mBlock->mBy;
897 $this->mBlockreason = $this->mBlock->mReason;
898 $this->mHideName = $this->mBlock->mHideName;
899 if ( $this->isLoggedIn() ) {
900 $this->spreadBlock();
901 }
902 } else {
903 $this->mBlock = null;
904 wfDebug( __METHOD__.": No block.\n" );
905 }
906
907 # Proxy blocking
908 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
909
910 # Local list
911 if ( wfIsLocallyBlockedProxy( $ip ) ) {
912 $this->mBlockedby = wfMsg( 'proxyblocker' );
913 $this->mBlockreason = wfMsg( 'proxyblockreason' );
914 }
915
916 # DNSBL
917 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
918 if ( $this->inSorbsBlacklist( $ip ) ) {
919 $this->mBlockedby = wfMsg( 'sorbs' );
920 $this->mBlockreason = wfMsg( 'sorbsreason' );
921 }
922 }
923 }
924
925 # Extensions
926 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
927
928 wfProfileOut( __METHOD__ );
929 }
930
931 function inSorbsBlacklist( $ip ) {
932 global $wgEnableSorbs, $wgSorbsUrl;
933
934 return $wgEnableSorbs &&
935 $this->inDnsBlacklist( $ip, $wgSorbsUrl );
936 }
937
938 function inDnsBlacklist( $ip, $base ) {
939 wfProfileIn( __METHOD__ );
940
941 $found = false;
942 $host = '';
943
944 $m = array();
945 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
946 # Make hostname
947 for ( $i=4; $i>=1; $i-- ) {
948 $host .= $m[$i] . '.';
949 }
950 $host .= $base;
951
952 # Send query
953 $ipList = gethostbynamel( $host );
954
955 if ( $ipList ) {
956 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
957 $found = true;
958 } else {
959 wfDebug( "Requested $host, not found in $base.\n" );
960 }
961 }
962
963 wfProfileOut( __METHOD__ );
964 return $found;
965 }
966
967 /**
968 * Is this user subject to rate limiting?
969 *
970 * @return bool
971 */
972 public function isPingLimitable() {
973 global $wgRateLimitsExcludedGroups;
974 return array_intersect($this->getEffectiveGroups(), $wgRateLimitsExcludedGroups) != array();
975 }
976
977 /**
978 * Primitive rate limits: enforce maximum actions per time period
979 * to put a brake on flooding.
980 *
981 * Note: when using a shared cache like memcached, IP-address
982 * last-hit counters will be shared across wikis.
983 *
984 * @return bool true if a rate limiter was tripped
985 * @public
986 */
987 function pingLimiter( $action='edit' ) {
988
989 # Call the 'PingLimiter' hook
990 $result = false;
991 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
992 return $result;
993 }
994
995 global $wgRateLimits, $wgRateLimitsExcludedGroups;
996 if( !isset( $wgRateLimits[$action] ) ) {
997 return false;
998 }
999
1000 # Some groups shouldn't trigger the ping limiter, ever
1001 if( !$this->isPingLimitable() )
1002 return false;
1003
1004 global $wgMemc, $wgRateLimitLog;
1005 wfProfileIn( __METHOD__ );
1006
1007 $limits = $wgRateLimits[$action];
1008 $keys = array();
1009 $id = $this->getId();
1010 $ip = wfGetIP();
1011
1012 if( isset( $limits['anon'] ) && $id == 0 ) {
1013 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
1014 }
1015
1016 if( isset( $limits['user'] ) && $id != 0 ) {
1017 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['user'];
1018 }
1019 if( $this->isNewbie() ) {
1020 if( isset( $limits['newbie'] ) && $id != 0 ) {
1021 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
1022 }
1023 if( isset( $limits['ip'] ) ) {
1024 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
1025 }
1026 $matches = array();
1027 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
1028 $subnet = $matches[1];
1029 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
1030 }
1031 }
1032
1033 $triggered = false;
1034 foreach( $keys as $key => $limit ) {
1035 list( $max, $period ) = $limit;
1036 $summary = "(limit $max in {$period}s)";
1037 $count = $wgMemc->get( $key );
1038 if( $count ) {
1039 if( $count > $max ) {
1040 wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
1041 if( $wgRateLimitLog ) {
1042 @error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
1043 }
1044 $triggered = true;
1045 } else {
1046 wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
1047 }
1048 } else {
1049 wfDebug( __METHOD__.": adding record for $key $summary\n" );
1050 $wgMemc->add( $key, 1, intval( $period ) );
1051 }
1052 $wgMemc->incr( $key );
1053 }
1054
1055 wfProfileOut( __METHOD__ );
1056 return $triggered;
1057 }
1058
1059 /**
1060 * Check if user is blocked
1061 * @return bool True if blocked, false otherwise
1062 */
1063 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
1064 wfDebug( "User::isBlocked: enter\n" );
1065 $this->getBlockedStatus( $bFromSlave );
1066 return $this->mBlockedby !== 0;
1067 }
1068
1069 /**
1070 * Check if user is blocked from editing a particular article
1071 */
1072 function isBlockedFrom( $title, $bFromSlave = false ) {
1073 global $wgBlockAllowsUTEdit;
1074 wfProfileIn( __METHOD__ );
1075 wfDebug( __METHOD__.": enter\n" );
1076
1077 wfDebug( __METHOD__.": asking isBlocked()\n" );
1078 $blocked = $this->isBlocked( $bFromSlave );
1079 # If a user's name is suppressed, they cannot make edits anywhere
1080 if ( !$this->mHideName && $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
1081 $title->getNamespace() == NS_USER_TALK ) {
1082 $blocked = false;
1083 wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
1084 }
1085 wfProfileOut( __METHOD__ );
1086 return $blocked;
1087 }
1088
1089 /**
1090 * Get name of blocker
1091 * @return string name of blocker
1092 */
1093 function blockedBy() {
1094 $this->getBlockedStatus();
1095 return $this->mBlockedby;
1096 }
1097
1098 /**
1099 * Get blocking reason
1100 * @return string Blocking reason
1101 */
1102 function blockedFor() {
1103 $this->getBlockedStatus();
1104 return $this->mBlockreason;
1105 }
1106
1107 /**
1108 * Get the user ID. Returns 0 if the user is anonymous or nonexistent.
1109 */
1110 function getID() {
1111 $this->load();
1112 return $this->mId;
1113 }
1114
1115 /**
1116 * Set the user and reload all fields according to that ID
1117 * @deprecated use User::newFromId()
1118 */
1119 function setID( $v ) {
1120 $this->mId = $v;
1121 $this->clearInstanceCache( 'id' );
1122 }
1123
1124 /**
1125 * Get the user name, or the IP for anons
1126 */
1127 function getName() {
1128 if ( !$this->mDataLoaded && $this->mFrom == 'name' ) {
1129 # Special case optimisation
1130 return $this->mName;
1131 } else {
1132 $this->load();
1133 if ( $this->mName === false ) {
1134 $this->mName = wfGetIP();
1135 }
1136 # Clean up IPs
1137 return IP::sanitizeIP($this->mName);
1138 }
1139 }
1140
1141 /**
1142 * Set the user name.
1143 *
1144 * This does not reload fields from the database according to the given
1145 * name. Rather, it is used to create a temporary "nonexistent user" for
1146 * later addition to the database. It can also be used to set the IP
1147 * address for an anonymous user to something other than the current
1148 * remote IP.
1149 *
1150 * User::newFromName() has rougly the same function, when the named user
1151 * does not exist.
1152 */
1153 function setName( $str ) {
1154 $this->load();
1155 $this->mName = $str;
1156 }
1157
1158 /**
1159 * Return the title dbkey form of the name, for eg user pages.
1160 * @return string
1161 * @public
1162 */
1163 function getTitleKey() {
1164 return str_replace( ' ', '_', $this->getName() );
1165 }
1166
1167 function getNewtalk() {
1168 $this->load();
1169
1170 # Load the newtalk status if it is unloaded (mNewtalk=-1)
1171 if( $this->mNewtalk === -1 ) {
1172 $this->mNewtalk = false; # reset talk page status
1173
1174 # Check memcached separately for anons, who have no
1175 # entire User object stored in there.
1176 if( !$this->mId ) {
1177 global $wgMemc;
1178 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
1179 $newtalk = $wgMemc->get( $key );
1180 if( $newtalk != "" ) {
1181 $this->mNewtalk = (bool)$newtalk;
1182 } else {
1183 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
1184 $wgMemc->set( $key, (int)$this->mNewtalk, time() + 1800 );
1185 }
1186 } else {
1187 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
1188 }
1189 }
1190
1191 return (bool)$this->mNewtalk;
1192 }
1193
1194 /**
1195 * Return the talk page(s) this user has new messages on.
1196 */
1197 function getNewMessageLinks() {
1198 $talks = array();
1199 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
1200 return $talks;
1201
1202 if (!$this->getNewtalk())
1203 return array();
1204 $up = $this->getUserPage();
1205 $utp = $up->getTalkPage();
1206 return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
1207 }
1208
1209
1210 /**
1211 * Perform a user_newtalk check on current slaves; if the memcached data
1212 * is funky we don't want newtalk state to get stuck on save, as that's
1213 * damn annoying.
1214 *
1215 * @param string $field
1216 * @param mixed $id
1217 * @return bool
1218 * @private
1219 */
1220 function checkNewtalk( $field, $id ) {
1221 $dbr = wfGetDB( DB_SLAVE );
1222 $ok = $dbr->selectField( 'user_newtalk', $field,
1223 array( $field => $id ), __METHOD__ );
1224 return $ok !== false;
1225 }
1226
1227 /**
1228 * Add or update the
1229 * @param string $field
1230 * @param mixed $id
1231 * @private
1232 */
1233 function updateNewtalk( $field, $id ) {
1234 if( $this->checkNewtalk( $field, $id ) ) {
1235 wfDebug( __METHOD__." already set ($field, $id), ignoring\n" );
1236 return false;
1237 }
1238 $dbw = wfGetDB( DB_MASTER );
1239 $dbw->insert( 'user_newtalk',
1240 array( $field => $id ),
1241 __METHOD__,
1242 'IGNORE' );
1243 wfDebug( __METHOD__.": set on ($field, $id)\n" );
1244 return true;
1245 }
1246
1247 /**
1248 * Clear the new messages flag for the given user
1249 * @param string $field
1250 * @param mixed $id
1251 * @private
1252 */
1253 function deleteNewtalk( $field, $id ) {
1254 if( !$this->checkNewtalk( $field, $id ) ) {
1255 wfDebug( __METHOD__.": already gone ($field, $id), ignoring\n" );
1256 return false;
1257 }
1258 $dbw = wfGetDB( DB_MASTER );
1259 $dbw->delete( 'user_newtalk',
1260 array( $field => $id ),
1261 __METHOD__ );
1262 wfDebug( __METHOD__.": killed on ($field, $id)\n" );
1263 return true;
1264 }
1265
1266 /**
1267 * Update the 'You have new messages!' status.
1268 * @param bool $val
1269 */
1270 function setNewtalk( $val ) {
1271 if( wfReadOnly() ) {
1272 return;
1273 }
1274
1275 $this->load();
1276 $this->mNewtalk = $val;
1277
1278 if( $this->isAnon() ) {
1279 $field = 'user_ip';
1280 $id = $this->getName();
1281 } else {
1282 $field = 'user_id';
1283 $id = $this->getId();
1284 }
1285
1286 if( $val ) {
1287 $changed = $this->updateNewtalk( $field, $id );
1288 } else {
1289 $changed = $this->deleteNewtalk( $field, $id );
1290 }
1291
1292 if( $changed ) {
1293 if( $this->isAnon() ) {
1294 // Anons have a separate memcached space, since
1295 // user records aren't kept for them.
1296 global $wgMemc;
1297 $key = wfMemcKey( 'newtalk', 'ip', $val );
1298 $wgMemc->set( $key, $val ? 1 : 0 );
1299 } else {
1300 if( $val ) {
1301 // Make sure the user page is watched, so a notification
1302 // will be sent out if enabled.
1303 $this->addWatch( $this->getTalkPage() );
1304 }
1305 }
1306 $this->invalidateCache();
1307 }
1308 }
1309
1310 /**
1311 * Generate a current or new-future timestamp to be stored in the
1312 * user_touched field when we update things.
1313 */
1314 private static function newTouchedTimestamp() {
1315 global $wgClockSkewFudge;
1316 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
1317 }
1318
1319 /**
1320 * Clear user data from memcached.
1321 * Use after applying fun updates to the database; caller's
1322 * responsibility to update user_touched if appropriate.
1323 *
1324 * Called implicitly from invalidateCache() and saveSettings().
1325 */
1326 private function clearSharedCache() {
1327 if( $this->mId ) {
1328 global $wgMemc;
1329 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
1330 }
1331 }
1332
1333 /**
1334 * Immediately touch the user data cache for this account.
1335 * Updates user_touched field, and removes account data from memcached
1336 * for reload on the next hit.
1337 */
1338 function invalidateCache() {
1339 $this->load();
1340 if( $this->mId ) {
1341 $this->mTouched = self::newTouchedTimestamp();
1342
1343 $dbw = wfGetDB( DB_MASTER );
1344 $dbw->update( 'user',
1345 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ),
1346 array( 'user_id' => $this->mId ),
1347 __METHOD__ );
1348
1349 $this->clearSharedCache();
1350 }
1351 }
1352
1353 function validateCache( $timestamp ) {
1354 $this->load();
1355 return ($timestamp >= $this->mTouched);
1356 }
1357
1358 /**
1359 * Encrypt a password.
1360 * It can eventuall salt a password @see User::addSalt()
1361 * @param string $p clear Password.
1362 * @return string Encrypted password.
1363 */
1364 function encryptPassword( $p ) {
1365 $this->load();
1366 return wfEncryptPassword( $this->mId, $p );
1367 }
1368
1369 /**
1370 * Set the password and reset the random token
1371 * Calls through to authentication plugin if necessary;
1372 * will have no effect if the auth plugin refuses to
1373 * pass the change through or if the legal password
1374 * checks fail.
1375 *
1376 * As a special case, setting the password to null
1377 * wipes it, so the account cannot be logged in until
1378 * a new password is set, for instance via e-mail.
1379 *
1380 * @param string $str
1381 * @throws PasswordError on failure
1382 */
1383 function setPassword( $str ) {
1384 global $wgAuth;
1385
1386 if( $str !== null ) {
1387 if( !$wgAuth->allowPasswordChange() ) {
1388 throw new PasswordError( wfMsg( 'password-change-forbidden' ) );
1389 }
1390
1391 if( !$this->isValidPassword( $str ) ) {
1392 global $wgMinimalPasswordLength;
1393 throw new PasswordError( wfMsg( 'passwordtooshort',
1394 $wgMinimalPasswordLength ) );
1395 }
1396 }
1397
1398 if( !$wgAuth->setPassword( $this, $str ) ) {
1399 throw new PasswordError( wfMsg( 'externaldberror' ) );
1400 }
1401
1402 $this->setInternalPassword( $str );
1403
1404 return true;
1405 }
1406
1407 /**
1408 * Set the password and reset the random token no matter
1409 * what.
1410 *
1411 * @param string $str
1412 */
1413 function setInternalPassword( $str ) {
1414 $this->load();
1415 $this->setToken();
1416
1417 if( $str === null ) {
1418 // Save an invalid hash...
1419 $this->mPassword = '';
1420 } else {
1421 $this->mPassword = $this->encryptPassword( $str );
1422 }
1423 $this->mNewpassword = '';
1424 $this->mNewpassTime = null;
1425 }
1426 /**
1427 * Set the random token (used for persistent authentication)
1428 * Called from loadDefaults() among other places.
1429 * @private
1430 */
1431 function setToken( $token = false ) {
1432 global $wgSecretKey, $wgProxyKey;
1433 $this->load();
1434 if ( !$token ) {
1435 if ( $wgSecretKey ) {
1436 $key = $wgSecretKey;
1437 } elseif ( $wgProxyKey ) {
1438 $key = $wgProxyKey;
1439 } else {
1440 $key = microtime();
1441 }
1442 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
1443 } else {
1444 $this->mToken = $token;
1445 }
1446 }
1447
1448 function setCookiePassword( $str ) {
1449 $this->load();
1450 $this->mCookiePassword = md5( $str );
1451 }
1452
1453 /**
1454 * Set the password for a password reminder or new account email
1455 * Sets the user_newpass_time field if $throttle is true
1456 */
1457 function setNewpassword( $str, $throttle = true ) {
1458 $this->load();
1459 $this->mNewpassword = $this->encryptPassword( $str );
1460 if ( $throttle ) {
1461 $this->mNewpassTime = wfTimestampNow();
1462 }
1463 }
1464
1465 /**
1466 * Returns true if a password reminder email has already been sent within
1467 * the last $wgPasswordReminderResendTime hours
1468 */
1469 function isPasswordReminderThrottled() {
1470 global $wgPasswordReminderResendTime;
1471 $this->load();
1472 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
1473 return false;
1474 }
1475 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
1476 return time() < $expiry;
1477 }
1478
1479 function getEmail() {
1480 $this->load();
1481 return $this->mEmail;
1482 }
1483
1484 function getEmailAuthenticationTimestamp() {
1485 $this->load();
1486 return $this->mEmailAuthenticated;
1487 }
1488
1489 function setEmail( $str ) {
1490 $this->load();
1491 $this->mEmail = $str;
1492 }
1493
1494 function getRealName() {
1495 $this->load();
1496 return $this->mRealName;
1497 }
1498
1499 function setRealName( $str ) {
1500 $this->load();
1501 $this->mRealName = $str;
1502 }
1503
1504 /**
1505 * @param string $oname The option to check
1506 * @param string $defaultOverride A default value returned if the option does not exist
1507 * @return string
1508 */
1509 function getOption( $oname, $defaultOverride = '' ) {
1510 $this->load();
1511
1512 if ( is_null( $this->mOptions ) ) {
1513 if($defaultOverride != '') {
1514 return $defaultOverride;
1515 }
1516 $this->mOptions = User::getDefaultOptions();
1517 }
1518
1519 if ( array_key_exists( $oname, $this->mOptions ) ) {
1520 return trim( $this->mOptions[$oname] );
1521 } else {
1522 return $defaultOverride;
1523 }
1524 }
1525
1526 /**
1527 * Get the user's date preference, including some important migration for
1528 * old user rows.
1529 */
1530 function getDatePreference() {
1531 if ( is_null( $this->mDatePreference ) ) {
1532 global $wgLang;
1533 $value = $this->getOption( 'date' );
1534 $map = $wgLang->getDatePreferenceMigrationMap();
1535 if ( isset( $map[$value] ) ) {
1536 $value = $map[$value];
1537 }
1538 $this->mDatePreference = $value;
1539 }
1540 return $this->mDatePreference;
1541 }
1542
1543 /**
1544 * @param string $oname The option to check
1545 * @return bool False if the option is not selected, true if it is
1546 */
1547 function getBoolOption( $oname ) {
1548 return (bool)$this->getOption( $oname );
1549 }
1550
1551 /**
1552 * Get an option as an integer value from the source string.
1553 * @param string $oname The option to check
1554 * @param int $default Optional value to return if option is unset/blank.
1555 * @return int
1556 */
1557 function getIntOption( $oname, $default=0 ) {
1558 $val = $this->getOption( $oname );
1559 if( $val == '' ) {
1560 $val = $default;
1561 }
1562 return intval( $val );
1563 }
1564
1565 function setOption( $oname, $val ) {
1566 $this->load();
1567 if ( is_null( $this->mOptions ) ) {
1568 $this->mOptions = User::getDefaultOptions();
1569 }
1570 if ( $oname == 'skin' ) {
1571 # Clear cached skin, so the new one displays immediately in Special:Preferences
1572 unset( $this->mSkin );
1573 }
1574 // Filter out any newlines that may have passed through input validation.
1575 // Newlines are used to separate items in the options blob.
1576 $val = str_replace( "\r\n", "\n", $val );
1577 $val = str_replace( "\r", "\n", $val );
1578 $val = str_replace( "\n", " ", $val );
1579 $this->mOptions[$oname] = $val;
1580 }
1581
1582 function getRights() {
1583 if ( is_null( $this->mRights ) ) {
1584 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
1585 }
1586 return $this->mRights;
1587 }
1588
1589 /**
1590 * Get the list of explicit group memberships this user has.
1591 * The implicit * and user groups are not included.
1592 * @return array of strings
1593 */
1594 function getGroups() {
1595 $this->load();
1596 return $this->mGroups;
1597 }
1598
1599 /**
1600 * Get the list of implicit group memberships this user has.
1601 * This includes all explicit groups, plus 'user' if logged in
1602 * and '*' for all accounts.
1603 * @param boolean $recache Don't use the cache
1604 * @return array of strings
1605 */
1606 function getEffectiveGroups( $recache = false ) {
1607 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
1608 $this->load();
1609 $this->mEffectiveGroups = $this->mGroups;
1610 $this->mEffectiveGroups[] = '*';
1611 if( $this->mId ) {
1612 $this->mEffectiveGroups[] = 'user';
1613
1614 global $wgAutoConfirmAge, $wgAutoConfirmCount;
1615
1616 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
1617 if( $accountAge >= $wgAutoConfirmAge && $this->getEditCount() >= $wgAutoConfirmCount ) {
1618 $this->mEffectiveGroups[] = 'autoconfirmed';
1619 }
1620 # Implicit group for users whose email addresses are confirmed
1621 global $wgEmailAuthentication;
1622 if( self::isValidEmailAddr( $this->mEmail ) ) {
1623 if( $wgEmailAuthentication ) {
1624 if( $this->mEmailAuthenticated )
1625 $this->mEffectiveGroups[] = 'emailconfirmed';
1626 } else {
1627 $this->mEffectiveGroups[] = 'emailconfirmed';
1628 }
1629 }
1630 }
1631 }
1632 return $this->mEffectiveGroups;
1633 }
1634
1635 /* Return the edit count for the user. This is where User::edits should have been */
1636 function getEditCount() {
1637 if ($this->mId) {
1638 if ( !isset( $this->mEditCount ) ) {
1639 /* Populate the count, if it has not been populated yet */
1640 $this->mEditCount = User::edits($this->mId);
1641 }
1642 return $this->mEditCount;
1643 } else {
1644 /* nil */
1645 return null;
1646 }
1647 }
1648
1649 /**
1650 * Add the user to the given group.
1651 * This takes immediate effect.
1652 * @param string $group
1653 */
1654 function addGroup( $group ) {
1655 $this->load();
1656 $dbw = wfGetDB( DB_MASTER );
1657 if( $this->getId() ) {
1658 $dbw->insert( 'user_groups',
1659 array(
1660 'ug_user' => $this->getID(),
1661 'ug_group' => $group,
1662 ),
1663 'User::addGroup',
1664 array( 'IGNORE' ) );
1665 }
1666
1667 $this->mGroups[] = $group;
1668 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1669
1670 $this->invalidateCache();
1671 }
1672
1673 /**
1674 * Remove the user from the given group.
1675 * This takes immediate effect.
1676 * @param string $group
1677 */
1678 function removeGroup( $group ) {
1679 $this->load();
1680 $dbw = wfGetDB( DB_MASTER );
1681 $dbw->delete( 'user_groups',
1682 array(
1683 'ug_user' => $this->getID(),
1684 'ug_group' => $group,
1685 ),
1686 'User::removeGroup' );
1687
1688 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1689 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
1690
1691 $this->invalidateCache();
1692 }
1693
1694
1695 /**
1696 * A more legible check for non-anonymousness.
1697 * Returns true if the user is not an anonymous visitor.
1698 *
1699 * @return bool
1700 */
1701 function isLoggedIn() {
1702 return( $this->getID() != 0 );
1703 }
1704
1705 /**
1706 * A more legible check for anonymousness.
1707 * Returns true if the user is an anonymous visitor.
1708 *
1709 * @return bool
1710 */
1711 function isAnon() {
1712 return !$this->isLoggedIn();
1713 }
1714
1715 /**
1716 * Whether the user is a bot
1717 * @deprecated
1718 */
1719 function isBot() {
1720 return $this->isAllowed( 'bot' );
1721 }
1722
1723 /**
1724 * Check if user is allowed to access a feature / make an action
1725 * @param string $action Action to be checked
1726 * @return boolean True: action is allowed, False: action should not be allowed
1727 */
1728 function isAllowed($action='') {
1729 if ( $action === '' )
1730 // In the spirit of DWIM
1731 return true;
1732
1733 return in_array( $action, $this->getRights() );
1734 }
1735
1736 /**
1737 * Load a skin if it doesn't exist or return it
1738 * @todo FIXME : need to check the old failback system [AV]
1739 */
1740 function &getSkin() {
1741 global $wgRequest;
1742 if ( ! isset( $this->mSkin ) ) {
1743 wfProfileIn( __METHOD__ );
1744
1745 # get the user skin
1746 $userSkin = $this->getOption( 'skin' );
1747 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1748
1749 $this->mSkin =& Skin::newFromKey( $userSkin );
1750 wfProfileOut( __METHOD__ );
1751 }
1752 return $this->mSkin;
1753 }
1754
1755 /**#@+
1756 * @param string $title Article title to look at
1757 */
1758
1759 /**
1760 * Check watched status of an article
1761 * @return bool True if article is watched
1762 */
1763 function isWatched( $title ) {
1764 $wl = WatchedItem::fromUserTitle( $this, $title );
1765 return $wl->isWatched();
1766 }
1767
1768 /**
1769 * Watch an article
1770 */
1771 function addWatch( $title ) {
1772 $wl = WatchedItem::fromUserTitle( $this, $title );
1773 $wl->addWatch();
1774 $this->invalidateCache();
1775 }
1776
1777 /**
1778 * Stop watching an article
1779 */
1780 function removeWatch( $title ) {
1781 $wl = WatchedItem::fromUserTitle( $this, $title );
1782 $wl->removeWatch();
1783 $this->invalidateCache();
1784 }
1785
1786 /**
1787 * Clear the user's notification timestamp for the given title.
1788 * If e-notif e-mails are on, they will receive notification mails on
1789 * the next change of the page if it's watched etc.
1790 */
1791 function clearNotification( &$title ) {
1792 global $wgUser, $wgUseEnotif;
1793
1794 # Do nothing if the database is locked to writes
1795 if( wfReadOnly() ) {
1796 return;
1797 }
1798
1799 if ($title->getNamespace() == NS_USER_TALK &&
1800 $title->getText() == $this->getName() ) {
1801 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1802 return;
1803 $this->setNewtalk( false );
1804 }
1805
1806 if( !$wgUseEnotif ) {
1807 return;
1808 }
1809
1810 if( $this->isAnon() ) {
1811 // Nothing else to do...
1812 return;
1813 }
1814
1815 // Only update the timestamp if the page is being watched.
1816 // The query to find out if it is watched is cached both in memcached and per-invocation,
1817 // and when it does have to be executed, it can be on a slave
1818 // If this is the user's newtalk page, we always update the timestamp
1819 if ($title->getNamespace() == NS_USER_TALK &&
1820 $title->getText() == $wgUser->getName())
1821 {
1822 $watched = true;
1823 } elseif ( $this->getID() == $wgUser->getID() ) {
1824 $watched = $title->userIsWatching();
1825 } else {
1826 $watched = true;
1827 }
1828
1829 // If the page is watched by the user (or may be watched), update the timestamp on any
1830 // any matching rows
1831 if ( $watched ) {
1832 $dbw = wfGetDB( DB_MASTER );
1833 $dbw->update( 'watchlist',
1834 array( /* SET */
1835 'wl_notificationtimestamp' => NULL
1836 ), array( /* WHERE */
1837 'wl_title' => $title->getDBkey(),
1838 'wl_namespace' => $title->getNamespace(),
1839 'wl_user' => $this->getID()
1840 ), 'User::clearLastVisited'
1841 );
1842 }
1843 }
1844
1845 /**#@-*/
1846
1847 /**
1848 * Resets all of the given user's page-change notification timestamps.
1849 * If e-notif e-mails are on, they will receive notification mails on
1850 * the next change of any watched page.
1851 *
1852 * @param int $currentUser user ID number
1853 * @public
1854 */
1855 function clearAllNotifications( $currentUser ) {
1856 global $wgUseEnotif;
1857 if ( !$wgUseEnotif ) {
1858 $this->setNewtalk( false );
1859 return;
1860 }
1861 if( $currentUser != 0 ) {
1862
1863 $dbw = wfGetDB( DB_MASTER );
1864 $dbw->update( 'watchlist',
1865 array( /* SET */
1866 'wl_notificationtimestamp' => NULL
1867 ), array( /* WHERE */
1868 'wl_user' => $currentUser
1869 ), 'UserMailer::clearAll'
1870 );
1871
1872 # we also need to clear here the "you have new message" notification for the own user_talk page
1873 # This is cleared one page view later in Article::viewUpdates();
1874 }
1875 }
1876
1877 /**
1878 * @private
1879 * @return string Encoding options
1880 */
1881 function encodeOptions() {
1882 $this->load();
1883 if ( is_null( $this->mOptions ) ) {
1884 $this->mOptions = User::getDefaultOptions();
1885 }
1886 $a = array();
1887 foreach ( $this->mOptions as $oname => $oval ) {
1888 array_push( $a, $oname.'='.$oval );
1889 }
1890 $s = implode( "\n", $a );
1891 return $s;
1892 }
1893
1894 /**
1895 * @private
1896 */
1897 function decodeOptions( $str ) {
1898 $this->mOptions = array();
1899 $a = explode( "\n", $str );
1900 foreach ( $a as $s ) {
1901 $m = array();
1902 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1903 $this->mOptions[$m[1]] = $m[2];
1904 }
1905 }
1906 }
1907
1908 function setCookies() {
1909 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1910 $this->load();
1911 if ( 0 == $this->mId ) return;
1912 $exp = time() + $wgCookieExpiration;
1913
1914 $_SESSION['wsUserID'] = $this->mId;
1915 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1916
1917 $_SESSION['wsUserName'] = $this->getName();
1918 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1919
1920 $_SESSION['wsToken'] = $this->mToken;
1921 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1922 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1923 } else {
1924 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1925 }
1926 }
1927
1928 /**
1929 * Logout user
1930 * Clears the cookies and session, resets the instance cache
1931 */
1932 function logout() {
1933 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1934 $this->clearInstanceCache( 'defaults' );
1935
1936 $_SESSION['wsUserID'] = 0;
1937
1938 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1939 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1940
1941 # Remember when user logged out, to prevent seeing cached pages
1942 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1943 }
1944
1945 /**
1946 * Save object settings into database
1947 * @todo Only rarely do all these fields need to be set!
1948 */
1949 function saveSettings() {
1950 $this->load();
1951 if ( wfReadOnly() ) { return; }
1952 if ( 0 == $this->mId ) { return; }
1953
1954 $this->mTouched = self::newTouchedTimestamp();
1955
1956 $dbw = wfGetDB( DB_MASTER );
1957 $dbw->update( 'user',
1958 array( /* SET */
1959 'user_name' => $this->mName,
1960 'user_password' => $this->mPassword,
1961 'user_newpassword' => $this->mNewpassword,
1962 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
1963 'user_real_name' => $this->mRealName,
1964 'user_email' => $this->mEmail,
1965 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1966 'user_options' => $this->encodeOptions(),
1967 'user_touched' => $dbw->timestamp($this->mTouched),
1968 'user_token' => $this->mToken
1969 ), array( /* WHERE */
1970 'user_id' => $this->mId
1971 ), __METHOD__
1972 );
1973 $this->clearSharedCache();
1974 }
1975
1976
1977 /**
1978 * Checks if a user with the given name exists, returns the ID
1979 */
1980 function idForName() {
1981 $s = trim( $this->getName() );
1982 if ( 0 == strcmp( '', $s ) ) return 0;
1983
1984 $dbr = wfGetDB( DB_SLAVE );
1985 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
1986 if ( $id === false ) {
1987 $id = 0;
1988 }
1989 return $id;
1990 }
1991
1992 /**
1993 * Add a user to the database, return the user object
1994 *
1995 * @param string $name The user's name
1996 * @param array $params Associative array of non-default parameters to save to the database:
1997 * password The user's password. Password logins will be disabled if this is omitted.
1998 * newpassword A temporary password mailed to the user
1999 * email The user's email address
2000 * email_authenticated The email authentication timestamp
2001 * real_name The user's real name
2002 * options An associative array of non-default options
2003 * token Random authentication token. Do not set.
2004 * registration Registration timestamp. Do not set.
2005 *
2006 * @return User object, or null if the username already exists
2007 */
2008 static function createNew( $name, $params = array() ) {
2009 $user = new User;
2010 $user->load();
2011 if ( isset( $params['options'] ) ) {
2012 $user->mOptions = $params['options'] + $user->mOptions;
2013 unset( $params['options'] );
2014 }
2015 $dbw = wfGetDB( DB_MASTER );
2016 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2017 $fields = array(
2018 'user_id' => $seqVal,
2019 'user_name' => $name,
2020 'user_password' => $user->mPassword,
2021 'user_newpassword' => $user->mNewpassword,
2022 'user_newpass_time' => $dbw->timestamp( $user->mNewpassTime ),
2023 'user_email' => $user->mEmail,
2024 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
2025 'user_real_name' => $user->mRealName,
2026 'user_options' => $user->encodeOptions(),
2027 'user_token' => $user->mToken,
2028 'user_registration' => $dbw->timestamp( $user->mRegistration ),
2029 'user_editcount' => 0,
2030 );
2031 foreach ( $params as $name => $value ) {
2032 $fields["user_$name"] = $value;
2033 }
2034 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
2035 if ( $dbw->affectedRows() ) {
2036 $newUser = User::newFromId( $dbw->insertId() );
2037 } else {
2038 $newUser = null;
2039 }
2040 return $newUser;
2041 }
2042
2043 /**
2044 * Add an existing user object to the database
2045 */
2046 function addToDatabase() {
2047 $this->load();
2048 $dbw = wfGetDB( DB_MASTER );
2049 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
2050 $dbw->insert( 'user',
2051 array(
2052 'user_id' => $seqVal,
2053 'user_name' => $this->mName,
2054 'user_password' => $this->mPassword,
2055 'user_newpassword' => $this->mNewpassword,
2056 'user_newpass_time' => $dbw->timestamp( $this->mNewpassTime ),
2057 'user_email' => $this->mEmail,
2058 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
2059 'user_real_name' => $this->mRealName,
2060 'user_options' => $this->encodeOptions(),
2061 'user_token' => $this->mToken,
2062 'user_registration' => $dbw->timestamp( $this->mRegistration ),
2063 'user_editcount' => 0,
2064 ), __METHOD__
2065 );
2066 $this->mId = $dbw->insertId();
2067
2068 # Clear instance cache other than user table data, which is already accurate
2069 $this->clearInstanceCache();
2070 }
2071
2072 /**
2073 * If the (non-anonymous) user is blocked, this function will block any IP address
2074 * that they successfully log on from.
2075 */
2076 function spreadBlock() {
2077 wfDebug( __METHOD__."()\n" );
2078 $this->load();
2079 if ( $this->mId == 0 ) {
2080 return;
2081 }
2082
2083 $userblock = Block::newFromDB( '', $this->mId );
2084 if ( !$userblock ) {
2085 return;
2086 }
2087
2088 $userblock->doAutoblock( wfGetIp() );
2089
2090 }
2091
2092 /**
2093 * Generate a string which will be different for any combination of
2094 * user options which would produce different parser output.
2095 * This will be used as part of the hash key for the parser cache,
2096 * so users will the same options can share the same cached data
2097 * safely.
2098 *
2099 * Extensions which require it should install 'PageRenderingHash' hook,
2100 * which will give them a chance to modify this key based on their own
2101 * settings.
2102 *
2103 * @return string
2104 */
2105 function getPageRenderingHash() {
2106 global $wgContLang, $wgUseDynamicDates, $wgLang;
2107 if( $this->mHash ){
2108 return $this->mHash;
2109 }
2110
2111 // stubthreshold is only included below for completeness,
2112 // it will always be 0 when this function is called by parsercache.
2113
2114 $confstr = $this->getOption( 'math' );
2115 $confstr .= '!' . $this->getOption( 'stubthreshold' );
2116 if ( $wgUseDynamicDates ) {
2117 $confstr .= '!' . $this->getDatePreference();
2118 }
2119 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
2120 $confstr .= '!' . $wgLang->getCode();
2121 $confstr .= '!' . $this->getOption( 'thumbsize' );
2122 // add in language specific options, if any
2123 $extra = $wgContLang->getExtraHashOptions();
2124 $confstr .= $extra;
2125
2126 // Give a chance for extensions to modify the hash, if they have
2127 // extra options or other effects on the parser cache.
2128 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
2129
2130 $this->mHash = $confstr;
2131 return $confstr;
2132 }
2133
2134 function isBlockedFromCreateAccount() {
2135 $this->getBlockedStatus();
2136 return $this->mBlock && $this->mBlock->mCreateAccount;
2137 }
2138
2139 function isAllowedToCreateAccount() {
2140 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
2141 }
2142
2143 /**
2144 * @deprecated
2145 */
2146 function setLoaded( $loaded ) {}
2147
2148 /**
2149 * Get this user's personal page title.
2150 *
2151 * @return Title
2152 * @public
2153 */
2154 function getUserPage() {
2155 return Title::makeTitle( NS_USER, $this->getName() );
2156 }
2157
2158 /**
2159 * Get this user's talk page title.
2160 *
2161 * @return Title
2162 * @public
2163 */
2164 function getTalkPage() {
2165 $title = $this->getUserPage();
2166 return $title->getTalkPage();
2167 }
2168
2169 /**
2170 * @static
2171 */
2172 function getMaxID() {
2173 static $res; // cache
2174
2175 if ( isset( $res ) )
2176 return $res;
2177 else {
2178 $dbr = wfGetDB( DB_SLAVE );
2179 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
2180 }
2181 }
2182
2183 /**
2184 * Determine whether the user is a newbie. Newbies are either
2185 * anonymous IPs, or the most recently created accounts.
2186 * @return bool True if it is a newbie.
2187 */
2188 function isNewbie() {
2189 return !$this->isAllowed( 'autoconfirmed' );
2190 }
2191
2192 /**
2193 * Check to see if the given clear-text password is one of the accepted passwords
2194 * @param string $password User password.
2195 * @return bool True if the given password is correct otherwise False.
2196 */
2197 function checkPassword( $password ) {
2198 global $wgAuth;
2199 $this->load();
2200
2201 // Even though we stop people from creating passwords that
2202 // are shorter than this, doesn't mean people wont be able
2203 // to. Certain authentication plugins do NOT want to save
2204 // domain passwords in a mysql database, so we should
2205 // check this (incase $wgAuth->strict() is false).
2206 if( !$this->isValidPassword( $password ) ) {
2207 return false;
2208 }
2209
2210 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
2211 return true;
2212 } elseif( $wgAuth->strict() ) {
2213 /* Auth plugin doesn't allow local authentication */
2214 return false;
2215 }
2216 $ep = $this->encryptPassword( $password );
2217 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
2218 return true;
2219 } elseif ( function_exists( 'iconv' ) ) {
2220 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
2221 # Check for this with iconv
2222 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ) );
2223 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
2224 return true;
2225 }
2226 }
2227 return false;
2228 }
2229
2230 /**
2231 * Check if the given clear-text password matches the temporary password
2232 * sent by e-mail for password reset operations.
2233 * @return bool
2234 */
2235 function checkTemporaryPassword( $plaintext ) {
2236 $hash = $this->encryptPassword( $plaintext );
2237 return $hash === $this->mNewpassword;
2238 }
2239
2240 /**
2241 * Initialize (if necessary) and return a session token value
2242 * which can be used in edit forms to show that the user's
2243 * login credentials aren't being hijacked with a foreign form
2244 * submission.
2245 *
2246 * @param mixed $salt - Optional function-specific data for hash.
2247 * Use a string or an array of strings.
2248 * @return string
2249 * @public
2250 */
2251 function editToken( $salt = '' ) {
2252 if( !isset( $_SESSION['wsEditToken'] ) ) {
2253 $token = $this->generateToken();
2254 $_SESSION['wsEditToken'] = $token;
2255 } else {
2256 $token = $_SESSION['wsEditToken'];
2257 }
2258 if( is_array( $salt ) ) {
2259 $salt = implode( '|', $salt );
2260 }
2261 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
2262 }
2263
2264 /**
2265 * Generate a hex-y looking random token for various uses.
2266 * Could be made more cryptographically sure if someone cares.
2267 * @return string
2268 */
2269 function generateToken( $salt = '' ) {
2270 $token = dechex( mt_rand() ) . dechex( mt_rand() );
2271 return md5( $token . $salt );
2272 }
2273
2274 /**
2275 * Check given value against the token value stored in the session.
2276 * A match should confirm that the form was submitted from the
2277 * user's own login session, not a form submission from a third-party
2278 * site.
2279 *
2280 * @param string $val - the input value to compare
2281 * @param string $salt - Optional function-specific data for hash
2282 * @return bool
2283 * @public
2284 */
2285 function matchEditToken( $val, $salt = '' ) {
2286 global $wgMemc;
2287 $sessionToken = $this->editToken( $salt );
2288 if ( $val != $sessionToken ) {
2289 wfDebug( "User::matchEditToken: broken session data\n" );
2290 }
2291 return $val == $sessionToken;
2292 }
2293
2294 /**
2295 * Generate a new e-mail confirmation token and send a confirmation
2296 * mail to the user's given address.
2297 *
2298 * @return mixed True on success, a WikiError object on failure.
2299 */
2300 function sendConfirmationMail() {
2301 global $wgContLang;
2302 $expiration = null; // gets passed-by-ref and defined in next line.
2303 $url = $this->confirmationTokenUrl( $expiration );
2304 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
2305 wfMsg( 'confirmemail_body',
2306 wfGetIP(),
2307 $this->getName(),
2308 $url,
2309 $wgContLang->timeanddate( $expiration, false ) ) );
2310 }
2311
2312 /**
2313 * Send an e-mail to this user's account. Does not check for
2314 * confirmed status or validity.
2315 *
2316 * @param string $subject
2317 * @param string $body
2318 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
2319 * @return mixed True on success, a WikiError object on failure.
2320 */
2321 function sendMail( $subject, $body, $from = null ) {
2322 if( is_null( $from ) ) {
2323 global $wgPasswordSender;
2324 $from = $wgPasswordSender;
2325 }
2326
2327 require_once( 'UserMailer.php' );
2328 $to = new MailAddress( $this );
2329 $sender = new MailAddress( $from );
2330 $error = userMailer( $to, $sender, $subject, $body );
2331
2332 if( $error == '' ) {
2333 return true;
2334 } else {
2335 return new WikiError( $error );
2336 }
2337 }
2338
2339 /**
2340 * Generate, store, and return a new e-mail confirmation code.
2341 * A hash (unsalted since it's used as a key) is stored.
2342 * @param &$expiration mixed output: accepts the expiration time
2343 * @return string
2344 * @private
2345 */
2346 function confirmationToken( &$expiration ) {
2347 $now = time();
2348 $expires = $now + 7 * 24 * 60 * 60;
2349 $expiration = wfTimestamp( TS_MW, $expires );
2350
2351 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
2352 $hash = md5( $token );
2353
2354 $dbw = wfGetDB( DB_MASTER );
2355 $dbw->update( 'user',
2356 array( 'user_email_token' => $hash,
2357 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
2358 array( 'user_id' => $this->mId ),
2359 __METHOD__ );
2360
2361 return $token;
2362 }
2363
2364 /**
2365 * Generate and store a new e-mail confirmation token, and return
2366 * the URL the user can use to confirm.
2367 * @param &$expiration mixed output: accepts the expiration time
2368 * @return string
2369 * @private
2370 */
2371 function confirmationTokenUrl( &$expiration ) {
2372 $token = $this->confirmationToken( $expiration );
2373 $title = SpecialPage::getTitleFor( 'Confirmemail', $token );
2374 return $title->getFullUrl();
2375 }
2376
2377 /**
2378 * Mark the e-mail address confirmed and save.
2379 */
2380 function confirmEmail() {
2381 $this->load();
2382 $this->mEmailAuthenticated = wfTimestampNow();
2383 $this->saveSettings();
2384 return true;
2385 }
2386
2387 /**
2388 * Is this user allowed to send e-mails within limits of current
2389 * site configuration?
2390 * @return bool
2391 */
2392 function canSendEmail() {
2393 return $this->isEmailConfirmed();
2394 }
2395
2396 /**
2397 * Is this user allowed to receive e-mails within limits of current
2398 * site configuration?
2399 * @return bool
2400 */
2401 function canReceiveEmail() {
2402 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
2403 }
2404
2405 /**
2406 * Is this user's e-mail address valid-looking and confirmed within
2407 * limits of the current site configuration?
2408 *
2409 * If $wgEmailAuthentication is on, this may require the user to have
2410 * confirmed their address by returning a code or using a password
2411 * sent to the address from the wiki.
2412 *
2413 * @return bool
2414 */
2415 function isEmailConfirmed() {
2416 global $wgEmailAuthentication;
2417 $this->load();
2418 $confirmed = true;
2419 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2420 if( $this->isAnon() )
2421 return false;
2422 if( !self::isValidEmailAddr( $this->mEmail ) )
2423 return false;
2424 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2425 return false;
2426 return true;
2427 } else {
2428 return $confirmed;
2429 }
2430 }
2431
2432 /**
2433 * Return true if there is an outstanding request for e-mail confirmation.
2434 * @return bool
2435 */
2436 function isEmailConfirmationPending() {
2437 global $wgEmailAuthentication;
2438 return $wgEmailAuthentication &&
2439 !$this->isEmailConfirmed() &&
2440 $this->mEmailToken &&
2441 $this->mEmailTokenExpires > wfTimestamp();
2442 }
2443
2444 /**
2445 * @param array $groups list of groups
2446 * @return array list of permission key names for given groups combined
2447 * @static
2448 */
2449 static function getGroupPermissions( $groups ) {
2450 global $wgGroupPermissions;
2451 $rights = array();
2452 foreach( $groups as $group ) {
2453 if( isset( $wgGroupPermissions[$group] ) ) {
2454 $rights = array_merge( $rights,
2455 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2456 }
2457 }
2458 return $rights;
2459 }
2460
2461 /**
2462 * @param string $group key name
2463 * @return string localized descriptive name for group, if provided
2464 * @static
2465 */
2466 static function getGroupName( $group ) {
2467 MessageCache::loadAllMessages();
2468 $key = "group-$group";
2469 $name = wfMsg( $key );
2470 return $name == '' || wfEmptyMsg( $key, $name )
2471 ? $group
2472 : $name;
2473 }
2474
2475 /**
2476 * @param string $group key name
2477 * @return string localized descriptive name for member of a group, if provided
2478 * @static
2479 */
2480 static function getGroupMember( $group ) {
2481 MessageCache::loadAllMessages();
2482 $key = "group-$group-member";
2483 $name = wfMsg( $key );
2484 return $name == '' || wfEmptyMsg( $key, $name )
2485 ? $group
2486 : $name;
2487 }
2488
2489 /**
2490 * Return the set of defined explicit groups.
2491 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2492 * groups are not included, as they are defined
2493 * automatically, not in the database.
2494 * @return array
2495 * @static
2496 */
2497 static function getAllGroups() {
2498 global $wgGroupPermissions;
2499 return array_diff(
2500 array_keys( $wgGroupPermissions ),
2501 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
2502 }
2503
2504 /**
2505 * Get the title of a page describing a particular group
2506 *
2507 * @param $group Name of the group
2508 * @return mixed
2509 */
2510 static function getGroupPage( $group ) {
2511 MessageCache::loadAllMessages();
2512 $page = wfMsgForContent( 'grouppage-' . $group );
2513 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2514 $title = Title::newFromText( $page );
2515 if( is_object( $title ) )
2516 return $title;
2517 }
2518 return false;
2519 }
2520
2521 /**
2522 * Create a link to the group in HTML, if available
2523 *
2524 * @param $group Name of the group
2525 * @param $text The text of the link
2526 * @return mixed
2527 */
2528 static function makeGroupLinkHTML( $group, $text = '' ) {
2529 if( $text == '' ) {
2530 $text = self::getGroupName( $group );
2531 }
2532 $title = self::getGroupPage( $group );
2533 if( $title ) {
2534 global $wgUser;
2535 $sk = $wgUser->getSkin();
2536 return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
2537 } else {
2538 return $text;
2539 }
2540 }
2541
2542 /**
2543 * Create a link to the group in Wikitext, if available
2544 *
2545 * @param $group Name of the group
2546 * @param $text The text of the link (by default, the name of the group)
2547 * @return mixed
2548 */
2549 static function makeGroupLinkWiki( $group, $text = '' ) {
2550 if( $text == '' ) {
2551 $text = self::getGroupName( $group );
2552 }
2553 $title = self::getGroupPage( $group );
2554 if( $title ) {
2555 $page = $title->getPrefixedText();
2556 return "[[$page|$text]]";
2557 } else {
2558 return $text;
2559 }
2560 }
2561
2562 /**
2563 * Increment the user's edit-count field.
2564 * Will have no effect for anonymous users.
2565 */
2566 function incEditCount() {
2567 if( !$this->isAnon() ) {
2568 $dbw = wfGetDB( DB_MASTER );
2569 $dbw->update( 'user',
2570 array( 'user_editcount=user_editcount+1' ),
2571 array( 'user_id' => $this->getId() ),
2572 __METHOD__ );
2573
2574 // Lazy initialization check...
2575 if( $dbw->affectedRows() == 0 ) {
2576 // Pull from a slave to be less cruel to servers
2577 // Accuracy isn't the point anyway here
2578 $dbr = wfGetDB( DB_SLAVE );
2579 $count = $dbr->selectField( 'revision',
2580 'COUNT(rev_user)',
2581 array( 'rev_user' => $this->getId() ),
2582 __METHOD__ );
2583
2584 // Now here's a goddamn hack...
2585 if( $dbr !== $dbw ) {
2586 // If we actually have a slave server, the count is
2587 // at least one behind because the current transaction
2588 // has not been committed and replicated.
2589 $count++;
2590 } else {
2591 // But if DB_SLAVE is selecting the master, then the
2592 // count we just read includes the revision that was
2593 // just added in the working transaction.
2594 }
2595
2596 $dbw->update( 'user',
2597 array( 'user_editcount' => $count ),
2598 array( 'user_id' => $this->getId() ),
2599 __METHOD__ );
2600 }
2601 }
2602 // edit count in user cache too
2603 $this->invalidateCache();
2604 }
2605 }
2606
2607 ?>