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