Removing the last references to OutputPage::sysopRequired() and OutputPage::developer...
[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 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1311 * @deprecated
1312 */
1313 function isSysop() {
1314 throw new MWException( "Call to deprecated (v1.7) User::isSysop() method\n" );
1315 }
1316
1317 /**
1318 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1319 * @deprecated
1320 */
1321 function isDeveloper() {
1322 throw new MWException( "Call to deprecated (v1.7) User::isDeveloper() method\n" );
1323 }
1324
1325 /**
1326 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1327 * @deprecated
1328 */
1329 function isBureaucrat() {
1330 throw new MWException( "Call to deprecated (v1.7) User::isBureaucrat() method\n" );
1331 }
1332
1333 /**
1334 * Whether the user is a bot
1335 * @deprecated
1336 */
1337 function isBot() {
1338 return $this->isAllowed( 'bot' );
1339 }
1340
1341 /**
1342 * Check if user is allowed to access a feature / make an action
1343 * @param string $action Action to be checked
1344 * @return boolean True: action is allowed, False: action should not be allowed
1345 */
1346 function isAllowed($action='') {
1347 if ( $action === '' )
1348 // In the spirit of DWIM
1349 return true;
1350
1351 $this->loadFromDatabase();
1352 return in_array( $action , $this->mRights );
1353 }
1354
1355 /**
1356 * Load a skin if it doesn't exist or return it
1357 * @todo FIXME : need to check the old failback system [AV]
1358 */
1359 function &getSkin() {
1360 global $IP, $wgRequest;
1361 if ( ! isset( $this->mSkin ) ) {
1362 $fname = 'User::getSkin';
1363 wfProfileIn( $fname );
1364
1365 # get the user skin
1366 $userSkin = $this->getOption( 'skin' );
1367 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1368
1369 $this->mSkin =& Skin::newFromKey( $userSkin );
1370 wfProfileOut( $fname );
1371 }
1372 return $this->mSkin;
1373 }
1374
1375 /**#@+
1376 * @param string $title Article title to look at
1377 */
1378
1379 /**
1380 * Check watched status of an article
1381 * @return bool True if article is watched
1382 */
1383 function isWatched( $title ) {
1384 $wl = WatchedItem::fromUserTitle( $this, $title );
1385 return $wl->isWatched();
1386 }
1387
1388 /**
1389 * Watch an article
1390 */
1391 function addWatch( $title ) {
1392 $wl = WatchedItem::fromUserTitle( $this, $title );
1393 $wl->addWatch();
1394 $this->invalidateCache();
1395 }
1396
1397 /**
1398 * Stop watching an article
1399 */
1400 function removeWatch( $title ) {
1401 $wl = WatchedItem::fromUserTitle( $this, $title );
1402 $wl->removeWatch();
1403 $this->invalidateCache();
1404 }
1405
1406 /**
1407 * Clear the user's notification timestamp for the given title.
1408 * If e-notif e-mails are on, they will receive notification mails on
1409 * the next change of the page if it's watched etc.
1410 */
1411 function clearNotification( &$title ) {
1412 global $wgUser, $wgUseEnotif;
1413
1414
1415 if ($title->getNamespace() == NS_USER_TALK &&
1416 $title->getText() == $this->getName() ) {
1417 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1418 return;
1419 $this->setNewtalk( false );
1420 }
1421
1422 if( !$wgUseEnotif ) {
1423 return;
1424 }
1425
1426 if( $this->isAnon() ) {
1427 // Nothing else to do...
1428 return;
1429 }
1430
1431 // Only update the timestamp if the page is being watched.
1432 // The query to find out if it is watched is cached both in memcached and per-invocation,
1433 // and when it does have to be executed, it can be on a slave
1434 // If this is the user's newtalk page, we always update the timestamp
1435 if ($title->getNamespace() == NS_USER_TALK &&
1436 $title->getText() == $wgUser->getName())
1437 {
1438 $watched = true;
1439 } elseif ( $this->getID() == $wgUser->getID() ) {
1440 $watched = $title->userIsWatching();
1441 } else {
1442 $watched = true;
1443 }
1444
1445 // If the page is watched by the user (or may be watched), update the timestamp on any
1446 // any matching rows
1447 if ( $watched ) {
1448 $dbw =& wfGetDB( DB_MASTER );
1449 $success = $dbw->update( 'watchlist',
1450 array( /* SET */
1451 'wl_notificationtimestamp' => NULL
1452 ), array( /* WHERE */
1453 'wl_title' => $title->getDBkey(),
1454 'wl_namespace' => $title->getNamespace(),
1455 'wl_user' => $this->getID()
1456 ), 'User::clearLastVisited'
1457 );
1458 }
1459 }
1460
1461 /**#@-*/
1462
1463 /**
1464 * Resets all of the given user's page-change notification timestamps.
1465 * If e-notif e-mails are on, they will receive notification mails on
1466 * the next change of any watched page.
1467 *
1468 * @param int $currentUser user ID number
1469 * @public
1470 */
1471 function clearAllNotifications( $currentUser ) {
1472 global $wgUseEnotif;
1473 if ( !$wgUseEnotif ) {
1474 $this->setNewtalk( false );
1475 return;
1476 }
1477 if( $currentUser != 0 ) {
1478
1479 $dbw =& wfGetDB( DB_MASTER );
1480 $success = $dbw->update( 'watchlist',
1481 array( /* SET */
1482 'wl_notificationtimestamp' => 0
1483 ), array( /* WHERE */
1484 'wl_user' => $currentUser
1485 ), 'UserMailer::clearAll'
1486 );
1487
1488 # we also need to clear here the "you have new message" notification for the own user_talk page
1489 # This is cleared one page view later in Article::viewUpdates();
1490 }
1491 }
1492
1493 /**
1494 * @private
1495 * @return string Encoding options
1496 */
1497 function encodeOptions() {
1498 if ( is_null( $this->mOptions ) ) {
1499 $this->mOptions = User::getDefaultOptions();
1500 }
1501 $a = array();
1502 foreach ( $this->mOptions as $oname => $oval ) {
1503 array_push( $a, $oname.'='.$oval );
1504 }
1505 $s = implode( "\n", $a );
1506 return $s;
1507 }
1508
1509 /**
1510 * @private
1511 */
1512 function decodeOptions( $str ) {
1513 global $wgLang;
1514
1515 $this->mOptions = array();
1516 $a = explode( "\n", $str );
1517 foreach ( $a as $s ) {
1518 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1519 $this->mOptions[$m[1]] = $m[2];
1520 }
1521 }
1522 }
1523
1524 function setCookies() {
1525 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1526 if ( 0 == $this->mId ) return;
1527 $this->loadFromDatabase();
1528 $exp = time() + $wgCookieExpiration;
1529
1530 $_SESSION['wsUserID'] = $this->mId;
1531 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1532
1533 $_SESSION['wsUserName'] = $this->getName();
1534 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1535
1536 $_SESSION['wsToken'] = $this->mToken;
1537 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1538 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1539 } else {
1540 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1541 }
1542 }
1543
1544 /**
1545 * Logout user
1546 * It will clean the session cookie
1547 */
1548 function logout() {
1549 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1550 $this->loadDefaults();
1551 $this->setLoaded( true );
1552
1553 $_SESSION['wsUserID'] = 0;
1554
1555 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1556 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1557
1558 # Remember when user logged out, to prevent seeing cached pages
1559 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1560 }
1561
1562 /**
1563 * Save object settings into database
1564 */
1565 function saveSettings() {
1566 global $wgMemc, $wgDBname;
1567 $fname = 'User::saveSettings';
1568
1569 if ( wfReadOnly() ) { return; }
1570 if ( 0 == $this->mId ) { return; }
1571
1572 $dbw =& wfGetDB( DB_MASTER );
1573 $dbw->update( 'user',
1574 array( /* SET */
1575 'user_name' => $this->mName,
1576 'user_password' => $this->mPassword,
1577 'user_newpassword' => $this->mNewpassword,
1578 'user_real_name' => $this->mRealName,
1579 'user_email' => $this->mEmail,
1580 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1581 'user_options' => $this->encodeOptions(),
1582 'user_touched' => $dbw->timestamp($this->mTouched),
1583 'user_token' => $this->mToken
1584 ), array( /* WHERE */
1585 'user_id' => $this->mId
1586 ), $fname
1587 );
1588 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1589 }
1590
1591
1592 /**
1593 * Checks if a user with the given name exists, returns the ID
1594 */
1595 function idForName() {
1596 $fname = 'User::idForName';
1597
1598 $gotid = 0;
1599 $s = trim( $this->getName() );
1600 if ( 0 == strcmp( '', $s ) ) return 0;
1601
1602 $dbr =& wfGetDB( DB_SLAVE );
1603 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1604 if ( $id === false ) {
1605 $id = 0;
1606 }
1607 return $id;
1608 }
1609
1610 /**
1611 * Add user object to the database
1612 */
1613 function addToDatabase() {
1614 $fname = 'User::addToDatabase';
1615 $dbw =& wfGetDB( DB_MASTER );
1616 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1617 $dbw->insert( 'user',
1618 array(
1619 'user_id' => $seqVal,
1620 'user_name' => $this->mName,
1621 'user_password' => $this->mPassword,
1622 'user_newpassword' => $this->mNewpassword,
1623 'user_email' => $this->mEmail,
1624 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1625 'user_real_name' => $this->mRealName,
1626 'user_options' => $this->encodeOptions(),
1627 'user_token' => $this->mToken,
1628 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1629 ), $fname
1630 );
1631 $this->mId = $dbw->insertId();
1632 }
1633
1634 function spreadBlock() {
1635 # If the (non-anonymous) user is blocked, this function will block any IP address
1636 # that they successfully log on from.
1637 $fname = 'User::spreadBlock';
1638
1639 wfDebug( "User:spreadBlock()\n" );
1640 if ( $this->mId == 0 ) {
1641 return;
1642 }
1643
1644 $userblock = Block::newFromDB( '', $this->mId );
1645 if ( !$userblock ) {
1646 return;
1647 }
1648
1649 # Check if this IP address is already blocked
1650 $ipblock = Block::newFromDB( wfGetIP() );
1651 if ( $ipblock ) {
1652 # If the user is already blocked. Then check if the autoblock would
1653 # excede the user block. If it would excede, then do nothing, else
1654 # prolong block time
1655 if ($userblock->mExpiry &&
1656 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1657 return;
1658 }
1659 # Just update the timestamp
1660 $ipblock->updateTimestamp();
1661 return;
1662 } else {
1663 $ipblock = new Block;
1664 }
1665
1666 # Make a new block object with the desired properties
1667 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1668 $ipblock->mAddress = wfGetIP();
1669 $ipblock->mUser = 0;
1670 $ipblock->mBy = $userblock->mBy;
1671 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1672 $ipblock->mTimestamp = wfTimestampNow();
1673 $ipblock->mAuto = 1;
1674 # If the user is already blocked with an expiry date, we don't
1675 # want to pile on top of that!
1676 if($userblock->mExpiry) {
1677 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1678 } else {
1679 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1680 }
1681
1682 # Insert it
1683 $ipblock->insert();
1684
1685 }
1686
1687 /**
1688 * Generate a string which will be different for any combination of
1689 * user options which would produce different parser output.
1690 * This will be used as part of the hash key for the parser cache,
1691 * so users will the same options can share the same cached data
1692 * safely.
1693 *
1694 * Extensions which require it should install 'PageRenderingHash' hook,
1695 * which will give them a chance to modify this key based on their own
1696 * settings.
1697 *
1698 * @return string
1699 */
1700 function getPageRenderingHash() {
1701 global $wgContLang, $wgUseDynamicDates;
1702 if( $this->mHash ){
1703 return $this->mHash;
1704 }
1705
1706 // stubthreshold is only included below for completeness,
1707 // it will always be 0 when this function is called by parsercache.
1708
1709 $confstr = $this->getOption( 'math' );
1710 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1711 if ( $wgUseDynamicDates ) {
1712 $confstr .= '!' . $this->getDatePreference();
1713 }
1714 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1715 $confstr .= '!' . $this->getOption( 'language' );
1716 $confstr .= '!' . $this->getOption( 'thumbsize' );
1717 // add in language specific options, if any
1718 $extra = $wgContLang->getExtraHashOptions();
1719 $confstr .= $extra;
1720
1721 // Give a chance for extensions to modify the hash, if they have
1722 // extra options or other effects on the parser cache.
1723 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1724
1725 $this->mHash = $confstr;
1726 return $confstr;
1727 }
1728
1729 function isBlockedFromCreateAccount() {
1730 $this->getBlockedStatus();
1731 return $this->mBlock && $this->mBlock->mCreateAccount;
1732 }
1733
1734 function isAllowedToCreateAccount() {
1735 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
1736 }
1737
1738 /**
1739 * Set mDataLoaded, return previous value
1740 * Use this to prevent DB access in command-line scripts or similar situations
1741 */
1742 function setLoaded( $loaded ) {
1743 return wfSetVar( $this->mDataLoaded, $loaded );
1744 }
1745
1746 /**
1747 * Get this user's personal page title.
1748 *
1749 * @return Title
1750 * @public
1751 */
1752 function getUserPage() {
1753 return Title::makeTitle( NS_USER, $this->getName() );
1754 }
1755
1756 /**
1757 * Get this user's talk page title.
1758 *
1759 * @return Title
1760 * @public
1761 */
1762 function getTalkPage() {
1763 $title = $this->getUserPage();
1764 return $title->getTalkPage();
1765 }
1766
1767 /**
1768 * @static
1769 */
1770 function getMaxID() {
1771 static $res; // cache
1772
1773 if ( isset( $res ) )
1774 return $res;
1775 else {
1776 $dbr =& wfGetDB( DB_SLAVE );
1777 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1778 }
1779 }
1780
1781 /**
1782 * Determine whether the user is a newbie. Newbies are either
1783 * anonymous IPs, or the most recently created accounts.
1784 * @return bool True if it is a newbie.
1785 */
1786 function isNewbie() {
1787 return !$this->isAllowed( 'autoconfirmed' );
1788 }
1789
1790 /**
1791 * Check to see if the given clear-text password is one of the accepted passwords
1792 * @param string $password User password.
1793 * @return bool True if the given password is correct otherwise False.
1794 */
1795 function checkPassword( $password ) {
1796 global $wgAuth, $wgMinimalPasswordLength;
1797 $this->loadFromDatabase();
1798
1799 // Even though we stop people from creating passwords that
1800 // are shorter than this, doesn't mean people wont be able
1801 // to. Certain authentication plugins do NOT want to save
1802 // domain passwords in a mysql database, so we should
1803 // check this (incase $wgAuth->strict() is false).
1804 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1805 return false;
1806 }
1807
1808 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1809 return true;
1810 } elseif( $wgAuth->strict() ) {
1811 /* Auth plugin doesn't allow local authentication */
1812 return false;
1813 }
1814 $ep = $this->encryptPassword( $password );
1815 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1816 return true;
1817 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1818 return true;
1819 } elseif ( function_exists( 'iconv' ) ) {
1820 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1821 # Check for this with iconv
1822 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1823 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1824 return true;
1825 }
1826 }
1827 return false;
1828 }
1829
1830 /**
1831 * Initialize (if necessary) and return a session token value
1832 * which can be used in edit forms to show that the user's
1833 * login credentials aren't being hijacked with a foreign form
1834 * submission.
1835 *
1836 * @param mixed $salt - Optional function-specific data for hash.
1837 * Use a string or an array of strings.
1838 * @return string
1839 * @public
1840 */
1841 function editToken( $salt = '' ) {
1842 if( !isset( $_SESSION['wsEditToken'] ) ) {
1843 $token = $this->generateToken();
1844 $_SESSION['wsEditToken'] = $token;
1845 } else {
1846 $token = $_SESSION['wsEditToken'];
1847 }
1848 if( is_array( $salt ) ) {
1849 $salt = implode( '|', $salt );
1850 }
1851 return md5( $token . $salt );
1852 }
1853
1854 /**
1855 * Generate a hex-y looking random token for various uses.
1856 * Could be made more cryptographically sure if someone cares.
1857 * @return string
1858 */
1859 function generateToken( $salt = '' ) {
1860 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1861 return md5( $token . $salt );
1862 }
1863
1864 /**
1865 * Check given value against the token value stored in the session.
1866 * A match should confirm that the form was submitted from the
1867 * user's own login session, not a form submission from a third-party
1868 * site.
1869 *
1870 * @param string $val - the input value to compare
1871 * @param string $salt - Optional function-specific data for hash
1872 * @return bool
1873 * @public
1874 */
1875 function matchEditToken( $val, $salt = '' ) {
1876 global $wgMemc;
1877 $sessionToken = $this->editToken( $salt );
1878 if ( $val != $sessionToken ) {
1879 wfDebug( "User::matchEditToken: broken session data\n" );
1880 }
1881 return $val == $sessionToken;
1882 }
1883
1884 /**
1885 * Generate a new e-mail confirmation token and send a confirmation
1886 * mail to the user's given address.
1887 *
1888 * @return mixed True on success, a WikiError object on failure.
1889 */
1890 function sendConfirmationMail() {
1891 global $wgContLang;
1892 $url = $this->confirmationTokenUrl( $expiration );
1893 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1894 wfMsg( 'confirmemail_body',
1895 wfGetIP(),
1896 $this->getName(),
1897 $url,
1898 $wgContLang->timeanddate( $expiration, false ) ) );
1899 }
1900
1901 /**
1902 * Send an e-mail to this user's account. Does not check for
1903 * confirmed status or validity.
1904 *
1905 * @param string $subject
1906 * @param string $body
1907 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1908 * @return mixed True on success, a WikiError object on failure.
1909 */
1910 function sendMail( $subject, $body, $from = null ) {
1911 if( is_null( $from ) ) {
1912 global $wgPasswordSender;
1913 $from = $wgPasswordSender;
1914 }
1915
1916 require_once( 'UserMailer.php' );
1917 $to = new MailAddress( $this );
1918 $sender = new MailAddress( $from );
1919 $error = userMailer( $to, $sender, $subject, $body );
1920
1921 if( $error == '' ) {
1922 return true;
1923 } else {
1924 return new WikiError( $error );
1925 }
1926 }
1927
1928 /**
1929 * Generate, store, and return a new e-mail confirmation code.
1930 * A hash (unsalted since it's used as a key) is stored.
1931 * @param &$expiration mixed output: accepts the expiration time
1932 * @return string
1933 * @private
1934 */
1935 function confirmationToken( &$expiration ) {
1936 $fname = 'User::confirmationToken';
1937
1938 $now = time();
1939 $expires = $now + 7 * 24 * 60 * 60;
1940 $expiration = wfTimestamp( TS_MW, $expires );
1941
1942 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1943 $hash = md5( $token );
1944
1945 $dbw =& wfGetDB( DB_MASTER );
1946 $dbw->update( 'user',
1947 array( 'user_email_token' => $hash,
1948 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1949 array( 'user_id' => $this->mId ),
1950 $fname );
1951
1952 return $token;
1953 }
1954
1955 /**
1956 * Generate and store a new e-mail confirmation token, and return
1957 * the URL the user can use to confirm.
1958 * @param &$expiration mixed output: accepts the expiration time
1959 * @return string
1960 * @private
1961 */
1962 function confirmationTokenUrl( &$expiration ) {
1963 $token = $this->confirmationToken( $expiration );
1964 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1965 return $title->getFullUrl();
1966 }
1967
1968 /**
1969 * Mark the e-mail address confirmed and save.
1970 */
1971 function confirmEmail() {
1972 $this->loadFromDatabase();
1973 $this->mEmailAuthenticated = wfTimestampNow();
1974 $this->saveSettings();
1975 return true;
1976 }
1977
1978 /**
1979 * Is this user allowed to send e-mails within limits of current
1980 * site configuration?
1981 * @return bool
1982 */
1983 function canSendEmail() {
1984 return $this->isEmailConfirmed();
1985 }
1986
1987 /**
1988 * Is this user allowed to receive e-mails within limits of current
1989 * site configuration?
1990 * @return bool
1991 */
1992 function canReceiveEmail() {
1993 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1994 }
1995
1996 /**
1997 * Is this user's e-mail address valid-looking and confirmed within
1998 * limits of the current site configuration?
1999 *
2000 * If $wgEmailAuthentication is on, this may require the user to have
2001 * confirmed their address by returning a code or using a password
2002 * sent to the address from the wiki.
2003 *
2004 * @return bool
2005 */
2006 function isEmailConfirmed() {
2007 global $wgEmailAuthentication;
2008 $this->loadFromDatabase();
2009 $confirmed = true;
2010 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2011 if( $this->isAnon() )
2012 return false;
2013 if( !$this->isValidEmailAddr( $this->mEmail ) )
2014 return false;
2015 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2016 return false;
2017 return true;
2018 } else {
2019 return $confirmed;
2020 }
2021 }
2022
2023 /**
2024 * @param array $groups list of groups
2025 * @return array list of permission key names for given groups combined
2026 * @static
2027 */
2028 function getGroupPermissions( $groups ) {
2029 global $wgGroupPermissions;
2030 $rights = array();
2031 foreach( $groups as $group ) {
2032 if( isset( $wgGroupPermissions[$group] ) ) {
2033 $rights = array_merge( $rights,
2034 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2035 }
2036 }
2037 return $rights;
2038 }
2039
2040 /**
2041 * @param string $group key name
2042 * @return string localized descriptive name for group, if provided
2043 * @static
2044 */
2045 function getGroupName( $group ) {
2046 $key = "group-$group";
2047 $name = wfMsg( $key );
2048 if( $name == '' || $name == "&lt;$key&gt;" ) {
2049 return $group;
2050 } else {
2051 return $name;
2052 }
2053 }
2054
2055 /**
2056 * @param string $group key name
2057 * @return string localized descriptive name for member of a group, if provided
2058 * @static
2059 */
2060 function getGroupMember( $group ) {
2061 $key = "group-$group-member";
2062 $name = wfMsg( $key );
2063 if( $name == '' || $name == "&lt;$key&gt;" ) {
2064 return $group;
2065 } else {
2066 return $name;
2067 }
2068 }
2069
2070
2071 /**
2072 * Return the set of defined explicit groups.
2073 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2074 * groups are not included, as they are defined
2075 * automatically, not in the database.
2076 * @return array
2077 * @static
2078 */
2079 function getAllGroups() {
2080 global $wgGroupPermissions;
2081 return array_diff(
2082 array_keys( $wgGroupPermissions ),
2083 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
2084 }
2085
2086 /**
2087 * Get the title of a page describing a particular group
2088 *
2089 * @param $group Name of the group
2090 * @return mixed
2091 */
2092 function getGroupPage( $group ) {
2093 $page = wfMsgForContent( 'grouppage-' . $group );
2094 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2095 $title = Title::newFromText( $page );
2096 if( is_object( $title ) )
2097 return $title;
2098 }
2099 return false;
2100 }
2101
2102
2103 }
2104
2105 ?>