Defer loading default options, this avoids a theoretical unstub loop.
[lhc/web/wiklou.git] / includes / User.php
1 <?php
2 /**
3 * See user.txt
4 *
5 * @package MediaWiki
6 */
7
8 # Number of characters in user_token field
9 define( 'USER_TOKEN_LENGTH', 32 );
10
11 # Serialized record version
12 define( 'MW_USER_VERSION', 3 );
13
14 /**
15 *
16 * @package MediaWiki
17 */
18 class User {
19 /*
20 * When adding a new private variable, dont forget to add it to __sleep()
21 */
22 /**@{{
23 * @private
24 */
25 var $mBlockedby; //!<
26 var $mBlockreason; //!<
27 var $mBlock; //!<
28 var $mDataLoaded; //!<
29 var $mEmail; //!<
30 var $mEmailAuthenticated; //!<
31 var $mGroups; //!<
32 var $mHash; //!<
33 var $mId; //!<
34 var $mName; //!<
35 var $mNewpassword; //!<
36 var $mNewtalk; //!<
37 var $mOptions; //!<
38 var $mPassword; //!<
39 var $mRealName; //!<
40 var $mRegistration; //!<
41 var $mRights; //!<
42 var $mSkin; //!<
43 var $mToken; //!<
44 var $mTouched; //!<
45 var $mDatePreference; // !<
46 var $mVersion; //!< serialized version
47 /**@}} */
48
49 /**
50 * Default user options
51 * To change this array at runtime, use a UserDefaultOptions hook
52 */
53 static public $mDefaultOptions = array(
54 'quickbar' => 1,
55 'underline' => 2,
56 'cols' => 80,
57 'rows' => 25,
58 'searchlimit' => 20,
59 'contextlines' => 5,
60 'contextchars' => 50,
61 'skin' => false,
62 'math' => 1,
63 'rcdays' => 7,
64 'rclimit' => 50,
65 'wllimit' => 250,
66 'highlightbroken' => 1,
67 'stubthreshold' => 0,
68 'previewontop' => 1,
69 'editsection' => 1,
70 'editsectiononrightclick'=> 0,
71 'showtoc' => 1,
72 'showtoolbar' => 1,
73 'date' => 'default',
74 'imagesize' => 2,
75 'thumbsize' => 2,
76 'rememberpassword' => 0,
77 'enotifwatchlistpages' => 0,
78 'enotifusertalkpages' => 1,
79 'enotifminoredits' => 0,
80 'enotifrevealaddr' => 0,
81 'shownumberswatching' => 1,
82 'fancysig' => 0,
83 'externaleditor' => 0,
84 'externaldiff' => 0,
85 'showjumplinks' => 1,
86 'numberheadings' => 0,
87 'uselivepreview' => 0,
88 'watchlistdays' => 3.0,
89 );
90
91 static public $mToggles = array(
92 'highlightbroken',
93 'justify',
94 'hideminor',
95 'extendwatchlist',
96 'usenewrc',
97 'numberheadings',
98 'showtoolbar',
99 'editondblclick',
100 'editsection',
101 'editsectiononrightclick',
102 'showtoc',
103 'rememberpassword',
104 'editwidth',
105 'watchcreations',
106 'watchdefault',
107 'minordefault',
108 'previewontop',
109 'previewonfirst',
110 'nocache',
111 'enotifwatchlistpages',
112 'enotifusertalkpages',
113 'enotifminoredits',
114 'enotifrevealaddr',
115 'shownumberswatching',
116 'fancysig',
117 'externaleditor',
118 'externaldiff',
119 'showjumplinks',
120 'uselivepreview',
121 'autopatrol',
122 'forceeditsummary',
123 'watchlisthideown',
124 'watchlisthidebots',
125 );
126
127 /** Constructor using User:loadDefaults() */
128 function User() {
129 $this->loadDefaults();
130 $this->mVersion = MW_USER_VERSION;
131 }
132
133 /**
134 * Static factory method
135 * @param string $name Username, validated by Title:newFromText()
136 * @param bool $validate Validate username
137 * @return User
138 * @static
139 */
140 function newFromName( $name, $validate = true ) {
141 # Force usernames to capital
142 global $wgContLang;
143 $name = $wgContLang->ucfirst( $name );
144
145 # Clean up name according to title rules
146 $t = Title::newFromText( $name );
147 if( is_null( $t ) ) {
148 return null;
149 }
150
151 # Reject various classes of invalid names
152 $canonicalName = $t->getText();
153 global $wgAuth;
154 $canonicalName = $wgAuth->getCanonicalName( $t->getText() );
155
156 if( $validate && !User::isValidUserName( $canonicalName ) ) {
157 return null;
158 }
159
160 $u = new User();
161 $u->setName( $canonicalName );
162 $u->setId( $u->idFromName( $canonicalName ) );
163 return $u;
164 }
165
166 /**
167 * Factory method to fetch whichever use has a given email confirmation code.
168 * This code is generated when an account is created or its e-mail address
169 * has changed.
170 *
171 * If the code is invalid or has expired, returns NULL.
172 *
173 * @param string $code
174 * @return User
175 * @static
176 */
177 function newFromConfirmationCode( $code ) {
178 $dbr =& wfGetDB( DB_SLAVE );
179 $name = $dbr->selectField( 'user', 'user_name', array(
180 'user_email_token' => md5( $code ),
181 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
182 ) );
183 if( is_string( $name ) ) {
184 return User::newFromName( $name );
185 } else {
186 return null;
187 }
188 }
189
190 /**
191 * Serialze sleep function, for better cache efficiency and avoidance of
192 * silly "incomplete type" errors when skins are cached. The array should
193 * contain names of private variables (see at top of User.php).
194 */
195 function __sleep() {
196 return array(
197 'mDataLoaded',
198 'mEmail',
199 'mEmailAuthenticated',
200 'mGroups',
201 'mHash',
202 'mId',
203 'mName',
204 'mNewpassword',
205 'mNewtalk',
206 'mOptions',
207 'mPassword',
208 'mRealName',
209 'mRegistration',
210 'mRights',
211 'mToken',
212 'mTouched',
213 'mVersion',
214 );
215 }
216
217 /**
218 * Get username given an id.
219 * @param integer $id Database user id
220 * @return string Nickname of a user
221 * @static
222 */
223 function whoIs( $id ) {
224 $dbr =& wfGetDB( DB_SLAVE );
225 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
226 }
227
228 /**
229 * Get real username given an id.
230 * @param integer $id Database user id
231 * @return string Realname of a user
232 * @static
233 */
234 function whoIsReal( $id ) {
235 $dbr =& wfGetDB( DB_SLAVE );
236 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
237 }
238
239 /**
240 * Get database id given a user name
241 * @param string $name Nickname of a user
242 * @return integer|null Database user id (null: if non existent
243 * @static
244 */
245 function idFromName( $name ) {
246 $fname = "User::idFromName";
247
248 $nt = Title::newFromText( $name );
249 if( is_null( $nt ) ) {
250 # Illegal name
251 return null;
252 }
253 $dbr =& wfGetDB( DB_SLAVE );
254 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
255
256 if ( $s === false ) {
257 return 0;
258 } else {
259 return $s->user_id;
260 }
261 }
262
263 /**
264 * Does the string match an anonymous IPv4 address?
265 *
266 * This function exists for username validation, in order to reject
267 * usernames which are similar in form to IP addresses. Strings such
268 * as 300.300.300.300 will return true because it looks like an IP
269 * address, despite not being strictly valid.
270 *
271 * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
272 * address because the usemod software would "cloak" anonymous IP
273 * addresses like this, if we allowed accounts like this to be created
274 * new users could get the old edits of these anonymous users.
275 *
276 * @bug 3631
277 *
278 * @static
279 * @param string $name Nickname of a user
280 * @return bool
281 */
282 function isIP( $name ) {
283 return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/",$name);
284 /*return preg_match("/^
285 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
286 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
287 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
288 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
289 $/x", $name);*/
290 }
291
292 /**
293 * Is the input a valid username?
294 *
295 * Checks if the input is a valid username, we don't want an empty string,
296 * an IP address, anything that containins slashes (would mess up subpages),
297 * is longer than the maximum allowed username size or doesn't begin with
298 * a capital letter.
299 *
300 * @param string $name
301 * @return bool
302 * @static
303 */
304 function isValidUserName( $name ) {
305 global $wgContLang, $wgMaxNameChars;
306
307 if ( $name == ''
308 || User::isIP( $name )
309 || strpos( $name, '/' ) !== false
310 || strlen( $name ) > $wgMaxNameChars
311 || $name != $wgContLang->ucfirst( $name ) )
312 return false;
313
314 // Ensure that the name can't be misresolved as a different title,
315 // such as with extra namespace keys at the start.
316 $parsed = Title::newFromText( $name );
317 if( is_null( $parsed )
318 || $parsed->getNamespace()
319 || strcmp( $name, $parsed->getPrefixedText() ) )
320 return false;
321
322 // Check an additional blacklist of troublemaker characters.
323 // Should these be merged into the title char list?
324 $unicodeBlacklist = '/[' .
325 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
326 '\x{00a0}' . # non-breaking space
327 '\x{2000}-\x{200f}' . # various whitespace
328 '\x{2028}-\x{202f}' . # breaks and control chars
329 '\x{3000}' . # ideographic space
330 '\x{e000}-\x{f8ff}' . # private use
331 ']/u';
332 if( preg_match( $unicodeBlacklist, $name ) ) {
333 return false;
334 }
335
336 return true;
337 }
338
339 /**
340 * Is the input a valid password?
341 *
342 * @param string $password
343 * @return bool
344 * @static
345 */
346 function isValidPassword( $password ) {
347 global $wgMinimalPasswordLength;
348 return strlen( $password ) >= $wgMinimalPasswordLength;
349 }
350
351 /**
352 * Does the string match roughly an email address ?
353 *
354 * There used to be a regular expression here, it got removed because it
355 * rejected valid addresses. Actually just check if there is '@' somewhere
356 * in the given address.
357 *
358 * @todo Check for RFC 2822 compilance
359 * @bug 959
360 *
361 * @param string $addr email address
362 * @static
363 * @return bool
364 */
365 function isValidEmailAddr ( $addr ) {
366 return ( trim( $addr ) != '' ) &&
367 (false !== strpos( $addr, '@' ) );
368 }
369
370 /**
371 * Count the number of edits of a user
372 *
373 * @param int $uid The user ID to check
374 * @return int
375 */
376 function edits( $uid ) {
377 $fname = 'User::edits';
378
379 $dbr =& wfGetDB( DB_SLAVE );
380 return $dbr->selectField(
381 'revision', 'count(*)',
382 array( 'rev_user' => $uid ),
383 $fname
384 );
385 }
386
387 /**
388 * probably return a random password
389 * @return string probably a random password
390 * @static
391 * @todo Check what is doing really [AV]
392 */
393 function randomPassword() {
394 global $wgMinimalPasswordLength;
395 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
396 $l = strlen( $pwchars ) - 1;
397
398 $pwlength = max( 7, $wgMinimalPasswordLength );
399 $digit = mt_rand(0, $pwlength - 1);
400 $np = '';
401 for ( $i = 0; $i < $pwlength; $i++ ) {
402 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
403 }
404 return $np;
405 }
406
407 /**
408 * Set properties to default
409 * Used at construction. It will load per language default settings only
410 * if we have an available language object.
411 */
412 function loadDefaults() {
413 static $n=0;
414 $n++;
415 $fname = 'User::loadDefaults' . $n;
416 wfProfileIn( $fname );
417
418 global $wgCookiePrefix;
419 global $wgNamespacesToBeSearchedDefault;
420
421 $this->mId = 0;
422 $this->mNewtalk = -1;
423 $this->mName = false;
424 $this->mRealName = $this->mEmail = '';
425 $this->mEmailAuthenticated = null;
426 $this->mPassword = $this->mNewpassword = '';
427 $this->mRights = array();
428 $this->mGroups = array();
429 $this->mOptions = null;
430 $this->mDatePreference = null;
431
432 unset( $this->mSkin );
433 $this->mDataLoaded = false;
434 $this->mBlockedby = -1; # Unset
435 $this->setToken(); # Random
436 $this->mHash = false;
437
438 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
439 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
440 }
441 else {
442 $this->mTouched = '0'; # Allow any pages to be cached
443 }
444
445 $this->mRegistration = wfTimestamp( TS_MW );
446
447 wfProfileOut( $fname );
448 }
449
450 /**
451 * Combine the language default options with any site-specific options
452 * and add the default language variants.
453 *
454 * @return array
455 * @static
456 * @private
457 */
458 function getDefaultOptions() {
459 global $wgNamespacesToBeSearchedDefault;
460 /**
461 * Site defaults will override the global/language defaults
462 */
463 global $wgContLang;
464 $defOpt = self::$mDefaultOptions + $wgContLang->getDefaultUserOptionOverrides();
465
466 /**
467 * default language setting
468 */
469 $variant = $wgContLang->getPreferredVariant( false );
470 $defOpt['variant'] = $variant;
471 $defOpt['language'] = $variant;
472
473 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
474 $defOpt['searchNs'.$nsnum] = $val;
475 }
476 return $defOpt;
477 }
478
479 /**
480 * Get a given default option value.
481 *
482 * @param string $opt
483 * @return string
484 * @static
485 * @public
486 */
487 function getDefaultOption( $opt ) {
488 $defOpts = User::getDefaultOptions();
489 if( isset( $defOpts[$opt] ) ) {
490 return $defOpts[$opt];
491 } else {
492 return '';
493 }
494 }
495
496 /**
497 * Get a list of user toggle names
498 * @return array
499 */
500 static function getToggles() {
501 global $wgContLang;
502 $extraToggles = array();
503 wfRunHooks( 'UserToggles', array( &$extraToggles ) );
504 return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() );
505 }
506
507
508 /**
509 * Get blocking information
510 * @private
511 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
512 * non-critical checks are done against slaves. Check when actually saving should be done against
513 * master.
514 */
515 function getBlockedStatus( $bFromSlave = true ) {
516 global $wgEnableSorbs, $wgProxyWhitelist;
517
518 if ( -1 != $this->mBlockedby ) {
519 wfDebug( "User::getBlockedStatus: already loaded.\n" );
520 return;
521 }
522
523 $fname = 'User::getBlockedStatus';
524 wfProfileIn( $fname );
525 wfDebug( "$fname: checking...\n" );
526
527 $this->mBlockedby = 0;
528 $ip = wfGetIP();
529
530 # User/IP blocking
531 $this->mBlock = new Block();
532 $this->mBlock->fromMaster( !$bFromSlave );
533 if ( $this->mBlock->load( $ip , $this->mId ) ) {
534 wfDebug( "$fname: Found block.\n" );
535 $this->mBlockedby = $this->mBlock->mBy;
536 $this->mBlockreason = $this->mBlock->mReason;
537 if ( $this->isLoggedIn() ) {
538 $this->spreadBlock();
539 }
540 } else {
541 $this->mBlock = null;
542 wfDebug( "$fname: No block.\n" );
543 }
544
545 # Proxy blocking
546 # FIXME ? proxyunbannable is to deprecate the old isSysop()
547 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
548
549 # Local list
550 if ( wfIsLocallyBlockedProxy( $ip ) ) {
551 $this->mBlockedby = wfMsg( 'proxyblocker' );
552 $this->mBlockreason = wfMsg( 'proxyblockreason' );
553 }
554
555 # DNSBL
556 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
557 if ( $this->inSorbsBlacklist( $ip ) ) {
558 $this->mBlockedby = wfMsg( 'sorbs' );
559 $this->mBlockreason = wfMsg( 'sorbsreason' );
560 }
561 }
562 }
563
564 # Extensions
565 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
566
567 wfProfileOut( $fname );
568 }
569
570 function inSorbsBlacklist( $ip ) {
571 global $wgEnableSorbs;
572 return $wgEnableSorbs &&
573 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
574 }
575
576 function inDnsBlacklist( $ip, $base ) {
577 $fname = 'User::inDnsBlacklist';
578 wfProfileIn( $fname );
579
580 $found = false;
581 $host = '';
582
583 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
584 # Make hostname
585 for ( $i=4; $i>=1; $i-- ) {
586 $host .= $m[$i] . '.';
587 }
588 $host .= $base;
589
590 # Send query
591 $ipList = gethostbynamel( $host );
592
593 if ( $ipList ) {
594 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
595 $found = true;
596 } else {
597 wfDebug( "Requested $host, not found in $base.\n" );
598 }
599 }
600
601 wfProfileOut( $fname );
602 return $found;
603 }
604
605 /**
606 * Primitive rate limits: enforce maximum actions per time period
607 * to put a brake on flooding.
608 *
609 * Note: when using a shared cache like memcached, IP-address
610 * last-hit counters will be shared across wikis.
611 *
612 * @return bool true if a rate limiter was tripped
613 * @public
614 */
615 function pingLimiter( $action='edit' ) {
616 global $wgRateLimits, $wgRateLimitsExcludedGroups;
617 if( !isset( $wgRateLimits[$action] ) ) {
618 return false;
619 }
620
621 # Some groups shouldn't trigger the ping limiter, ever
622 foreach( $this->getGroups() as $group ) {
623 if( array_search( $group, $wgRateLimitsExcludedGroups ) !== false )
624 return false;
625 }
626
627 global $wgMemc, $wgDBname, $wgRateLimitLog;
628 $fname = 'User::pingLimiter';
629 wfProfileIn( $fname );
630
631 $limits = $wgRateLimits[$action];
632 $keys = array();
633 $id = $this->getId();
634 $ip = wfGetIP();
635
636 if( isset( $limits['anon'] ) && $id == 0 ) {
637 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
638 }
639
640 if( isset( $limits['user'] ) && $id != 0 ) {
641 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
642 }
643 if( $this->isNewbie() ) {
644 if( isset( $limits['newbie'] ) && $id != 0 ) {
645 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
646 }
647 if( isset( $limits['ip'] ) ) {
648 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
649 }
650 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
651 $subnet = $matches[1];
652 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
653 }
654 }
655
656 $triggered = false;
657 foreach( $keys as $key => $limit ) {
658 list( $max, $period ) = $limit;
659 $summary = "(limit $max in {$period}s)";
660 $count = $wgMemc->get( $key );
661 if( $count ) {
662 if( $count > $max ) {
663 wfDebug( "$fname: tripped! $key at $count $summary\n" );
664 if( $wgRateLimitLog ) {
665 @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
666 }
667 $triggered = true;
668 } else {
669 wfDebug( "$fname: ok. $key at $count $summary\n" );
670 }
671 } else {
672 wfDebug( "$fname: adding record for $key $summary\n" );
673 $wgMemc->add( $key, 1, intval( $period ) );
674 }
675 $wgMemc->incr( $key );
676 }
677
678 wfProfileOut( $fname );
679 return $triggered;
680 }
681
682 /**
683 * Check if user is blocked
684 * @return bool True if blocked, false otherwise
685 */
686 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
687 wfDebug( "User::isBlocked: enter\n" );
688 $this->getBlockedStatus( $bFromSlave );
689 return $this->mBlockedby !== 0;
690 }
691
692 /**
693 * Check if user is blocked from editing a particular article
694 */
695 function isBlockedFrom( $title, $bFromSlave = false ) {
696 global $wgBlockAllowsUTEdit;
697 $fname = 'User::isBlockedFrom';
698 wfProfileIn( $fname );
699 wfDebug( "$fname: enter\n" );
700
701 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
702 $title->getNamespace() == NS_USER_TALK )
703 {
704 $blocked = false;
705 wfDebug( "$fname: self-talk page, ignoring any blocks\n" );
706 } else {
707 wfDebug( "$fname: asking isBlocked()\n" );
708 $blocked = $this->isBlocked( $bFromSlave );
709 }
710 wfProfileOut( $fname );
711 return $blocked;
712 }
713
714 /**
715 * Get name of blocker
716 * @return string name of blocker
717 */
718 function blockedBy() {
719 $this->getBlockedStatus();
720 return $this->mBlockedby;
721 }
722
723 /**
724 * Get blocking reason
725 * @return string Blocking reason
726 */
727 function blockedFor() {
728 $this->getBlockedStatus();
729 return $this->mBlockreason;
730 }
731
732 /**
733 * Initialise php session
734 * @deprecated use wfSetupSession()
735 */
736 function SetupSession() {
737 wfSetupSession();
738 }
739
740 /**
741 * Create a new user object using data from session
742 * @static
743 */
744 function loadFromSession() {
745 global $wgMemc, $wgDBname, $wgCookiePrefix;
746
747 if ( isset( $_SESSION['wsUserID'] ) ) {
748 if ( 0 != $_SESSION['wsUserID'] ) {
749 $sId = $_SESSION['wsUserID'];
750 } else {
751 return new User();
752 }
753 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
754 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
755 $_SESSION['wsUserID'] = $sId;
756 } else {
757 return new User();
758 }
759 if ( isset( $_SESSION['wsUserName'] ) ) {
760 $sName = $_SESSION['wsUserName'];
761 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
762 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
763 $_SESSION['wsUserName'] = $sName;
764 } else {
765 return new User();
766 }
767
768 $passwordCorrect = FALSE;
769 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
770 if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) {
771 # Expire old serialized objects; they may be corrupt.
772 $user = false;
773 }
774 if($makenew = !$user) {
775 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
776 $user = new User();
777 $user->mId = $sId;
778 $user->loadFromDatabase();
779 } else {
780 wfDebug( "User::loadFromSession() got from cache!\n" );
781 # Set block status to unloaded, that should be loaded every time
782 $user->mBlockedby = -1;
783 }
784
785 if ( isset( $_SESSION['wsToken'] ) ) {
786 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken;
787 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
788 $passwordCorrect = $user->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
789 } else {
790 return new User(); # Can't log in from session
791 }
792
793 if ( ( $sName == $user->mName ) && $passwordCorrect ) {
794 if($makenew) {
795 if($wgMemc->set( $key, $user ))
796 wfDebug( "User::loadFromSession() successfully saved user\n" );
797 else
798 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
799 }
800 return $user;
801 }
802 return new User(); # Can't log in from session
803 }
804
805 /**
806 * Load a user from the database
807 */
808 function loadFromDatabase() {
809 $fname = "User::loadFromDatabase";
810
811 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
812 # loading in a command line script, don't assume all command line scripts need it like this
813 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
814 if ( $this->mDataLoaded ) {
815 return;
816 }
817
818 # Paranoia
819 $this->mId = intval( $this->mId );
820
821 /** Anonymous user */
822 if( !$this->mId ) {
823 /** Get rights */
824 $this->mRights = $this->getGroupPermissions( array( '*' ) );
825 $this->mDataLoaded = true;
826 return;
827 } # the following stuff is for non-anonymous users only
828
829 $dbr =& wfGetDB( DB_SLAVE );
830 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
831 'user_email_authenticated',
832 'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ),
833 array( 'user_id' => $this->mId ), $fname );
834
835 if ( $s !== false ) {
836 $this->mName = $s->user_name;
837 $this->mEmail = $s->user_email;
838 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
839 $this->mRealName = $s->user_real_name;
840 $this->mPassword = $s->user_password;
841 $this->mNewpassword = $s->user_newpassword;
842 $this->decodeOptions( $s->user_options );
843 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
844 $this->mToken = $s->user_token;
845 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
846
847 $res = $dbr->select( 'user_groups',
848 array( 'ug_group' ),
849 array( 'ug_user' => $this->mId ),
850 $fname );
851 $this->mGroups = array();
852 while( $row = $dbr->fetchObject( $res ) ) {
853 $this->mGroups[] = $row->ug_group;
854 }
855 $implicitGroups = array( '*', 'user' );
856
857 global $wgAutoConfirmAge;
858 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
859 if( $accountAge >= $wgAutoConfirmAge ) {
860 $implicitGroups[] = 'autoconfirmed';
861 }
862
863 # Implicit group for users whose email addresses are confirmed
864 global $wgEmailAuthentication;
865 if( $this->isValidEmailAddr( $this->mEmail ) ) {
866 if( $wgEmailAuthentication ) {
867 if( $this->mEmailAuthenticated )
868 $implicitGroups[] = 'emailconfirmed';
869 } else {
870 $implicitGroups[] = 'emailconfirmed';
871 }
872 }
873
874 $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
875 $this->mRights = $this->getGroupPermissions( $effectiveGroups );
876 }
877
878 $this->mDataLoaded = true;
879 }
880
881 function getID() { return $this->mId; }
882 function setID( $v ) {
883 $this->mId = $v;
884 $this->mDataLoaded = false;
885 }
886
887 function getName() {
888 $this->loadFromDatabase();
889 if ( $this->mName === false ) {
890 $this->mName = wfGetIP();
891 }
892 return $this->mName;
893 }
894
895 function setName( $str ) {
896 $this->loadFromDatabase();
897 $this->mName = $str;
898 }
899
900
901 /**
902 * Return the title dbkey form of the name, for eg user pages.
903 * @return string
904 * @public
905 */
906 function getTitleKey() {
907 return str_replace( ' ', '_', $this->getName() );
908 }
909
910 function getNewtalk() {
911 $this->loadFromDatabase();
912
913 # Load the newtalk status if it is unloaded (mNewtalk=-1)
914 if( $this->mNewtalk === -1 ) {
915 $this->mNewtalk = false; # reset talk page status
916
917 # Check memcached separately for anons, who have no
918 # entire User object stored in there.
919 if( !$this->mId ) {
920 global $wgDBname, $wgMemc;
921 $key = "$wgDBname:newtalk:ip:" . $this->getName();
922 $newtalk = $wgMemc->get( $key );
923 if( is_integer( $newtalk ) ) {
924 $this->mNewtalk = (bool)$newtalk;
925 } else {
926 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
927 $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
928 }
929 } else {
930 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
931 }
932 }
933
934 return (bool)$this->mNewtalk;
935 }
936
937 /**
938 * Return the talk page(s) this user has new messages on.
939 */
940 function getNewMessageLinks() {
941 global $wgDBname;
942 $talks = array();
943 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
944 return $talks;
945
946 if (!$this->getNewtalk())
947 return array();
948 $up = $this->getUserPage();
949 $utp = $up->getTalkPage();
950 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
951 }
952
953
954 /**
955 * Perform a user_newtalk check on current slaves; if the memcached data
956 * is funky we don't want newtalk state to get stuck on save, as that's
957 * damn annoying.
958 *
959 * @param string $field
960 * @param mixed $id
961 * @return bool
962 * @private
963 */
964 function checkNewtalk( $field, $id ) {
965 $fname = 'User::checkNewtalk';
966 $dbr =& wfGetDB( DB_SLAVE );
967 $ok = $dbr->selectField( 'user_newtalk', $field,
968 array( $field => $id ), $fname );
969 return $ok !== false;
970 }
971
972 /**
973 * Add or update the
974 * @param string $field
975 * @param mixed $id
976 * @private
977 */
978 function updateNewtalk( $field, $id ) {
979 $fname = 'User::updateNewtalk';
980 if( $this->checkNewtalk( $field, $id ) ) {
981 wfDebug( "$fname already set ($field, $id), ignoring\n" );
982 return false;
983 }
984 $dbw =& wfGetDB( DB_MASTER );
985 $dbw->insert( 'user_newtalk',
986 array( $field => $id ),
987 $fname,
988 'IGNORE' );
989 wfDebug( "$fname: set on ($field, $id)\n" );
990 return true;
991 }
992
993 /**
994 * Clear the new messages flag for the given user
995 * @param string $field
996 * @param mixed $id
997 * @private
998 */
999 function deleteNewtalk( $field, $id ) {
1000 $fname = 'User::deleteNewtalk';
1001 if( !$this->checkNewtalk( $field, $id ) ) {
1002 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
1003 return false;
1004 }
1005 $dbw =& wfGetDB( DB_MASTER );
1006 $dbw->delete( 'user_newtalk',
1007 array( $field => $id ),
1008 $fname );
1009 wfDebug( "$fname: killed on ($field, $id)\n" );
1010 return true;
1011 }
1012
1013 /**
1014 * Update the 'You have new messages!' status.
1015 * @param bool $val
1016 */
1017 function setNewtalk( $val ) {
1018 if( wfReadOnly() ) {
1019 return;
1020 }
1021
1022 $this->loadFromDatabase();
1023 $this->mNewtalk = $val;
1024
1025 $fname = 'User::setNewtalk';
1026
1027 if( $this->isAnon() ) {
1028 $field = 'user_ip';
1029 $id = $this->getName();
1030 } else {
1031 $field = 'user_id';
1032 $id = $this->getId();
1033 }
1034
1035 if( $val ) {
1036 $changed = $this->updateNewtalk( $field, $id );
1037 } else {
1038 $changed = $this->deleteNewtalk( $field, $id );
1039 }
1040
1041 if( $changed ) {
1042 if( $this->isAnon() ) {
1043 // Anons have a separate memcached space, since
1044 // user records aren't kept for them.
1045 global $wgDBname, $wgMemc;
1046 $key = "$wgDBname:newtalk:ip:$val";
1047 $wgMemc->set( $key, $val ? 1 : 0 );
1048 } else {
1049 if( $val ) {
1050 // Make sure the user page is watched, so a notification
1051 // will be sent out if enabled.
1052 $this->addWatch( $this->getTalkPage() );
1053 }
1054 }
1055 $this->invalidateCache();
1056 $this->saveSettings();
1057 }
1058 }
1059
1060 function invalidateCache() {
1061 global $wgClockSkewFudge;
1062 $this->loadFromDatabase();
1063 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
1064 # Don't forget to save the options after this or
1065 # it won't take effect!
1066 }
1067
1068 function validateCache( $timestamp ) {
1069 $this->loadFromDatabase();
1070 return ($timestamp >= $this->mTouched);
1071 }
1072
1073 /**
1074 * Encrypt a password.
1075 * It can eventuall salt a password @see User::addSalt()
1076 * @param string $p clear Password.
1077 * @return string Encrypted password.
1078 */
1079 function encryptPassword( $p ) {
1080 return wfEncryptPassword( $this->mId, $p );
1081 }
1082
1083 # Set the password and reset the random token
1084 function setPassword( $str ) {
1085 $this->loadFromDatabase();
1086 $this->setToken();
1087 $this->mPassword = $this->encryptPassword( $str );
1088 $this->mNewpassword = '';
1089 }
1090
1091 # Set the random token (used for persistent authentication)
1092 function setToken( $token = false ) {
1093 global $wgSecretKey, $wgProxyKey, $wgDBname;
1094 if ( !$token ) {
1095 if ( $wgSecretKey ) {
1096 $key = $wgSecretKey;
1097 } elseif ( $wgProxyKey ) {
1098 $key = $wgProxyKey;
1099 } else {
1100 $key = microtime();
1101 }
1102 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1103 } else {
1104 $this->mToken = $token;
1105 }
1106 }
1107
1108
1109 function setCookiePassword( $str ) {
1110 $this->loadFromDatabase();
1111 $this->mCookiePassword = md5( $str );
1112 }
1113
1114 function setNewpassword( $str ) {
1115 $this->loadFromDatabase();
1116 $this->mNewpassword = $this->encryptPassword( $str );
1117 }
1118
1119 function getEmail() {
1120 $this->loadFromDatabase();
1121 return $this->mEmail;
1122 }
1123
1124 function getEmailAuthenticationTimestamp() {
1125 $this->loadFromDatabase();
1126 return $this->mEmailAuthenticated;
1127 }
1128
1129 function setEmail( $str ) {
1130 $this->loadFromDatabase();
1131 $this->mEmail = $str;
1132 }
1133
1134 function getRealName() {
1135 $this->loadFromDatabase();
1136 return $this->mRealName;
1137 }
1138
1139 function setRealName( $str ) {
1140 $this->loadFromDatabase();
1141 $this->mRealName = $str;
1142 }
1143
1144 /**
1145 * @param string $oname The option to check
1146 * @return string
1147 */
1148 function getOption( $oname ) {
1149 $this->loadFromDatabase();
1150 if ( is_null( $this->mOptions ) ) {
1151 $this->mOptions = User::getDefaultOptions();
1152 }
1153 if ( array_key_exists( $oname, $this->mOptions ) ) {
1154 return trim( $this->mOptions[$oname] );
1155 } else {
1156 return '';
1157 }
1158 }
1159
1160 /**
1161 * Get the user's date preference, including some important migration for
1162 * old user rows.
1163 */
1164 function getDatePreference() {
1165 if ( is_null( $this->mDatePreference ) ) {
1166 global $wgLang;
1167 $value = $this->getOption( 'date' );
1168 $map = $wgLang->getDatePreferenceMigrationMap();
1169 if ( isset( $map[$value] ) ) {
1170 $value = $map[$value];
1171 }
1172 $this->mDatePreference = $value;
1173 }
1174 return $this->mDatePreference;
1175 }
1176
1177 /**
1178 * @param string $oname The option to check
1179 * @return bool False if the option is not selected, true if it is
1180 */
1181 function getBoolOption( $oname ) {
1182 return (bool)$this->getOption( $oname );
1183 }
1184
1185 /**
1186 * Get an option as an integer value from the source string.
1187 * @param string $oname The option to check
1188 * @param int $default Optional value to return if option is unset/blank.
1189 * @return int
1190 */
1191 function getIntOption( $oname, $default=0 ) {
1192 $val = $this->getOption( $oname );
1193 if( $val == '' ) {
1194 $val = $default;
1195 }
1196 return intval( $val );
1197 }
1198
1199 function setOption( $oname, $val ) {
1200 $this->loadFromDatabase();
1201 if ( is_null( $this->mOptions ) ) {
1202 $this->mOptions = User::getDefaultOptions();
1203 }
1204 if ( $oname == 'skin' ) {
1205 # Clear cached skin, so the new one displays immediately in Special:Preferences
1206 unset( $this->mSkin );
1207 }
1208 // Filter out any newlines that may have passed through input validation.
1209 // Newlines are used to separate items in the options blob.
1210 $val = str_replace( "\r\n", "\n", $val );
1211 $val = str_replace( "\r", "\n", $val );
1212 $val = str_replace( "\n", " ", $val );
1213 $this->mOptions[$oname] = $val;
1214 $this->invalidateCache();
1215 }
1216
1217 function getRights() {
1218 $this->loadFromDatabase();
1219 return $this->mRights;
1220 }
1221
1222 /**
1223 * Get the list of explicit group memberships this user has.
1224 * The implicit * and user groups are not included.
1225 * @return array of strings
1226 */
1227 function getGroups() {
1228 $this->loadFromDatabase();
1229 return $this->mGroups;
1230 }
1231
1232 /**
1233 * Get the list of implicit group memberships this user has.
1234 * This includes all explicit groups, plus 'user' if logged in
1235 * and '*' for all accounts.
1236 * @return array of strings
1237 */
1238 function getEffectiveGroups() {
1239 $base = array( '*' );
1240 if( $this->isLoggedIn() ) {
1241 $base[] = 'user';
1242 }
1243 return array_merge( $base, $this->getGroups() );
1244 }
1245
1246 /**
1247 * Add the user to the given group.
1248 * This takes immediate effect.
1249 * @string $group
1250 */
1251 function addGroup( $group ) {
1252 $dbw =& wfGetDB( DB_MASTER );
1253 $dbw->insert( 'user_groups',
1254 array(
1255 'ug_user' => $this->getID(),
1256 'ug_group' => $group,
1257 ),
1258 'User::addGroup',
1259 array( 'IGNORE' ) );
1260
1261 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1262 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1263
1264 $this->invalidateCache();
1265 $this->saveSettings();
1266 }
1267
1268 /**
1269 * Remove the user from the given group.
1270 * This takes immediate effect.
1271 * @string $group
1272 */
1273 function removeGroup( $group ) {
1274 $dbw =& wfGetDB( DB_MASTER );
1275 $dbw->delete( 'user_groups',
1276 array(
1277 'ug_user' => $this->getID(),
1278 'ug_group' => $group,
1279 ),
1280 'User::removeGroup' );
1281
1282 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1283 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1284
1285 $this->invalidateCache();
1286 $this->saveSettings();
1287 }
1288
1289
1290 /**
1291 * A more legible check for non-anonymousness.
1292 * Returns true if the user is not an anonymous visitor.
1293 *
1294 * @return bool
1295 */
1296 function isLoggedIn() {
1297 return( $this->getID() != 0 );
1298 }
1299
1300 /**
1301 * A more legible check for anonymousness.
1302 * Returns true if the user is an anonymous visitor.
1303 *
1304 * @return bool
1305 */
1306 function isAnon() {
1307 return !$this->isLoggedIn();
1308 }
1309
1310 /**
1311 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1312 * @deprecated
1313 */
1314 function isSysop() {
1315 throw new MWException( "Call to deprecated (v1.7) User::isSysop() method\n" );
1316 }
1317
1318 /**
1319 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1320 * @deprecated
1321 */
1322 function isDeveloper() {
1323 throw new MWException( "Call to deprecated (v1.7) User::isDeveloper() method\n" );
1324 }
1325
1326 /**
1327 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1328 * @deprecated
1329 */
1330 function isBureaucrat() {
1331 throw new MWException( "Call to deprecated (v1.7) User::isBureaucrat() method\n" );
1332 }
1333
1334 /**
1335 * Whether the user is a bot
1336 * @deprecated
1337 */
1338 function isBot() {
1339 return $this->isAllowed( 'bot' );
1340 }
1341
1342 /**
1343 * Check if user is allowed to access a feature / make an action
1344 * @param string $action Action to be checked
1345 * @return boolean True: action is allowed, False: action should not be allowed
1346 */
1347 function isAllowed($action='') {
1348 if ( $action === '' )
1349 // In the spirit of DWIM
1350 return true;
1351
1352 $this->loadFromDatabase();
1353 return in_array( $action , $this->mRights );
1354 }
1355
1356 /**
1357 * Load a skin if it doesn't exist or return it
1358 * @todo FIXME : need to check the old failback system [AV]
1359 */
1360 function &getSkin() {
1361 global $IP, $wgRequest;
1362 if ( ! isset( $this->mSkin ) ) {
1363 $fname = 'User::getSkin';
1364 wfProfileIn( $fname );
1365
1366 # get the user skin
1367 $userSkin = $this->getOption( 'skin' );
1368 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1369
1370 $this->mSkin =& Skin::newFromKey( $userSkin );
1371 wfProfileOut( $fname );
1372 }
1373 return $this->mSkin;
1374 }
1375
1376 /**#@+
1377 * @param string $title Article title to look at
1378 */
1379
1380 /**
1381 * Check watched status of an article
1382 * @return bool True if article is watched
1383 */
1384 function isWatched( $title ) {
1385 $wl = WatchedItem::fromUserTitle( $this, $title );
1386 return $wl->isWatched();
1387 }
1388
1389 /**
1390 * Watch an article
1391 */
1392 function addWatch( $title ) {
1393 $wl = WatchedItem::fromUserTitle( $this, $title );
1394 $wl->addWatch();
1395 $this->invalidateCache();
1396 }
1397
1398 /**
1399 * Stop watching an article
1400 */
1401 function removeWatch( $title ) {
1402 $wl = WatchedItem::fromUserTitle( $this, $title );
1403 $wl->removeWatch();
1404 $this->invalidateCache();
1405 }
1406
1407 /**
1408 * Clear the user's notification timestamp for the given title.
1409 * If e-notif e-mails are on, they will receive notification mails on
1410 * the next change of the page if it's watched etc.
1411 */
1412 function clearNotification( &$title ) {
1413 global $wgUser, $wgUseEnotif;
1414
1415
1416 if ($title->getNamespace() == NS_USER_TALK &&
1417 $title->getText() == $this->getName() ) {
1418 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1419 return;
1420 $this->setNewtalk( false );
1421 }
1422
1423 if( !$wgUseEnotif ) {
1424 return;
1425 }
1426
1427 if( $this->isAnon() ) {
1428 // Nothing else to do...
1429 return;
1430 }
1431
1432 // Only update the timestamp if the page is being watched.
1433 // The query to find out if it is watched is cached both in memcached and per-invocation,
1434 // and when it does have to be executed, it can be on a slave
1435 // If this is the user's newtalk page, we always update the timestamp
1436 if ($title->getNamespace() == NS_USER_TALK &&
1437 $title->getText() == $wgUser->getName())
1438 {
1439 $watched = true;
1440 } elseif ( $this->getID() == $wgUser->getID() ) {
1441 $watched = $title->userIsWatching();
1442 } else {
1443 $watched = true;
1444 }
1445
1446 // If the page is watched by the user (or may be watched), update the timestamp on any
1447 // any matching rows
1448 if ( $watched ) {
1449 $dbw =& wfGetDB( DB_MASTER );
1450 $success = $dbw->update( 'watchlist',
1451 array( /* SET */
1452 'wl_notificationtimestamp' => NULL
1453 ), array( /* WHERE */
1454 'wl_title' => $title->getDBkey(),
1455 'wl_namespace' => $title->getNamespace(),
1456 'wl_user' => $this->getID()
1457 ), 'User::clearLastVisited'
1458 );
1459 }
1460 }
1461
1462 /**#@-*/
1463
1464 /**
1465 * Resets all of the given user's page-change notification timestamps.
1466 * If e-notif e-mails are on, they will receive notification mails on
1467 * the next change of any watched page.
1468 *
1469 * @param int $currentUser user ID number
1470 * @public
1471 */
1472 function clearAllNotifications( $currentUser ) {
1473 global $wgUseEnotif;
1474 if ( !$wgUseEnotif ) {
1475 $this->setNewtalk( false );
1476 return;
1477 }
1478 if( $currentUser != 0 ) {
1479
1480 $dbw =& wfGetDB( DB_MASTER );
1481 $success = $dbw->update( 'watchlist',
1482 array( /* SET */
1483 'wl_notificationtimestamp' => 0
1484 ), array( /* WHERE */
1485 'wl_user' => $currentUser
1486 ), 'UserMailer::clearAll'
1487 );
1488
1489 # we also need to clear here the "you have new message" notification for the own user_talk page
1490 # This is cleared one page view later in Article::viewUpdates();
1491 }
1492 }
1493
1494 /**
1495 * @private
1496 * @return string Encoding options
1497 */
1498 function encodeOptions() {
1499 if ( is_null( $this->mOptions ) ) {
1500 $this->mOptions = User::getDefaultOptions();
1501 }
1502 $a = array();
1503 foreach ( $this->mOptions as $oname => $oval ) {
1504 array_push( $a, $oname.'='.$oval );
1505 }
1506 $s = implode( "\n", $a );
1507 return $s;
1508 }
1509
1510 /**
1511 * @private
1512 */
1513 function decodeOptions( $str ) {
1514 global $wgLang;
1515
1516 $this->mOptions = array();
1517 $a = explode( "\n", $str );
1518 foreach ( $a as $s ) {
1519 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1520 $this->mOptions[$m[1]] = $m[2];
1521 }
1522 }
1523 }
1524
1525 function setCookies() {
1526 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1527 if ( 0 == $this->mId ) return;
1528 $this->loadFromDatabase();
1529 $exp = time() + $wgCookieExpiration;
1530
1531 $_SESSION['wsUserID'] = $this->mId;
1532 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1533
1534 $_SESSION['wsUserName'] = $this->getName();
1535 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1536
1537 $_SESSION['wsToken'] = $this->mToken;
1538 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1539 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1540 } else {
1541 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1542 }
1543 }
1544
1545 /**
1546 * Logout user
1547 * It will clean the session cookie
1548 */
1549 function logout() {
1550 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1551 $this->loadDefaults();
1552 $this->setLoaded( true );
1553
1554 $_SESSION['wsUserID'] = 0;
1555
1556 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1557 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1558
1559 # Remember when user logged out, to prevent seeing cached pages
1560 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1561 }
1562
1563 /**
1564 * Save object settings into database
1565 */
1566 function saveSettings() {
1567 global $wgMemc, $wgDBname;
1568 $fname = 'User::saveSettings';
1569
1570 if ( wfReadOnly() ) { return; }
1571 if ( 0 == $this->mId ) { return; }
1572
1573 $dbw =& wfGetDB( DB_MASTER );
1574 $dbw->update( 'user',
1575 array( /* SET */
1576 'user_name' => $this->mName,
1577 'user_password' => $this->mPassword,
1578 'user_newpassword' => $this->mNewpassword,
1579 'user_real_name' => $this->mRealName,
1580 'user_email' => $this->mEmail,
1581 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1582 'user_options' => $this->encodeOptions(),
1583 'user_touched' => $dbw->timestamp($this->mTouched),
1584 'user_token' => $this->mToken
1585 ), array( /* WHERE */
1586 'user_id' => $this->mId
1587 ), $fname
1588 );
1589 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1590 }
1591
1592
1593 /**
1594 * Checks if a user with the given name exists, returns the ID
1595 */
1596 function idForName() {
1597 $fname = 'User::idForName';
1598
1599 $gotid = 0;
1600 $s = trim( $this->getName() );
1601 if ( 0 == strcmp( '', $s ) ) return 0;
1602
1603 $dbr =& wfGetDB( DB_SLAVE );
1604 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1605 if ( $id === false ) {
1606 $id = 0;
1607 }
1608 return $id;
1609 }
1610
1611 /**
1612 * Add user object to the database
1613 */
1614 function addToDatabase() {
1615 $fname = 'User::addToDatabase';
1616 $dbw =& wfGetDB( DB_MASTER );
1617 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1618 $dbw->insert( 'user',
1619 array(
1620 'user_id' => $seqVal,
1621 'user_name' => $this->mName,
1622 'user_password' => $this->mPassword,
1623 'user_newpassword' => $this->mNewpassword,
1624 'user_email' => $this->mEmail,
1625 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1626 'user_real_name' => $this->mRealName,
1627 'user_options' => $this->encodeOptions(),
1628 'user_token' => $this->mToken,
1629 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1630 ), $fname
1631 );
1632 $this->mId = $dbw->insertId();
1633 }
1634
1635 function spreadBlock() {
1636 # If the (non-anonymous) user is blocked, this function will block any IP address
1637 # that they successfully log on from.
1638 $fname = 'User::spreadBlock';
1639
1640 wfDebug( "User:spreadBlock()\n" );
1641 if ( $this->mId == 0 ) {
1642 return;
1643 }
1644
1645 $userblock = Block::newFromDB( '', $this->mId );
1646 if ( !$userblock ) {
1647 return;
1648 }
1649
1650 # Check if this IP address is already blocked
1651 $ipblock = Block::newFromDB( wfGetIP() );
1652 if ( $ipblock ) {
1653 # If the user is already blocked. Then check if the autoblock would
1654 # excede the user block. If it would excede, then do nothing, else
1655 # prolong block time
1656 if ($userblock->mExpiry &&
1657 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1658 return;
1659 }
1660 # Just update the timestamp
1661 $ipblock->updateTimestamp();
1662 return;
1663 } else {
1664 $ipblock = new Block;
1665 }
1666
1667 # Make a new block object with the desired properties
1668 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1669 $ipblock->mAddress = wfGetIP();
1670 $ipblock->mUser = 0;
1671 $ipblock->mBy = $userblock->mBy;
1672 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1673 $ipblock->mTimestamp = wfTimestampNow();
1674 $ipblock->mAuto = 1;
1675 # If the user is already blocked with an expiry date, we don't
1676 # want to pile on top of that!
1677 if($userblock->mExpiry) {
1678 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1679 } else {
1680 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1681 }
1682
1683 # Insert it
1684 $ipblock->insert();
1685
1686 }
1687
1688 /**
1689 * Generate a string which will be different for any combination of
1690 * user options which would produce different parser output.
1691 * This will be used as part of the hash key for the parser cache,
1692 * so users will the same options can share the same cached data
1693 * safely.
1694 *
1695 * Extensions which require it should install 'PageRenderingHash' hook,
1696 * which will give them a chance to modify this key based on their own
1697 * settings.
1698 *
1699 * @return string
1700 */
1701 function getPageRenderingHash() {
1702 global $wgContLang, $wgUseDynamicDates;
1703 if( $this->mHash ){
1704 return $this->mHash;
1705 }
1706
1707 // stubthreshold is only included below for completeness,
1708 // it will always be 0 when this function is called by parsercache.
1709
1710 $confstr = $this->getOption( 'math' );
1711 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1712 if ( $wgUseDynamicDates ) {
1713 $confstr .= '!' . $this->getDatePreference();
1714 }
1715 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1716 $confstr .= '!' . $this->getOption( 'language' );
1717 $confstr .= '!' . $this->getOption( 'thumbsize' );
1718 // add in language specific options, if any
1719 $extra = $wgContLang->getExtraHashOptions();
1720 $confstr .= $extra;
1721
1722 // Give a chance for extensions to modify the hash, if they have
1723 // extra options or other effects on the parser cache.
1724 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1725
1726 $this->mHash = $confstr;
1727 return $confstr;
1728 }
1729
1730 function isBlockedFromCreateAccount() {
1731 $this->getBlockedStatus();
1732 return $this->mBlock && $this->mBlock->mCreateAccount;
1733 }
1734
1735 function isAllowedToCreateAccount() {
1736 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
1737 }
1738
1739 /**
1740 * Set mDataLoaded, return previous value
1741 * Use this to prevent DB access in command-line scripts or similar situations
1742 */
1743 function setLoaded( $loaded ) {
1744 return wfSetVar( $this->mDataLoaded, $loaded );
1745 }
1746
1747 /**
1748 * Get this user's personal page title.
1749 *
1750 * @return Title
1751 * @public
1752 */
1753 function getUserPage() {
1754 return Title::makeTitle( NS_USER, $this->getName() );
1755 }
1756
1757 /**
1758 * Get this user's talk page title.
1759 *
1760 * @return Title
1761 * @public
1762 */
1763 function getTalkPage() {
1764 $title = $this->getUserPage();
1765 return $title->getTalkPage();
1766 }
1767
1768 /**
1769 * @static
1770 */
1771 function getMaxID() {
1772 static $res; // cache
1773
1774 if ( isset( $res ) )
1775 return $res;
1776 else {
1777 $dbr =& wfGetDB( DB_SLAVE );
1778 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1779 }
1780 }
1781
1782 /**
1783 * Determine whether the user is a newbie. Newbies are either
1784 * anonymous IPs, or the most recently created accounts.
1785 * @return bool True if it is a newbie.
1786 */
1787 function isNewbie() {
1788 return !$this->isAllowed( 'autoconfirmed' );
1789 }
1790
1791 /**
1792 * Check to see if the given clear-text password is one of the accepted passwords
1793 * @param string $password User password.
1794 * @return bool True if the given password is correct otherwise False.
1795 */
1796 function checkPassword( $password ) {
1797 global $wgAuth, $wgMinimalPasswordLength;
1798 $this->loadFromDatabase();
1799
1800 // Even though we stop people from creating passwords that
1801 // are shorter than this, doesn't mean people wont be able
1802 // to. Certain authentication plugins do NOT want to save
1803 // domain passwords in a mysql database, so we should
1804 // check this (incase $wgAuth->strict() is false).
1805 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1806 return false;
1807 }
1808
1809 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1810 return true;
1811 } elseif( $wgAuth->strict() ) {
1812 /* Auth plugin doesn't allow local authentication */
1813 return false;
1814 }
1815 $ep = $this->encryptPassword( $password );
1816 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1817 return true;
1818 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1819 return true;
1820 } elseif ( function_exists( 'iconv' ) ) {
1821 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1822 # Check for this with iconv
1823 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1824 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1825 return true;
1826 }
1827 }
1828 return false;
1829 }
1830
1831 /**
1832 * Initialize (if necessary) and return a session token value
1833 * which can be used in edit forms to show that the user's
1834 * login credentials aren't being hijacked with a foreign form
1835 * submission.
1836 *
1837 * @param mixed $salt - Optional function-specific data for hash.
1838 * Use a string or an array of strings.
1839 * @return string
1840 * @public
1841 */
1842 function editToken( $salt = '' ) {
1843 if( !isset( $_SESSION['wsEditToken'] ) ) {
1844 $token = $this->generateToken();
1845 $_SESSION['wsEditToken'] = $token;
1846 } else {
1847 $token = $_SESSION['wsEditToken'];
1848 }
1849 if( is_array( $salt ) ) {
1850 $salt = implode( '|', $salt );
1851 }
1852 return md5( $token . $salt );
1853 }
1854
1855 /**
1856 * Generate a hex-y looking random token for various uses.
1857 * Could be made more cryptographically sure if someone cares.
1858 * @return string
1859 */
1860 function generateToken( $salt = '' ) {
1861 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1862 return md5( $token . $salt );
1863 }
1864
1865 /**
1866 * Check given value against the token value stored in the session.
1867 * A match should confirm that the form was submitted from the
1868 * user's own login session, not a form submission from a third-party
1869 * site.
1870 *
1871 * @param string $val - the input value to compare
1872 * @param string $salt - Optional function-specific data for hash
1873 * @return bool
1874 * @public
1875 */
1876 function matchEditToken( $val, $salt = '' ) {
1877 global $wgMemc;
1878 $sessionToken = $this->editToken( $salt );
1879 if ( $val != $sessionToken ) {
1880 wfDebug( "User::matchEditToken: broken session data\n" );
1881 }
1882 return $val == $sessionToken;
1883 }
1884
1885 /**
1886 * Generate a new e-mail confirmation token and send a confirmation
1887 * mail to the user's given address.
1888 *
1889 * @return mixed True on success, a WikiError object on failure.
1890 */
1891 function sendConfirmationMail() {
1892 global $wgContLang;
1893 $url = $this->confirmationTokenUrl( $expiration );
1894 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1895 wfMsg( 'confirmemail_body',
1896 wfGetIP(),
1897 $this->getName(),
1898 $url,
1899 $wgContLang->timeanddate( $expiration, false ) ) );
1900 }
1901
1902 /**
1903 * Send an e-mail to this user's account. Does not check for
1904 * confirmed status or validity.
1905 *
1906 * @param string $subject
1907 * @param string $body
1908 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1909 * @return mixed True on success, a WikiError object on failure.
1910 */
1911 function sendMail( $subject, $body, $from = null ) {
1912 if( is_null( $from ) ) {
1913 global $wgPasswordSender;
1914 $from = $wgPasswordSender;
1915 }
1916
1917 require_once( 'UserMailer.php' );
1918 $to = new MailAddress( $this );
1919 $sender = new MailAddress( $from );
1920 $error = userMailer( $to, $sender, $subject, $body );
1921
1922 if( $error == '' ) {
1923 return true;
1924 } else {
1925 return new WikiError( $error );
1926 }
1927 }
1928
1929 /**
1930 * Generate, store, and return a new e-mail confirmation code.
1931 * A hash (unsalted since it's used as a key) is stored.
1932 * @param &$expiration mixed output: accepts the expiration time
1933 * @return string
1934 * @private
1935 */
1936 function confirmationToken( &$expiration ) {
1937 $fname = 'User::confirmationToken';
1938
1939 $now = time();
1940 $expires = $now + 7 * 24 * 60 * 60;
1941 $expiration = wfTimestamp( TS_MW, $expires );
1942
1943 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1944 $hash = md5( $token );
1945
1946 $dbw =& wfGetDB( DB_MASTER );
1947 $dbw->update( 'user',
1948 array( 'user_email_token' => $hash,
1949 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1950 array( 'user_id' => $this->mId ),
1951 $fname );
1952
1953 return $token;
1954 }
1955
1956 /**
1957 * Generate and store a new e-mail confirmation token, and return
1958 * the URL the user can use to confirm.
1959 * @param &$expiration mixed output: accepts the expiration time
1960 * @return string
1961 * @private
1962 */
1963 function confirmationTokenUrl( &$expiration ) {
1964 $token = $this->confirmationToken( $expiration );
1965 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1966 return $title->getFullUrl();
1967 }
1968
1969 /**
1970 * Mark the e-mail address confirmed and save.
1971 */
1972 function confirmEmail() {
1973 $this->loadFromDatabase();
1974 $this->mEmailAuthenticated = wfTimestampNow();
1975 $this->saveSettings();
1976 return true;
1977 }
1978
1979 /**
1980 * Is this user allowed to send e-mails within limits of current
1981 * site configuration?
1982 * @return bool
1983 */
1984 function canSendEmail() {
1985 return $this->isEmailConfirmed();
1986 }
1987
1988 /**
1989 * Is this user allowed to receive e-mails within limits of current
1990 * site configuration?
1991 * @return bool
1992 */
1993 function canReceiveEmail() {
1994 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1995 }
1996
1997 /**
1998 * Is this user's e-mail address valid-looking and confirmed within
1999 * limits of the current site configuration?
2000 *
2001 * If $wgEmailAuthentication is on, this may require the user to have
2002 * confirmed their address by returning a code or using a password
2003 * sent to the address from the wiki.
2004 *
2005 * @return bool
2006 */
2007 function isEmailConfirmed() {
2008 global $wgEmailAuthentication;
2009 $this->loadFromDatabase();
2010 $confirmed = true;
2011 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2012 if( $this->isAnon() )
2013 return false;
2014 if( !$this->isValidEmailAddr( $this->mEmail ) )
2015 return false;
2016 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2017 return false;
2018 return true;
2019 } else {
2020 return $confirmed;
2021 }
2022 }
2023
2024 /**
2025 * @param array $groups list of groups
2026 * @return array list of permission key names for given groups combined
2027 * @static
2028 */
2029 function getGroupPermissions( $groups ) {
2030 global $wgGroupPermissions;
2031 $rights = array();
2032 foreach( $groups as $group ) {
2033 if( isset( $wgGroupPermissions[$group] ) ) {
2034 $rights = array_merge( $rights,
2035 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2036 }
2037 }
2038 return $rights;
2039 }
2040
2041 /**
2042 * @param string $group key name
2043 * @return string localized descriptive name for group, if provided
2044 * @static
2045 */
2046 function getGroupName( $group ) {
2047 $key = "group-$group";
2048 $name = wfMsg( $key );
2049 if( $name == '' || $name == "&lt;$key&gt;" ) {
2050 return $group;
2051 } else {
2052 return $name;
2053 }
2054 }
2055
2056 /**
2057 * @param string $group key name
2058 * @return string localized descriptive name for member of a group, if provided
2059 * @static
2060 */
2061 function getGroupMember( $group ) {
2062 $key = "group-$group-member";
2063 $name = wfMsg( $key );
2064 if( $name == '' || $name == "&lt;$key&gt;" ) {
2065 return $group;
2066 } else {
2067 return $name;
2068 }
2069 }
2070
2071
2072 /**
2073 * Return the set of defined explicit groups.
2074 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2075 * groups are not included, as they are defined
2076 * automatically, not in the database.
2077 * @return array
2078 * @static
2079 */
2080 function getAllGroups() {
2081 global $wgGroupPermissions;
2082 return array_diff(
2083 array_keys( $wgGroupPermissions ),
2084 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
2085 }
2086
2087 /**
2088 * Get the title of a page describing a particular group
2089 *
2090 * @param $group Name of the group
2091 * @return mixed
2092 */
2093 function getGroupPage( $group ) {
2094 $page = wfMsgForContent( 'grouppage-' . $group );
2095 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2096 $title = Title::newFromText( $page );
2097 if( is_object( $title ) )
2098 return $title;
2099 }
2100 return false;
2101 }
2102
2103
2104 }
2105
2106 ?>