* Non-working API to facilitate dev collaboration. Do not enable this yet in localset...
[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 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
547
548 # Local list
549 if ( wfIsLocallyBlockedProxy( $ip ) ) {
550 $this->mBlockedby = wfMsg( 'proxyblocker' );
551 $this->mBlockreason = wfMsg( 'proxyblockreason' );
552 }
553
554 # DNSBL
555 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
556 if ( $this->inSorbsBlacklist( $ip ) ) {
557 $this->mBlockedby = wfMsg( 'sorbs' );
558 $this->mBlockreason = wfMsg( 'sorbsreason' );
559 }
560 }
561 }
562
563 # Extensions
564 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
565
566 wfProfileOut( $fname );
567 }
568
569 function inSorbsBlacklist( $ip ) {
570 global $wgEnableSorbs;
571 return $wgEnableSorbs &&
572 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
573 }
574
575 function inDnsBlacklist( $ip, $base ) {
576 $fname = 'User::inDnsBlacklist';
577 wfProfileIn( $fname );
578
579 $found = false;
580 $host = '';
581
582 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
583 # Make hostname
584 for ( $i=4; $i>=1; $i-- ) {
585 $host .= $m[$i] . '.';
586 }
587 $host .= $base;
588
589 # Send query
590 $ipList = gethostbynamel( $host );
591
592 if ( $ipList ) {
593 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
594 $found = true;
595 } else {
596 wfDebug( "Requested $host, not found in $base.\n" );
597 }
598 }
599
600 wfProfileOut( $fname );
601 return $found;
602 }
603
604 /**
605 * Primitive rate limits: enforce maximum actions per time period
606 * to put a brake on flooding.
607 *
608 * Note: when using a shared cache like memcached, IP-address
609 * last-hit counters will be shared across wikis.
610 *
611 * @return bool true if a rate limiter was tripped
612 * @public
613 */
614 function pingLimiter( $action='edit' ) {
615 global $wgRateLimits, $wgRateLimitsExcludedGroups;
616 if( !isset( $wgRateLimits[$action] ) ) {
617 return false;
618 }
619
620 # Some groups shouldn't trigger the ping limiter, ever
621 foreach( $this->getGroups() as $group ) {
622 if( array_search( $group, $wgRateLimitsExcludedGroups ) !== false )
623 return false;
624 }
625
626 global $wgMemc, $wgDBname, $wgRateLimitLog;
627 $fname = 'User::pingLimiter';
628 wfProfileIn( $fname );
629
630 $limits = $wgRateLimits[$action];
631 $keys = array();
632 $id = $this->getId();
633 $ip = wfGetIP();
634
635 if( isset( $limits['anon'] ) && $id == 0 ) {
636 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
637 }
638
639 if( isset( $limits['user'] ) && $id != 0 ) {
640 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
641 }
642 if( $this->isNewbie() ) {
643 if( isset( $limits['newbie'] ) && $id != 0 ) {
644 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
645 }
646 if( isset( $limits['ip'] ) ) {
647 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
648 }
649 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
650 $subnet = $matches[1];
651 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
652 }
653 }
654
655 $triggered = false;
656 foreach( $keys as $key => $limit ) {
657 list( $max, $period ) = $limit;
658 $summary = "(limit $max in {$period}s)";
659 $count = $wgMemc->get( $key );
660 if( $count ) {
661 if( $count > $max ) {
662 wfDebug( "$fname: tripped! $key at $count $summary\n" );
663 if( $wgRateLimitLog ) {
664 @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
665 }
666 $triggered = true;
667 } else {
668 wfDebug( "$fname: ok. $key at $count $summary\n" );
669 }
670 } else {
671 wfDebug( "$fname: adding record for $key $summary\n" );
672 $wgMemc->add( $key, 1, intval( $period ) );
673 }
674 $wgMemc->incr( $key );
675 }
676
677 wfProfileOut( $fname );
678 return $triggered;
679 }
680
681 /**
682 * Check if user is blocked
683 * @return bool True if blocked, false otherwise
684 */
685 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
686 wfDebug( "User::isBlocked: enter\n" );
687 $this->getBlockedStatus( $bFromSlave );
688 return $this->mBlockedby !== 0;
689 }
690
691 /**
692 * Check if user is blocked from editing a particular article
693 */
694 function isBlockedFrom( $title, $bFromSlave = false ) {
695 global $wgBlockAllowsUTEdit;
696 $fname = 'User::isBlockedFrom';
697 wfProfileIn( $fname );
698 wfDebug( "$fname: enter\n" );
699
700 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
701 $title->getNamespace() == NS_USER_TALK )
702 {
703 $blocked = false;
704 wfDebug( "$fname: self-talk page, ignoring any blocks\n" );
705 } else {
706 wfDebug( "$fname: asking isBlocked()\n" );
707 $blocked = $this->isBlocked( $bFromSlave );
708 }
709 wfProfileOut( $fname );
710 return $blocked;
711 }
712
713 /**
714 * Get name of blocker
715 * @return string name of blocker
716 */
717 function blockedBy() {
718 $this->getBlockedStatus();
719 return $this->mBlockedby;
720 }
721
722 /**
723 * Get blocking reason
724 * @return string Blocking reason
725 */
726 function blockedFor() {
727 $this->getBlockedStatus();
728 return $this->mBlockreason;
729 }
730
731 /**
732 * Initialise php session
733 * @deprecated use wfSetupSession()
734 */
735 function SetupSession() {
736 wfSetupSession();
737 }
738
739 /**
740 * Create a new user object using data from session
741 * @static
742 */
743 function loadFromSession() {
744 global $wgMemc, $wgDBname, $wgCookiePrefix;
745
746 if ( isset( $_SESSION['wsUserID'] ) ) {
747 if ( 0 != $_SESSION['wsUserID'] ) {
748 $sId = $_SESSION['wsUserID'];
749 } else {
750 return new User();
751 }
752 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
753 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
754 $_SESSION['wsUserID'] = $sId;
755 } else {
756 return new User();
757 }
758 if ( isset( $_SESSION['wsUserName'] ) ) {
759 $sName = $_SESSION['wsUserName'];
760 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
761 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
762 $_SESSION['wsUserName'] = $sName;
763 } else {
764 return new User();
765 }
766
767 $passwordCorrect = FALSE;
768 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
769 if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) {
770 # Expire old serialized objects; they may be corrupt.
771 $user = false;
772 }
773 if($makenew = !$user) {
774 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
775 $user = new User();
776 $user->mId = $sId;
777 $user->loadFromDatabase();
778 } else {
779 wfDebug( "User::loadFromSession() got from cache!\n" );
780 # Set block status to unloaded, that should be loaded every time
781 $user->mBlockedby = -1;
782 }
783
784 if ( isset( $_SESSION['wsToken'] ) ) {
785 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken;
786 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
787 $passwordCorrect = $user->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
788 } else {
789 return new User(); # Can't log in from session
790 }
791
792 if ( ( $sName == $user->mName ) && $passwordCorrect ) {
793 if($makenew) {
794 if($wgMemc->set( $key, $user ))
795 wfDebug( "User::loadFromSession() successfully saved user\n" );
796 else
797 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
798 }
799 return $user;
800 }
801 return new User(); # Can't log in from session
802 }
803
804 /**
805 * Load a user from the database
806 */
807 function loadFromDatabase() {
808 $fname = "User::loadFromDatabase";
809
810 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
811 # loading in a command line script, don't assume all command line scripts need it like this
812 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
813 if ( $this->mDataLoaded ) {
814 return;
815 }
816
817 # Paranoia
818 $this->mId = intval( $this->mId );
819
820 /** Anonymous user */
821 if( !$this->mId ) {
822 /** Get rights */
823 $this->mRights = $this->getGroupPermissions( array( '*' ) );
824 $this->mDataLoaded = true;
825 return;
826 } # the following stuff is for non-anonymous users only
827
828 $dbr =& wfGetDB( DB_SLAVE );
829 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
830 'user_email_authenticated',
831 'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ),
832 array( 'user_id' => $this->mId ), $fname );
833
834 if ( $s !== false ) {
835 $this->mName = $s->user_name;
836 $this->mEmail = $s->user_email;
837 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
838 $this->mRealName = $s->user_real_name;
839 $this->mPassword = $s->user_password;
840 $this->mNewpassword = $s->user_newpassword;
841 $this->decodeOptions( $s->user_options );
842 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
843 $this->mToken = $s->user_token;
844 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
845
846 $res = $dbr->select( 'user_groups',
847 array( 'ug_group' ),
848 array( 'ug_user' => $this->mId ),
849 $fname );
850 $this->mGroups = array();
851 while( $row = $dbr->fetchObject( $res ) ) {
852 $this->mGroups[] = $row->ug_group;
853 }
854 $implicitGroups = array( '*', 'user' );
855
856 global $wgAutoConfirmAge;
857 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
858 if( $accountAge >= $wgAutoConfirmAge ) {
859 $implicitGroups[] = 'autoconfirmed';
860 }
861
862 # Implicit group for users whose email addresses are confirmed
863 global $wgEmailAuthentication;
864 if( $this->isValidEmailAddr( $this->mEmail ) ) {
865 if( $wgEmailAuthentication ) {
866 if( $this->mEmailAuthenticated )
867 $implicitGroups[] = 'emailconfirmed';
868 } else {
869 $implicitGroups[] = 'emailconfirmed';
870 }
871 }
872
873 $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
874 $this->mRights = $this->getGroupPermissions( $effectiveGroups );
875 }
876
877 $this->mDataLoaded = true;
878 }
879
880 function getID() { return $this->mId; }
881 function setID( $v ) {
882 $this->mId = $v;
883 $this->mDataLoaded = false;
884 }
885
886 function getName() {
887 $this->loadFromDatabase();
888 if ( $this->mName === false ) {
889 $this->mName = wfGetIP();
890 }
891 return $this->mName;
892 }
893
894 function setName( $str ) {
895 $this->loadFromDatabase();
896 $this->mName = $str;
897 }
898
899
900 /**
901 * Return the title dbkey form of the name, for eg user pages.
902 * @return string
903 * @public
904 */
905 function getTitleKey() {
906 return str_replace( ' ', '_', $this->getName() );
907 }
908
909 function getNewtalk() {
910 $this->loadFromDatabase();
911
912 # Load the newtalk status if it is unloaded (mNewtalk=-1)
913 if( $this->mNewtalk === -1 ) {
914 $this->mNewtalk = false; # reset talk page status
915
916 # Check memcached separately for anons, who have no
917 # entire User object stored in there.
918 if( !$this->mId ) {
919 global $wgDBname, $wgMemc;
920 $key = "$wgDBname:newtalk:ip:" . $this->getName();
921 $newtalk = $wgMemc->get( $key );
922 if( is_integer( $newtalk ) ) {
923 $this->mNewtalk = (bool)$newtalk;
924 } else {
925 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
926 $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
927 }
928 } else {
929 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
930 }
931 }
932
933 return (bool)$this->mNewtalk;
934 }
935
936 /**
937 * Return the talk page(s) this user has new messages on.
938 */
939 function getNewMessageLinks() {
940 global $wgDBname;
941 $talks = array();
942 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
943 return $talks;
944
945 if (!$this->getNewtalk())
946 return array();
947 $up = $this->getUserPage();
948 $utp = $up->getTalkPage();
949 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
950 }
951
952
953 /**
954 * Perform a user_newtalk check on current slaves; if the memcached data
955 * is funky we don't want newtalk state to get stuck on save, as that's
956 * damn annoying.
957 *
958 * @param string $field
959 * @param mixed $id
960 * @return bool
961 * @private
962 */
963 function checkNewtalk( $field, $id ) {
964 $fname = 'User::checkNewtalk';
965 $dbr =& wfGetDB( DB_SLAVE );
966 $ok = $dbr->selectField( 'user_newtalk', $field,
967 array( $field => $id ), $fname );
968 return $ok !== false;
969 }
970
971 /**
972 * Add or update the
973 * @param string $field
974 * @param mixed $id
975 * @private
976 */
977 function updateNewtalk( $field, $id ) {
978 $fname = 'User::updateNewtalk';
979 if( $this->checkNewtalk( $field, $id ) ) {
980 wfDebug( "$fname already set ($field, $id), ignoring\n" );
981 return false;
982 }
983 $dbw =& wfGetDB( DB_MASTER );
984 $dbw->insert( 'user_newtalk',
985 array( $field => $id ),
986 $fname,
987 'IGNORE' );
988 wfDebug( "$fname: set on ($field, $id)\n" );
989 return true;
990 }
991
992 /**
993 * Clear the new messages flag for the given user
994 * @param string $field
995 * @param mixed $id
996 * @private
997 */
998 function deleteNewtalk( $field, $id ) {
999 $fname = 'User::deleteNewtalk';
1000 if( !$this->checkNewtalk( $field, $id ) ) {
1001 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
1002 return false;
1003 }
1004 $dbw =& wfGetDB( DB_MASTER );
1005 $dbw->delete( 'user_newtalk',
1006 array( $field => $id ),
1007 $fname );
1008 wfDebug( "$fname: killed on ($field, $id)\n" );
1009 return true;
1010 }
1011
1012 /**
1013 * Update the 'You have new messages!' status.
1014 * @param bool $val
1015 */
1016 function setNewtalk( $val ) {
1017 if( wfReadOnly() ) {
1018 return;
1019 }
1020
1021 $this->loadFromDatabase();
1022 $this->mNewtalk = $val;
1023
1024 $fname = 'User::setNewtalk';
1025
1026 if( $this->isAnon() ) {
1027 $field = 'user_ip';
1028 $id = $this->getName();
1029 } else {
1030 $field = 'user_id';
1031 $id = $this->getId();
1032 }
1033
1034 if( $val ) {
1035 $changed = $this->updateNewtalk( $field, $id );
1036 } else {
1037 $changed = $this->deleteNewtalk( $field, $id );
1038 }
1039
1040 if( $changed ) {
1041 if( $this->isAnon() ) {
1042 // Anons have a separate memcached space, since
1043 // user records aren't kept for them.
1044 global $wgDBname, $wgMemc;
1045 $key = "$wgDBname:newtalk:ip:$val";
1046 $wgMemc->set( $key, $val ? 1 : 0 );
1047 } else {
1048 if( $val ) {
1049 // Make sure the user page is watched, so a notification
1050 // will be sent out if enabled.
1051 $this->addWatch( $this->getTalkPage() );
1052 }
1053 }
1054 $this->invalidateCache();
1055 $this->saveSettings();
1056 }
1057 }
1058
1059 function invalidateCache() {
1060 global $wgClockSkewFudge;
1061 $this->loadFromDatabase();
1062 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
1063 # Don't forget to save the options after this or
1064 # it won't take effect!
1065 }
1066
1067 function validateCache( $timestamp ) {
1068 $this->loadFromDatabase();
1069 return ($timestamp >= $this->mTouched);
1070 }
1071
1072 /**
1073 * Encrypt a password.
1074 * It can eventuall salt a password @see User::addSalt()
1075 * @param string $p clear Password.
1076 * @return string Encrypted password.
1077 */
1078 function encryptPassword( $p ) {
1079 return wfEncryptPassword( $this->mId, $p );
1080 }
1081
1082 # Set the password and reset the random token
1083 function setPassword( $str ) {
1084 $this->loadFromDatabase();
1085 $this->setToken();
1086 $this->mPassword = $this->encryptPassword( $str );
1087 $this->mNewpassword = '';
1088 }
1089
1090 # Set the random token (used for persistent authentication)
1091 function setToken( $token = false ) {
1092 global $wgSecretKey, $wgProxyKey, $wgDBname;
1093 if ( !$token ) {
1094 if ( $wgSecretKey ) {
1095 $key = $wgSecretKey;
1096 } elseif ( $wgProxyKey ) {
1097 $key = $wgProxyKey;
1098 } else {
1099 $key = microtime();
1100 }
1101 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1102 } else {
1103 $this->mToken = $token;
1104 }
1105 }
1106
1107
1108 function setCookiePassword( $str ) {
1109 $this->loadFromDatabase();
1110 $this->mCookiePassword = md5( $str );
1111 }
1112
1113 function setNewpassword( $str ) {
1114 $this->loadFromDatabase();
1115 $this->mNewpassword = $this->encryptPassword( $str );
1116 }
1117
1118 function getEmail() {
1119 $this->loadFromDatabase();
1120 return $this->mEmail;
1121 }
1122
1123 function getEmailAuthenticationTimestamp() {
1124 $this->loadFromDatabase();
1125 return $this->mEmailAuthenticated;
1126 }
1127
1128 function setEmail( $str ) {
1129 $this->loadFromDatabase();
1130 $this->mEmail = $str;
1131 }
1132
1133 function getRealName() {
1134 $this->loadFromDatabase();
1135 return $this->mRealName;
1136 }
1137
1138 function setRealName( $str ) {
1139 $this->loadFromDatabase();
1140 $this->mRealName = $str;
1141 }
1142
1143 /**
1144 * @param string $oname The option to check
1145 * @return string
1146 */
1147 function getOption( $oname ) {
1148 $this->loadFromDatabase();
1149 if ( is_null( $this->mOptions ) ) {
1150 $this->mOptions = User::getDefaultOptions();
1151 }
1152 if ( array_key_exists( $oname, $this->mOptions ) ) {
1153 return trim( $this->mOptions[$oname] );
1154 } else {
1155 return '';
1156 }
1157 }
1158
1159 /**
1160 * Get the user's date preference, including some important migration for
1161 * old user rows.
1162 */
1163 function getDatePreference() {
1164 if ( is_null( $this->mDatePreference ) ) {
1165 global $wgLang;
1166 $value = $this->getOption( 'date' );
1167 $map = $wgLang->getDatePreferenceMigrationMap();
1168 if ( isset( $map[$value] ) ) {
1169 $value = $map[$value];
1170 }
1171 $this->mDatePreference = $value;
1172 }
1173 return $this->mDatePreference;
1174 }
1175
1176 /**
1177 * @param string $oname The option to check
1178 * @return bool False if the option is not selected, true if it is
1179 */
1180 function getBoolOption( $oname ) {
1181 return (bool)$this->getOption( $oname );
1182 }
1183
1184 /**
1185 * Get an option as an integer value from the source string.
1186 * @param string $oname The option to check
1187 * @param int $default Optional value to return if option is unset/blank.
1188 * @return int
1189 */
1190 function getIntOption( $oname, $default=0 ) {
1191 $val = $this->getOption( $oname );
1192 if( $val == '' ) {
1193 $val = $default;
1194 }
1195 return intval( $val );
1196 }
1197
1198 function setOption( $oname, $val ) {
1199 $this->loadFromDatabase();
1200 if ( is_null( $this->mOptions ) ) {
1201 $this->mOptions = User::getDefaultOptions();
1202 }
1203 if ( $oname == 'skin' ) {
1204 # Clear cached skin, so the new one displays immediately in Special:Preferences
1205 unset( $this->mSkin );
1206 }
1207 // Filter out any newlines that may have passed through input validation.
1208 // Newlines are used to separate items in the options blob.
1209 $val = str_replace( "\r\n", "\n", $val );
1210 $val = str_replace( "\r", "\n", $val );
1211 $val = str_replace( "\n", " ", $val );
1212 $this->mOptions[$oname] = $val;
1213 $this->invalidateCache();
1214 }
1215
1216 function getRights() {
1217 $this->loadFromDatabase();
1218 return $this->mRights;
1219 }
1220
1221 /**
1222 * Get the list of explicit group memberships this user has.
1223 * The implicit * and user groups are not included.
1224 * @return array of strings
1225 */
1226 function getGroups() {
1227 $this->loadFromDatabase();
1228 return $this->mGroups;
1229 }
1230
1231 /**
1232 * Get the list of implicit group memberships this user has.
1233 * This includes all explicit groups, plus 'user' if logged in
1234 * and '*' for all accounts.
1235 * @return array of strings
1236 */
1237 function getEffectiveGroups() {
1238 $base = array( '*' );
1239 if( $this->isLoggedIn() ) {
1240 $base[] = 'user';
1241 }
1242 return array_merge( $base, $this->getGroups() );
1243 }
1244
1245 /**
1246 * Add the user to the given group.
1247 * This takes immediate effect.
1248 * @string $group
1249 */
1250 function addGroup( $group ) {
1251 $dbw =& wfGetDB( DB_MASTER );
1252 $dbw->insert( 'user_groups',
1253 array(
1254 'ug_user' => $this->getID(),
1255 'ug_group' => $group,
1256 ),
1257 'User::addGroup',
1258 array( 'IGNORE' ) );
1259
1260 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1261 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1262
1263 $this->invalidateCache();
1264 $this->saveSettings();
1265 }
1266
1267 /**
1268 * Remove the user from the given group.
1269 * This takes immediate effect.
1270 * @string $group
1271 */
1272 function removeGroup( $group ) {
1273 $dbw =& wfGetDB( DB_MASTER );
1274 $dbw->delete( 'user_groups',
1275 array(
1276 'ug_user' => $this->getID(),
1277 'ug_group' => $group,
1278 ),
1279 'User::removeGroup' );
1280
1281 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1282 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1283
1284 $this->invalidateCache();
1285 $this->saveSettings();
1286 }
1287
1288
1289 /**
1290 * A more legible check for non-anonymousness.
1291 * Returns true if the user is not an anonymous visitor.
1292 *
1293 * @return bool
1294 */
1295 function isLoggedIn() {
1296 return( $this->getID() != 0 );
1297 }
1298
1299 /**
1300 * A more legible check for anonymousness.
1301 * Returns true if the user is an anonymous visitor.
1302 *
1303 * @return bool
1304 */
1305 function isAnon() {
1306 return !$this->isLoggedIn();
1307 }
1308
1309 /**
1310 * Whether the user is a bot
1311 * @deprecated
1312 */
1313 function isBot() {
1314 return $this->isAllowed( 'bot' );
1315 }
1316
1317 /**
1318 * Check if user is allowed to access a feature / make an action
1319 * @param string $action Action to be checked
1320 * @return boolean True: action is allowed, False: action should not be allowed
1321 */
1322 function isAllowed($action='') {
1323 if ( $action === '' )
1324 // In the spirit of DWIM
1325 return true;
1326
1327 $this->loadFromDatabase();
1328 return in_array( $action , $this->mRights );
1329 }
1330
1331 /**
1332 * Load a skin if it doesn't exist or return it
1333 * @todo FIXME : need to check the old failback system [AV]
1334 */
1335 function &getSkin() {
1336 global $IP, $wgRequest;
1337 if ( ! isset( $this->mSkin ) ) {
1338 $fname = 'User::getSkin';
1339 wfProfileIn( $fname );
1340
1341 # get the user skin
1342 $userSkin = $this->getOption( 'skin' );
1343 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1344
1345 $this->mSkin =& Skin::newFromKey( $userSkin );
1346 wfProfileOut( $fname );
1347 }
1348 return $this->mSkin;
1349 }
1350
1351 /**#@+
1352 * @param string $title Article title to look at
1353 */
1354
1355 /**
1356 * Check watched status of an article
1357 * @return bool True if article is watched
1358 */
1359 function isWatched( $title ) {
1360 $wl = WatchedItem::fromUserTitle( $this, $title );
1361 return $wl->isWatched();
1362 }
1363
1364 /**
1365 * Watch an article
1366 */
1367 function addWatch( $title ) {
1368 $wl = WatchedItem::fromUserTitle( $this, $title );
1369 $wl->addWatch();
1370 $this->invalidateCache();
1371 }
1372
1373 /**
1374 * Stop watching an article
1375 */
1376 function removeWatch( $title ) {
1377 $wl = WatchedItem::fromUserTitle( $this, $title );
1378 $wl->removeWatch();
1379 $this->invalidateCache();
1380 }
1381
1382 /**
1383 * Clear the user's notification timestamp for the given title.
1384 * If e-notif e-mails are on, they will receive notification mails on
1385 * the next change of the page if it's watched etc.
1386 */
1387 function clearNotification( &$title ) {
1388 global $wgUser, $wgUseEnotif;
1389
1390
1391 if ($title->getNamespace() == NS_USER_TALK &&
1392 $title->getText() == $this->getName() ) {
1393 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1394 return;
1395 $this->setNewtalk( false );
1396 }
1397
1398 if( !$wgUseEnotif ) {
1399 return;
1400 }
1401
1402 if( $this->isAnon() ) {
1403 // Nothing else to do...
1404 return;
1405 }
1406
1407 // Only update the timestamp if the page is being watched.
1408 // The query to find out if it is watched is cached both in memcached and per-invocation,
1409 // and when it does have to be executed, it can be on a slave
1410 // If this is the user's newtalk page, we always update the timestamp
1411 if ($title->getNamespace() == NS_USER_TALK &&
1412 $title->getText() == $wgUser->getName())
1413 {
1414 $watched = true;
1415 } elseif ( $this->getID() == $wgUser->getID() ) {
1416 $watched = $title->userIsWatching();
1417 } else {
1418 $watched = true;
1419 }
1420
1421 // If the page is watched by the user (or may be watched), update the timestamp on any
1422 // any matching rows
1423 if ( $watched ) {
1424 $dbw =& wfGetDB( DB_MASTER );
1425 $success = $dbw->update( 'watchlist',
1426 array( /* SET */
1427 'wl_notificationtimestamp' => NULL
1428 ), array( /* WHERE */
1429 'wl_title' => $title->getDBkey(),
1430 'wl_namespace' => $title->getNamespace(),
1431 'wl_user' => $this->getID()
1432 ), 'User::clearLastVisited'
1433 );
1434 }
1435 }
1436
1437 /**#@-*/
1438
1439 /**
1440 * Resets all of the given user's page-change notification timestamps.
1441 * If e-notif e-mails are on, they will receive notification mails on
1442 * the next change of any watched page.
1443 *
1444 * @param int $currentUser user ID number
1445 * @public
1446 */
1447 function clearAllNotifications( $currentUser ) {
1448 global $wgUseEnotif;
1449 if ( !$wgUseEnotif ) {
1450 $this->setNewtalk( false );
1451 return;
1452 }
1453 if( $currentUser != 0 ) {
1454
1455 $dbw =& wfGetDB( DB_MASTER );
1456 $success = $dbw->update( 'watchlist',
1457 array( /* SET */
1458 'wl_notificationtimestamp' => 0
1459 ), array( /* WHERE */
1460 'wl_user' => $currentUser
1461 ), 'UserMailer::clearAll'
1462 );
1463
1464 # we also need to clear here the "you have new message" notification for the own user_talk page
1465 # This is cleared one page view later in Article::viewUpdates();
1466 }
1467 }
1468
1469 /**
1470 * @private
1471 * @return string Encoding options
1472 */
1473 function encodeOptions() {
1474 if ( is_null( $this->mOptions ) ) {
1475 $this->mOptions = User::getDefaultOptions();
1476 }
1477 $a = array();
1478 foreach ( $this->mOptions as $oname => $oval ) {
1479 array_push( $a, $oname.'='.$oval );
1480 }
1481 $s = implode( "\n", $a );
1482 return $s;
1483 }
1484
1485 /**
1486 * @private
1487 */
1488 function decodeOptions( $str ) {
1489 global $wgLang;
1490
1491 $this->mOptions = array();
1492 $a = explode( "\n", $str );
1493 foreach ( $a as $s ) {
1494 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1495 $this->mOptions[$m[1]] = $m[2];
1496 }
1497 }
1498 }
1499
1500 function setCookies() {
1501 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1502 if ( 0 == $this->mId ) return;
1503 $this->loadFromDatabase();
1504 $exp = time() + $wgCookieExpiration;
1505
1506 $_SESSION['wsUserID'] = $this->mId;
1507 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1508
1509 $_SESSION['wsUserName'] = $this->getName();
1510 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1511
1512 $_SESSION['wsToken'] = $this->mToken;
1513 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1514 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1515 } else {
1516 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1517 }
1518 }
1519
1520 /**
1521 * Logout user
1522 * It will clean the session cookie
1523 */
1524 function logout() {
1525 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1526 $this->loadDefaults();
1527 $this->setLoaded( true );
1528
1529 $_SESSION['wsUserID'] = 0;
1530
1531 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1532 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1533
1534 # Remember when user logged out, to prevent seeing cached pages
1535 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1536 }
1537
1538 /**
1539 * Save object settings into database
1540 */
1541 function saveSettings() {
1542 global $wgMemc, $wgDBname;
1543 $fname = 'User::saveSettings';
1544
1545 if ( wfReadOnly() ) { return; }
1546 if ( 0 == $this->mId ) { return; }
1547
1548 $dbw =& wfGetDB( DB_MASTER );
1549 $dbw->update( 'user',
1550 array( /* SET */
1551 'user_name' => $this->mName,
1552 'user_password' => $this->mPassword,
1553 'user_newpassword' => $this->mNewpassword,
1554 'user_real_name' => $this->mRealName,
1555 'user_email' => $this->mEmail,
1556 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1557 'user_options' => $this->encodeOptions(),
1558 'user_touched' => $dbw->timestamp($this->mTouched),
1559 'user_token' => $this->mToken
1560 ), array( /* WHERE */
1561 'user_id' => $this->mId
1562 ), $fname
1563 );
1564 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1565 }
1566
1567
1568 /**
1569 * Checks if a user with the given name exists, returns the ID
1570 */
1571 function idForName() {
1572 $fname = 'User::idForName';
1573
1574 $gotid = 0;
1575 $s = trim( $this->getName() );
1576 if ( 0 == strcmp( '', $s ) ) return 0;
1577
1578 $dbr =& wfGetDB( DB_SLAVE );
1579 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1580 if ( $id === false ) {
1581 $id = 0;
1582 }
1583 return $id;
1584 }
1585
1586 /**
1587 * Add user object to the database
1588 */
1589 function addToDatabase() {
1590 $fname = 'User::addToDatabase';
1591 $dbw =& wfGetDB( DB_MASTER );
1592 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1593 $dbw->insert( 'user',
1594 array(
1595 'user_id' => $seqVal,
1596 'user_name' => $this->mName,
1597 'user_password' => $this->mPassword,
1598 'user_newpassword' => $this->mNewpassword,
1599 'user_email' => $this->mEmail,
1600 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1601 'user_real_name' => $this->mRealName,
1602 'user_options' => $this->encodeOptions(),
1603 'user_token' => $this->mToken,
1604 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1605 ), $fname
1606 );
1607 $this->mId = $dbw->insertId();
1608 }
1609
1610 function spreadBlock() {
1611 # If the (non-anonymous) user is blocked, this function will block any IP address
1612 # that they successfully log on from.
1613 $fname = 'User::spreadBlock';
1614
1615 wfDebug( "User:spreadBlock()\n" );
1616 if ( $this->mId == 0 ) {
1617 return;
1618 }
1619
1620 $userblock = Block::newFromDB( '', $this->mId );
1621 if ( !$userblock ) {
1622 return;
1623 }
1624
1625 # Check if this IP address is already blocked
1626 $ipblock = Block::newFromDB( wfGetIP() );
1627 if ( $ipblock ) {
1628 # If the user is already blocked. Then check if the autoblock would
1629 # excede the user block. If it would excede, then do nothing, else
1630 # prolong block time
1631 if ($userblock->mExpiry &&
1632 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1633 return;
1634 }
1635 # Just update the timestamp
1636 $ipblock->updateTimestamp();
1637 return;
1638 } else {
1639 $ipblock = new Block;
1640 }
1641
1642 # Make a new block object with the desired properties
1643 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1644 $ipblock->mAddress = wfGetIP();
1645 $ipblock->mUser = 0;
1646 $ipblock->mBy = $userblock->mBy;
1647 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1648 $ipblock->mTimestamp = wfTimestampNow();
1649 $ipblock->mAuto = 1;
1650 # If the user is already blocked with an expiry date, we don't
1651 # want to pile on top of that!
1652 if($userblock->mExpiry) {
1653 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1654 } else {
1655 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1656 }
1657
1658 # Insert it
1659 $ipblock->insert();
1660
1661 }
1662
1663 /**
1664 * Generate a string which will be different for any combination of
1665 * user options which would produce different parser output.
1666 * This will be used as part of the hash key for the parser cache,
1667 * so users will the same options can share the same cached data
1668 * safely.
1669 *
1670 * Extensions which require it should install 'PageRenderingHash' hook,
1671 * which will give them a chance to modify this key based on their own
1672 * settings.
1673 *
1674 * @return string
1675 */
1676 function getPageRenderingHash() {
1677 global $wgContLang, $wgUseDynamicDates;
1678 if( $this->mHash ){
1679 return $this->mHash;
1680 }
1681
1682 // stubthreshold is only included below for completeness,
1683 // it will always be 0 when this function is called by parsercache.
1684
1685 $confstr = $this->getOption( 'math' );
1686 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1687 if ( $wgUseDynamicDates ) {
1688 $confstr .= '!' . $this->getDatePreference();
1689 }
1690 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1691 $confstr .= '!' . $this->getOption( 'language' );
1692 $confstr .= '!' . $this->getOption( 'thumbsize' );
1693 // add in language specific options, if any
1694 $extra = $wgContLang->getExtraHashOptions();
1695 $confstr .= $extra;
1696
1697 // Give a chance for extensions to modify the hash, if they have
1698 // extra options or other effects on the parser cache.
1699 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1700
1701 $this->mHash = $confstr;
1702 return $confstr;
1703 }
1704
1705 function isBlockedFromCreateAccount() {
1706 $this->getBlockedStatus();
1707 return $this->mBlock && $this->mBlock->mCreateAccount;
1708 }
1709
1710 function isAllowedToCreateAccount() {
1711 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
1712 }
1713
1714 /**
1715 * Set mDataLoaded, return previous value
1716 * Use this to prevent DB access in command-line scripts or similar situations
1717 */
1718 function setLoaded( $loaded ) {
1719 return wfSetVar( $this->mDataLoaded, $loaded );
1720 }
1721
1722 /**
1723 * Get this user's personal page title.
1724 *
1725 * @return Title
1726 * @public
1727 */
1728 function getUserPage() {
1729 return Title::makeTitle( NS_USER, $this->getName() );
1730 }
1731
1732 /**
1733 * Get this user's talk page title.
1734 *
1735 * @return Title
1736 * @public
1737 */
1738 function getTalkPage() {
1739 $title = $this->getUserPage();
1740 return $title->getTalkPage();
1741 }
1742
1743 /**
1744 * @static
1745 */
1746 function getMaxID() {
1747 static $res; // cache
1748
1749 if ( isset( $res ) )
1750 return $res;
1751 else {
1752 $dbr =& wfGetDB( DB_SLAVE );
1753 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1754 }
1755 }
1756
1757 /**
1758 * Determine whether the user is a newbie. Newbies are either
1759 * anonymous IPs, or the most recently created accounts.
1760 * @return bool True if it is a newbie.
1761 */
1762 function isNewbie() {
1763 return !$this->isAllowed( 'autoconfirmed' );
1764 }
1765
1766 /**
1767 * Check to see if the given clear-text password is one of the accepted passwords
1768 * @param string $password User password.
1769 * @return bool True if the given password is correct otherwise False.
1770 */
1771 function checkPassword( $password ) {
1772 global $wgAuth, $wgMinimalPasswordLength;
1773 $this->loadFromDatabase();
1774
1775 // Even though we stop people from creating passwords that
1776 // are shorter than this, doesn't mean people wont be able
1777 // to. Certain authentication plugins do NOT want to save
1778 // domain passwords in a mysql database, so we should
1779 // check this (incase $wgAuth->strict() is false).
1780 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1781 return false;
1782 }
1783
1784 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1785 return true;
1786 } elseif( $wgAuth->strict() ) {
1787 /* Auth plugin doesn't allow local authentication */
1788 return false;
1789 }
1790 $ep = $this->encryptPassword( $password );
1791 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1792 return true;
1793 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1794 return true;
1795 } elseif ( function_exists( 'iconv' ) ) {
1796 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1797 # Check for this with iconv
1798 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1799 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1800 return true;
1801 }
1802 }
1803 return false;
1804 }
1805
1806 /**
1807 * Initialize (if necessary) and return a session token value
1808 * which can be used in edit forms to show that the user's
1809 * login credentials aren't being hijacked with a foreign form
1810 * submission.
1811 *
1812 * @param mixed $salt - Optional function-specific data for hash.
1813 * Use a string or an array of strings.
1814 * @return string
1815 * @public
1816 */
1817 function editToken( $salt = '' ) {
1818 if( !isset( $_SESSION['wsEditToken'] ) ) {
1819 $token = $this->generateToken();
1820 $_SESSION['wsEditToken'] = $token;
1821 } else {
1822 $token = $_SESSION['wsEditToken'];
1823 }
1824 if( is_array( $salt ) ) {
1825 $salt = implode( '|', $salt );
1826 }
1827 return md5( $token . $salt );
1828 }
1829
1830 /**
1831 * Generate a hex-y looking random token for various uses.
1832 * Could be made more cryptographically sure if someone cares.
1833 * @return string
1834 */
1835 function generateToken( $salt = '' ) {
1836 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1837 return md5( $token . $salt );
1838 }
1839
1840 /**
1841 * Check given value against the token value stored in the session.
1842 * A match should confirm that the form was submitted from the
1843 * user's own login session, not a form submission from a third-party
1844 * site.
1845 *
1846 * @param string $val - the input value to compare
1847 * @param string $salt - Optional function-specific data for hash
1848 * @return bool
1849 * @public
1850 */
1851 function matchEditToken( $val, $salt = '' ) {
1852 global $wgMemc;
1853 $sessionToken = $this->editToken( $salt );
1854 if ( $val != $sessionToken ) {
1855 wfDebug( "User::matchEditToken: broken session data\n" );
1856 }
1857 return $val == $sessionToken;
1858 }
1859
1860 /**
1861 * Generate a new e-mail confirmation token and send a confirmation
1862 * mail to the user's given address.
1863 *
1864 * @return mixed True on success, a WikiError object on failure.
1865 */
1866 function sendConfirmationMail() {
1867 global $wgContLang;
1868 $url = $this->confirmationTokenUrl( $expiration );
1869 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1870 wfMsg( 'confirmemail_body',
1871 wfGetIP(),
1872 $this->getName(),
1873 $url,
1874 $wgContLang->timeanddate( $expiration, false ) ) );
1875 }
1876
1877 /**
1878 * Send an e-mail to this user's account. Does not check for
1879 * confirmed status or validity.
1880 *
1881 * @param string $subject
1882 * @param string $body
1883 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1884 * @return mixed True on success, a WikiError object on failure.
1885 */
1886 function sendMail( $subject, $body, $from = null ) {
1887 if( is_null( $from ) ) {
1888 global $wgPasswordSender;
1889 $from = $wgPasswordSender;
1890 }
1891
1892 require_once( 'UserMailer.php' );
1893 $to = new MailAddress( $this );
1894 $sender = new MailAddress( $from );
1895 $error = userMailer( $to, $sender, $subject, $body );
1896
1897 if( $error == '' ) {
1898 return true;
1899 } else {
1900 return new WikiError( $error );
1901 }
1902 }
1903
1904 /**
1905 * Generate, store, and return a new e-mail confirmation code.
1906 * A hash (unsalted since it's used as a key) is stored.
1907 * @param &$expiration mixed output: accepts the expiration time
1908 * @return string
1909 * @private
1910 */
1911 function confirmationToken( &$expiration ) {
1912 $fname = 'User::confirmationToken';
1913
1914 $now = time();
1915 $expires = $now + 7 * 24 * 60 * 60;
1916 $expiration = wfTimestamp( TS_MW, $expires );
1917
1918 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1919 $hash = md5( $token );
1920
1921 $dbw =& wfGetDB( DB_MASTER );
1922 $dbw->update( 'user',
1923 array( 'user_email_token' => $hash,
1924 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1925 array( 'user_id' => $this->mId ),
1926 $fname );
1927
1928 return $token;
1929 }
1930
1931 /**
1932 * Generate and store a new e-mail confirmation token, and return
1933 * the URL the user can use to confirm.
1934 * @param &$expiration mixed output: accepts the expiration time
1935 * @return string
1936 * @private
1937 */
1938 function confirmationTokenUrl( &$expiration ) {
1939 $token = $this->confirmationToken( $expiration );
1940 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1941 return $title->getFullUrl();
1942 }
1943
1944 /**
1945 * Mark the e-mail address confirmed and save.
1946 */
1947 function confirmEmail() {
1948 $this->loadFromDatabase();
1949 $this->mEmailAuthenticated = wfTimestampNow();
1950 $this->saveSettings();
1951 return true;
1952 }
1953
1954 /**
1955 * Is this user allowed to send e-mails within limits of current
1956 * site configuration?
1957 * @return bool
1958 */
1959 function canSendEmail() {
1960 return $this->isEmailConfirmed();
1961 }
1962
1963 /**
1964 * Is this user allowed to receive e-mails within limits of current
1965 * site configuration?
1966 * @return bool
1967 */
1968 function canReceiveEmail() {
1969 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1970 }
1971
1972 /**
1973 * Is this user's e-mail address valid-looking and confirmed within
1974 * limits of the current site configuration?
1975 *
1976 * If $wgEmailAuthentication is on, this may require the user to have
1977 * confirmed their address by returning a code or using a password
1978 * sent to the address from the wiki.
1979 *
1980 * @return bool
1981 */
1982 function isEmailConfirmed() {
1983 global $wgEmailAuthentication;
1984 $this->loadFromDatabase();
1985 $confirmed = true;
1986 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
1987 if( $this->isAnon() )
1988 return false;
1989 if( !$this->isValidEmailAddr( $this->mEmail ) )
1990 return false;
1991 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1992 return false;
1993 return true;
1994 } else {
1995 return $confirmed;
1996 }
1997 }
1998
1999 /**
2000 * @param array $groups list of groups
2001 * @return array list of permission key names for given groups combined
2002 * @static
2003 */
2004 static function getGroupPermissions( $groups ) {
2005 global $wgGroupPermissions;
2006 $rights = array();
2007 foreach( $groups as $group ) {
2008 if( isset( $wgGroupPermissions[$group] ) ) {
2009 $rights = array_merge( $rights,
2010 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2011 }
2012 }
2013 return $rights;
2014 }
2015
2016 /**
2017 * @param string $group key name
2018 * @return string localized descriptive name for group, if provided
2019 * @static
2020 */
2021 static function getGroupName( $group ) {
2022 $key = "group-$group";
2023 $name = wfMsg( $key );
2024 if( $name == '' || $name == "&lt;$key&gt;" ) {
2025 return $group;
2026 } else {
2027 return $name;
2028 }
2029 }
2030
2031 /**
2032 * @param string $group key name
2033 * @return string localized descriptive name for member of a group, if provided
2034 * @static
2035 */
2036 static function getGroupMember( $group ) {
2037 $key = "group-$group-member";
2038 $name = wfMsg( $key );
2039 if( $name == '' || $name == "&lt;$key&gt;" ) {
2040 return $group;
2041 } else {
2042 return $name;
2043 }
2044 }
2045
2046 /**
2047 * Return the set of defined explicit groups.
2048 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2049 * groups are not included, as they are defined
2050 * automatically, not in the database.
2051 * @return array
2052 * @static
2053 */
2054 static function getAllGroups() {
2055 global $wgGroupPermissions;
2056 return array_diff(
2057 array_keys( $wgGroupPermissions ),
2058 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
2059 }
2060
2061 /**
2062 * Get the title of a page describing a particular group
2063 *
2064 * @param $group Name of the group
2065 * @return mixed
2066 */
2067 static function getGroupPage( $group ) {
2068 $page = wfMsgForContent( 'grouppage-' . $group );
2069 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2070 $title = Title::newFromText( $page );
2071 if( is_object( $title ) )
2072 return $title;
2073 }
2074 return false;
2075 }
2076
2077 /**
2078 * Create a link to the group in HTML, if available
2079 *
2080 * @param $group Name of the group
2081 * @param $text The text of the link
2082 * @return mixed
2083 */
2084 static function makeGroupLinkHTML( $group, $text = '' ) {
2085 if( $text == '' ) {
2086 $text = self::getGroupName( $group );
2087 }
2088 $title = self::getGroupPage( $group );
2089 if( $title ) {
2090 global $wgUser;
2091 $sk = $wgUser->getSkin();
2092 return $sk->makeLinkObj( $title, $text );
2093 } else {
2094 return $text;
2095 }
2096 }
2097
2098 /**
2099 * Create a link to the group in Wikitext, if available
2100 *
2101 * @param $group Name of the group
2102 * @param $text The text of the link (by default, the name of the group)
2103 * @return mixed
2104 */
2105 static function makeGroupLinkWiki( $group, $text = '' ) {
2106 if( $text == '' ) {
2107 $text = self::getGroupName( $group );
2108 }
2109 $title = self::getGroupPage( $group );
2110 if( $title ) {
2111 $page = $title->getPrefixedText();
2112 return "[[$page|$text]]";
2113 } else {
2114 return $text;
2115 }
2116 }
2117 }
2118
2119 ?>