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