efe6f2b65d8a9def576248cded2fb442348c4e8c
[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, $wgDBname, $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["$wgDBname:limiter:$action:anon"] = $limits['anon'];
679 }
680
681 if( isset( $limits['user'] ) && $id != 0 ) {
682 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
683 }
684 if( $this->isNewbie() ) {
685 if( isset( $limits['newbie'] ) && $id != 0 ) {
686 $keys["$wgDBname: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 ) . ' ' . $wgDBname . ': ' . $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, $wgDBname, $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 = "$wgDBname: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 $wgDBname, $wgMemc;
962 $key = "$wgDBname: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 global $wgDBname;
983 $talks = array();
984 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
985 return $talks;
986
987 if (!$this->getNewtalk())
988 return array();
989 $up = $this->getUserPage();
990 $utp = $up->getTalkPage();
991 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
992 }
993
994
995 /**
996 * Perform a user_newtalk check on current slaves; if the memcached data
997 * is funky we don't want newtalk state to get stuck on save, as that's
998 * damn annoying.
999 *
1000 * @param string $field
1001 * @param mixed $id
1002 * @return bool
1003 * @private
1004 */
1005 function checkNewtalk( $field, $id ) {
1006 $fname = 'User::checkNewtalk';
1007 $dbr =& wfGetDB( DB_SLAVE );
1008 $ok = $dbr->selectField( 'user_newtalk', $field,
1009 array( $field => $id ), $fname );
1010 return $ok !== false;
1011 }
1012
1013 /**
1014 * Add or update the
1015 * @param string $field
1016 * @param mixed $id
1017 * @private
1018 */
1019 function updateNewtalk( $field, $id ) {
1020 $fname = 'User::updateNewtalk';
1021 if( $this->checkNewtalk( $field, $id ) ) {
1022 wfDebug( "$fname already set ($field, $id), ignoring\n" );
1023 return false;
1024 }
1025 $dbw =& wfGetDB( DB_MASTER );
1026 $dbw->insert( 'user_newtalk',
1027 array( $field => $id ),
1028 $fname,
1029 'IGNORE' );
1030 wfDebug( "$fname: set on ($field, $id)\n" );
1031 return true;
1032 }
1033
1034 /**
1035 * Clear the new messages flag for the given user
1036 * @param string $field
1037 * @param mixed $id
1038 * @private
1039 */
1040 function deleteNewtalk( $field, $id ) {
1041 $fname = 'User::deleteNewtalk';
1042 if( !$this->checkNewtalk( $field, $id ) ) {
1043 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
1044 return false;
1045 }
1046 $dbw =& wfGetDB( DB_MASTER );
1047 $dbw->delete( 'user_newtalk',
1048 array( $field => $id ),
1049 $fname );
1050 wfDebug( "$fname: killed on ($field, $id)\n" );
1051 return true;
1052 }
1053
1054 /**
1055 * Update the 'You have new messages!' status.
1056 * @param bool $val
1057 */
1058 function setNewtalk( $val ) {
1059 if( wfReadOnly() ) {
1060 return;
1061 }
1062
1063 $this->loadFromDatabase();
1064 $this->mNewtalk = $val;
1065
1066 $fname = 'User::setNewtalk';
1067
1068 if( $this->isAnon() ) {
1069 $field = 'user_ip';
1070 $id = $this->getName();
1071 } else {
1072 $field = 'user_id';
1073 $id = $this->getId();
1074 }
1075
1076 if( $val ) {
1077 $changed = $this->updateNewtalk( $field, $id );
1078 } else {
1079 $changed = $this->deleteNewtalk( $field, $id );
1080 }
1081
1082 if( $changed ) {
1083 if( $this->isAnon() ) {
1084 // Anons have a separate memcached space, since
1085 // user records aren't kept for them.
1086 global $wgDBname, $wgMemc;
1087 $key = "$wgDBname:newtalk:ip:$val";
1088 $wgMemc->set( $key, $val ? 1 : 0 );
1089 } else {
1090 if( $val ) {
1091 // Make sure the user page is watched, so a notification
1092 // will be sent out if enabled.
1093 $this->addWatch( $this->getTalkPage() );
1094 }
1095 }
1096 $this->invalidateCache();
1097 $this->saveSettings();
1098 }
1099 }
1100
1101 function invalidateCache() {
1102 global $wgClockSkewFudge;
1103 $this->loadFromDatabase();
1104 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
1105 # Don't forget to save the options after this or
1106 # it won't take effect!
1107 }
1108
1109 function validateCache( $timestamp ) {
1110 $this->loadFromDatabase();
1111 return ($timestamp >= $this->mTouched);
1112 }
1113
1114 /**
1115 * Encrypt a password.
1116 * It can eventuall salt a password @see User::addSalt()
1117 * @param string $p clear Password.
1118 * @return string Encrypted password.
1119 */
1120 function encryptPassword( $p ) {
1121 return wfEncryptPassword( $this->mId, $p );
1122 }
1123
1124 # Set the password and reset the random token
1125 function setPassword( $str ) {
1126 $this->loadFromDatabase();
1127 $this->setToken();
1128 $this->mPassword = $this->encryptPassword( $str );
1129 $this->mNewpassword = '';
1130 }
1131
1132 # Set the random token (used for persistent authentication)
1133 function setToken( $token = false ) {
1134 global $wgSecretKey, $wgProxyKey, $wgDBname;
1135 if ( !$token ) {
1136 if ( $wgSecretKey ) {
1137 $key = $wgSecretKey;
1138 } elseif ( $wgProxyKey ) {
1139 $key = $wgProxyKey;
1140 } else {
1141 $key = microtime();
1142 }
1143 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1144 } else {
1145 $this->mToken = $token;
1146 }
1147 }
1148
1149
1150 function setCookiePassword( $str ) {
1151 $this->loadFromDatabase();
1152 $this->mCookiePassword = md5( $str );
1153 }
1154
1155 function setNewpassword( $str ) {
1156 $this->loadFromDatabase();
1157 $this->mNewpassword = $this->encryptPassword( $str );
1158 }
1159
1160 function getEmail() {
1161 $this->loadFromDatabase();
1162 return $this->mEmail;
1163 }
1164
1165 function getEmailAuthenticationTimestamp() {
1166 $this->loadFromDatabase();
1167 return $this->mEmailAuthenticated;
1168 }
1169
1170 function setEmail( $str ) {
1171 $this->loadFromDatabase();
1172 $this->mEmail = $str;
1173 }
1174
1175 function getRealName() {
1176 $this->loadFromDatabase();
1177 return $this->mRealName;
1178 }
1179
1180 function setRealName( $str ) {
1181 $this->loadFromDatabase();
1182 $this->mRealName = $str;
1183 }
1184
1185 /**
1186 * @param string $oname The option to check
1187 * @return string
1188 */
1189 function getOption( $oname ) {
1190 $this->loadFromDatabase();
1191 if ( is_null( $this->mOptions ) ) {
1192 $this->mOptions = User::getDefaultOptions();
1193 }
1194 if ( array_key_exists( $oname, $this->mOptions ) ) {
1195 return trim( $this->mOptions[$oname] );
1196 } else {
1197 return '';
1198 }
1199 }
1200
1201 /**
1202 * Get the user's date preference, including some important migration for
1203 * old user rows.
1204 */
1205 function getDatePreference() {
1206 if ( is_null( $this->mDatePreference ) ) {
1207 global $wgLang;
1208 $value = $this->getOption( 'date' );
1209 $map = $wgLang->getDatePreferenceMigrationMap();
1210 if ( isset( $map[$value] ) ) {
1211 $value = $map[$value];
1212 }
1213 $this->mDatePreference = $value;
1214 }
1215 return $this->mDatePreference;
1216 }
1217
1218 /**
1219 * @param string $oname The option to check
1220 * @return bool False if the option is not selected, true if it is
1221 */
1222 function getBoolOption( $oname ) {
1223 return (bool)$this->getOption( $oname );
1224 }
1225
1226 /**
1227 * Get an option as an integer value from the source string.
1228 * @param string $oname The option to check
1229 * @param int $default Optional value to return if option is unset/blank.
1230 * @return int
1231 */
1232 function getIntOption( $oname, $default=0 ) {
1233 $val = $this->getOption( $oname );
1234 if( $val == '' ) {
1235 $val = $default;
1236 }
1237 return intval( $val );
1238 }
1239
1240 function setOption( $oname, $val ) {
1241 $this->loadFromDatabase();
1242 if ( is_null( $this->mOptions ) ) {
1243 $this->mOptions = User::getDefaultOptions();
1244 }
1245 if ( $oname == 'skin' ) {
1246 # Clear cached skin, so the new one displays immediately in Special:Preferences
1247 unset( $this->mSkin );
1248 }
1249 // Filter out any newlines that may have passed through input validation.
1250 // Newlines are used to separate items in the options blob.
1251 $val = str_replace( "\r\n", "\n", $val );
1252 $val = str_replace( "\r", "\n", $val );
1253 $val = str_replace( "\n", " ", $val );
1254 $this->mOptions[$oname] = $val;
1255 $this->invalidateCache();
1256 }
1257
1258 function getRights() {
1259 $this->loadFromDatabase();
1260 return $this->mRights;
1261 }
1262
1263 /**
1264 * Get the list of explicit group memberships this user has.
1265 * The implicit * and user groups are not included.
1266 * @return array of strings
1267 */
1268 function getGroups() {
1269 $this->loadFromDatabase();
1270 return $this->mGroups;
1271 }
1272
1273 /**
1274 * Get the list of implicit group memberships this user has.
1275 * This includes all explicit groups, plus 'user' if logged in
1276 * and '*' for all accounts.
1277 * @return array of strings
1278 */
1279 function getEffectiveGroups() {
1280 $base = array( '*' );
1281 if( $this->isLoggedIn() ) {
1282 $base[] = 'user';
1283 }
1284 return array_merge( $base, $this->getGroups() );
1285 }
1286
1287 /**
1288 * Add the user to the given group.
1289 * This takes immediate effect.
1290 * @string $group
1291 */
1292 function addGroup( $group ) {
1293 $dbw =& wfGetDB( DB_MASTER );
1294 $dbw->insert( 'user_groups',
1295 array(
1296 'ug_user' => $this->getID(),
1297 'ug_group' => $group,
1298 ),
1299 'User::addGroup',
1300 array( 'IGNORE' ) );
1301
1302 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1303 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1304
1305 $this->invalidateCache();
1306 $this->saveSettings();
1307 }
1308
1309 /**
1310 * Remove the user from the given group.
1311 * This takes immediate effect.
1312 * @string $group
1313 */
1314 function removeGroup( $group ) {
1315 $dbw =& wfGetDB( DB_MASTER );
1316 $dbw->delete( 'user_groups',
1317 array(
1318 'ug_user' => $this->getID(),
1319 'ug_group' => $group,
1320 ),
1321 'User::removeGroup' );
1322
1323 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1324 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1325
1326 $this->invalidateCache();
1327 $this->saveSettings();
1328 }
1329
1330
1331 /**
1332 * A more legible check for non-anonymousness.
1333 * Returns true if the user is not an anonymous visitor.
1334 *
1335 * @return bool
1336 */
1337 function isLoggedIn() {
1338 return( $this->getID() != 0 );
1339 }
1340
1341 /**
1342 * A more legible check for anonymousness.
1343 * Returns true if the user is an anonymous visitor.
1344 *
1345 * @return bool
1346 */
1347 function isAnon() {
1348 return !$this->isLoggedIn();
1349 }
1350
1351 /**
1352 * Whether the user is a bot
1353 * @deprecated
1354 */
1355 function isBot() {
1356 return $this->isAllowed( 'bot' );
1357 }
1358
1359 /**
1360 * Check if user is allowed to access a feature / make an action
1361 * @param string $action Action to be checked
1362 * @return boolean True: action is allowed, False: action should not be allowed
1363 */
1364 function isAllowed($action='') {
1365 if ( $action === '' )
1366 // In the spirit of DWIM
1367 return true;
1368
1369 $this->loadFromDatabase();
1370 return in_array( $action , $this->mRights );
1371 }
1372
1373 /**
1374 * Load a skin if it doesn't exist or return it
1375 * @todo FIXME : need to check the old failback system [AV]
1376 */
1377 function &getSkin() {
1378 global $IP, $wgRequest;
1379 if ( ! isset( $this->mSkin ) ) {
1380 $fname = 'User::getSkin';
1381 wfProfileIn( $fname );
1382
1383 # get the user skin
1384 $userSkin = $this->getOption( 'skin' );
1385 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1386
1387 $this->mSkin =& Skin::newFromKey( $userSkin );
1388 wfProfileOut( $fname );
1389 }
1390 return $this->mSkin;
1391 }
1392
1393 /**#@+
1394 * @param string $title Article title to look at
1395 */
1396
1397 /**
1398 * Check watched status of an article
1399 * @return bool True if article is watched
1400 */
1401 function isWatched( $title ) {
1402 $wl = WatchedItem::fromUserTitle( $this, $title );
1403 return $wl->isWatched();
1404 }
1405
1406 /**
1407 * Watch an article
1408 */
1409 function addWatch( $title ) {
1410 $wl = WatchedItem::fromUserTitle( $this, $title );
1411 $wl->addWatch();
1412 $this->invalidateCache();
1413 }
1414
1415 /**
1416 * Stop watching an article
1417 */
1418 function removeWatch( $title ) {
1419 $wl = WatchedItem::fromUserTitle( $this, $title );
1420 $wl->removeWatch();
1421 $this->invalidateCache();
1422 }
1423
1424 /**
1425 * Clear the user's notification timestamp for the given title.
1426 * If e-notif e-mails are on, they will receive notification mails on
1427 * the next change of the page if it's watched etc.
1428 */
1429 function clearNotification( &$title ) {
1430 global $wgUser, $wgUseEnotif;
1431
1432
1433 if ($title->getNamespace() == NS_USER_TALK &&
1434 $title->getText() == $this->getName() ) {
1435 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1436 return;
1437 $this->setNewtalk( false );
1438 }
1439
1440 if( !$wgUseEnotif ) {
1441 return;
1442 }
1443
1444 if( $this->isAnon() ) {
1445 // Nothing else to do...
1446 return;
1447 }
1448
1449 // Only update the timestamp if the page is being watched.
1450 // The query to find out if it is watched is cached both in memcached and per-invocation,
1451 // and when it does have to be executed, it can be on a slave
1452 // If this is the user's newtalk page, we always update the timestamp
1453 if ($title->getNamespace() == NS_USER_TALK &&
1454 $title->getText() == $wgUser->getName())
1455 {
1456 $watched = true;
1457 } elseif ( $this->getID() == $wgUser->getID() ) {
1458 $watched = $title->userIsWatching();
1459 } else {
1460 $watched = true;
1461 }
1462
1463 // If the page is watched by the user (or may be watched), update the timestamp on any
1464 // any matching rows
1465 if ( $watched ) {
1466 $dbw =& wfGetDB( DB_MASTER );
1467 $success = $dbw->update( 'watchlist',
1468 array( /* SET */
1469 'wl_notificationtimestamp' => NULL
1470 ), array( /* WHERE */
1471 'wl_title' => $title->getDBkey(),
1472 'wl_namespace' => $title->getNamespace(),
1473 'wl_user' => $this->getID()
1474 ), 'User::clearLastVisited'
1475 );
1476 }
1477 }
1478
1479 /**#@-*/
1480
1481 /**
1482 * Resets all of the given user's page-change notification timestamps.
1483 * If e-notif e-mails are on, they will receive notification mails on
1484 * the next change of any watched page.
1485 *
1486 * @param int $currentUser user ID number
1487 * @public
1488 */
1489 function clearAllNotifications( $currentUser ) {
1490 global $wgUseEnotif;
1491 if ( !$wgUseEnotif ) {
1492 $this->setNewtalk( false );
1493 return;
1494 }
1495 if( $currentUser != 0 ) {
1496
1497 $dbw =& wfGetDB( DB_MASTER );
1498 $success = $dbw->update( 'watchlist',
1499 array( /* SET */
1500 'wl_notificationtimestamp' => 0
1501 ), array( /* WHERE */
1502 'wl_user' => $currentUser
1503 ), 'UserMailer::clearAll'
1504 );
1505
1506 # we also need to clear here the "you have new message" notification for the own user_talk page
1507 # This is cleared one page view later in Article::viewUpdates();
1508 }
1509 }
1510
1511 /**
1512 * @private
1513 * @return string Encoding options
1514 */
1515 function encodeOptions() {
1516 if ( is_null( $this->mOptions ) ) {
1517 $this->mOptions = User::getDefaultOptions();
1518 }
1519 $a = array();
1520 foreach ( $this->mOptions as $oname => $oval ) {
1521 array_push( $a, $oname.'='.$oval );
1522 }
1523 $s = implode( "\n", $a );
1524 return $s;
1525 }
1526
1527 /**
1528 * @private
1529 */
1530 function decodeOptions( $str ) {
1531 global $wgLang;
1532
1533 $this->mOptions = array();
1534 $a = explode( "\n", $str );
1535 foreach ( $a as $s ) {
1536 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1537 $this->mOptions[$m[1]] = $m[2];
1538 }
1539 }
1540 }
1541
1542 function setCookies() {
1543 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1544 if ( 0 == $this->mId ) return;
1545 $this->loadFromDatabase();
1546 $exp = time() + $wgCookieExpiration;
1547
1548 $_SESSION['wsUserID'] = $this->mId;
1549 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1550
1551 $_SESSION['wsUserName'] = $this->getName();
1552 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1553
1554 $_SESSION['wsToken'] = $this->mToken;
1555 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1556 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1557 } else {
1558 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1559 }
1560 }
1561
1562 /**
1563 * Logout user
1564 * It will clean the session cookie
1565 */
1566 function logout() {
1567 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1568 $this->loadDefaults();
1569 $this->setLoaded( true );
1570
1571 $_SESSION['wsUserID'] = 0;
1572
1573 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1574 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1575
1576 # Remember when user logged out, to prevent seeing cached pages
1577 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1578 }
1579
1580 /**
1581 * Save object settings into database
1582 */
1583 function saveSettings() {
1584 global $wgMemc, $wgDBname;
1585 $fname = 'User::saveSettings';
1586
1587 if ( wfReadOnly() ) { return; }
1588 if ( 0 == $this->mId ) { return; }
1589
1590 $dbw =& wfGetDB( DB_MASTER );
1591 $dbw->update( 'user',
1592 array( /* SET */
1593 'user_name' => $this->mName,
1594 'user_password' => $this->mPassword,
1595 'user_newpassword' => $this->mNewpassword,
1596 'user_real_name' => $this->mRealName,
1597 'user_email' => $this->mEmail,
1598 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1599 'user_options' => $this->encodeOptions(),
1600 'user_touched' => $dbw->timestamp($this->mTouched),
1601 'user_token' => $this->mToken
1602 ), array( /* WHERE */
1603 'user_id' => $this->mId
1604 ), $fname
1605 );
1606 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1607 }
1608
1609
1610 /**
1611 * Checks if a user with the given name exists, returns the ID
1612 */
1613 function idForName() {
1614 $fname = 'User::idForName';
1615
1616 $gotid = 0;
1617 $s = trim( $this->getName() );
1618 if ( 0 == strcmp( '', $s ) ) return 0;
1619
1620 $dbr =& wfGetDB( DB_SLAVE );
1621 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1622 if ( $id === false ) {
1623 $id = 0;
1624 }
1625 return $id;
1626 }
1627
1628 /**
1629 * Add user object to the database
1630 */
1631 function addToDatabase() {
1632 $fname = 'User::addToDatabase';
1633 $dbw =& wfGetDB( DB_MASTER );
1634 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1635 $dbw->insert( 'user',
1636 array(
1637 'user_id' => $seqVal,
1638 'user_name' => $this->mName,
1639 'user_password' => $this->mPassword,
1640 'user_newpassword' => $this->mNewpassword,
1641 'user_email' => $this->mEmail,
1642 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1643 'user_real_name' => $this->mRealName,
1644 'user_options' => $this->encodeOptions(),
1645 'user_token' => $this->mToken,
1646 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1647 ), $fname
1648 );
1649 $this->mId = $dbw->insertId();
1650 }
1651
1652 function spreadBlock() {
1653 # If the (non-anonymous) user is blocked, this function will block any IP address
1654 # that they successfully log on from.
1655 $fname = 'User::spreadBlock';
1656
1657 wfDebug( "User:spreadBlock()\n" );
1658 if ( $this->mId == 0 ) {
1659 return;
1660 }
1661
1662 $userblock = Block::newFromDB( '', $this->mId );
1663 if ( !$userblock ) {
1664 return;
1665 }
1666
1667 # Check if this IP address is already blocked
1668 $ipblock = Block::newFromDB( wfGetIP() );
1669 if ( $ipblock ) {
1670 # If the user is already blocked. Then check if the autoblock would
1671 # excede the user block. If it would excede, then do nothing, else
1672 # prolong block time
1673 if ($userblock->mExpiry &&
1674 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1675 return;
1676 }
1677 # Just update the timestamp
1678 $ipblock->updateTimestamp();
1679 return;
1680 } else {
1681 $ipblock = new Block;
1682 }
1683
1684 # Make a new block object with the desired properties
1685 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1686 $ipblock->mAddress = wfGetIP();
1687 $ipblock->mUser = 0;
1688 $ipblock->mBy = $userblock->mBy;
1689 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1690 $ipblock->mTimestamp = wfTimestampNow();
1691 $ipblock->mAuto = 1;
1692 # If the user is already blocked with an expiry date, we don't
1693 # want to pile on top of that!
1694 if($userblock->mExpiry) {
1695 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1696 } else {
1697 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1698 }
1699
1700 # Insert it
1701 $ipblock->insert();
1702
1703 }
1704
1705 /**
1706 * Generate a string which will be different for any combination of
1707 * user options which would produce different parser output.
1708 * This will be used as part of the hash key for the parser cache,
1709 * so users will the same options can share the same cached data
1710 * safely.
1711 *
1712 * Extensions which require it should install 'PageRenderingHash' hook,
1713 * which will give them a chance to modify this key based on their own
1714 * settings.
1715 *
1716 * @return string
1717 */
1718 function getPageRenderingHash() {
1719 global $wgContLang, $wgUseDynamicDates;
1720 if( $this->mHash ){
1721 return $this->mHash;
1722 }
1723
1724 // stubthreshold is only included below for completeness,
1725 // it will always be 0 when this function is called by parsercache.
1726
1727 $confstr = $this->getOption( 'math' );
1728 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1729 if ( $wgUseDynamicDates ) {
1730 $confstr .= '!' . $this->getDatePreference();
1731 }
1732 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1733 $confstr .= '!' . $this->getOption( 'language' );
1734 $confstr .= '!' . $this->getOption( 'thumbsize' );
1735 // add in language specific options, if any
1736 $extra = $wgContLang->getExtraHashOptions();
1737 $confstr .= $extra;
1738
1739 // Give a chance for extensions to modify the hash, if they have
1740 // extra options or other effects on the parser cache.
1741 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1742
1743 $this->mHash = $confstr;
1744 return $confstr;
1745 }
1746
1747 function isBlockedFromCreateAccount() {
1748 $this->getBlockedStatus();
1749 return $this->mBlock && $this->mBlock->mCreateAccount;
1750 }
1751
1752 function isAllowedToCreateAccount() {
1753 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
1754 }
1755
1756 /**
1757 * Set mDataLoaded, return previous value
1758 * Use this to prevent DB access in command-line scripts or similar situations
1759 */
1760 function setLoaded( $loaded ) {
1761 return wfSetVar( $this->mDataLoaded, $loaded );
1762 }
1763
1764 /**
1765 * Get this user's personal page title.
1766 *
1767 * @return Title
1768 * @public
1769 */
1770 function getUserPage() {
1771 return Title::makeTitle( NS_USER, $this->getName() );
1772 }
1773
1774 /**
1775 * Get this user's talk page title.
1776 *
1777 * @return Title
1778 * @public
1779 */
1780 function getTalkPage() {
1781 $title = $this->getUserPage();
1782 return $title->getTalkPage();
1783 }
1784
1785 /**
1786 * @static
1787 */
1788 function getMaxID() {
1789 static $res; // cache
1790
1791 if ( isset( $res ) )
1792 return $res;
1793 else {
1794 $dbr =& wfGetDB( DB_SLAVE );
1795 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1796 }
1797 }
1798
1799 /**
1800 * Determine whether the user is a newbie. Newbies are either
1801 * anonymous IPs, or the most recently created accounts.
1802 * @return bool True if it is a newbie.
1803 */
1804 function isNewbie() {
1805 return !$this->isAllowed( 'autoconfirmed' );
1806 }
1807
1808 /**
1809 * Check to see if the given clear-text password is one of the accepted passwords
1810 * @param string $password User password.
1811 * @return bool True if the given password is correct otherwise False.
1812 */
1813 function checkPassword( $password ) {
1814 global $wgAuth, $wgMinimalPasswordLength;
1815 $this->loadFromDatabase();
1816
1817 // Even though we stop people from creating passwords that
1818 // are shorter than this, doesn't mean people wont be able
1819 // to. Certain authentication plugins do NOT want to save
1820 // domain passwords in a mysql database, so we should
1821 // check this (incase $wgAuth->strict() is false).
1822 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1823 return false;
1824 }
1825
1826 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1827 return true;
1828 } elseif( $wgAuth->strict() ) {
1829 /* Auth plugin doesn't allow local authentication */
1830 return false;
1831 }
1832 $ep = $this->encryptPassword( $password );
1833 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1834 return true;
1835 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1836 return true;
1837 } elseif ( function_exists( 'iconv' ) ) {
1838 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1839 # Check for this with iconv
1840 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1841 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1842 return true;
1843 }
1844 }
1845 return false;
1846 }
1847
1848 /**
1849 * Initialize (if necessary) and return a session token value
1850 * which can be used in edit forms to show that the user's
1851 * login credentials aren't being hijacked with a foreign form
1852 * submission.
1853 *
1854 * @param mixed $salt - Optional function-specific data for hash.
1855 * Use a string or an array of strings.
1856 * @return string
1857 * @public
1858 */
1859 function editToken( $salt = '' ) {
1860 if( !isset( $_SESSION['wsEditToken'] ) ) {
1861 $token = $this->generateToken();
1862 $_SESSION['wsEditToken'] = $token;
1863 } else {
1864 $token = $_SESSION['wsEditToken'];
1865 }
1866 if( is_array( $salt ) ) {
1867 $salt = implode( '|', $salt );
1868 }
1869 return md5( $token . $salt );
1870 }
1871
1872 /**
1873 * Generate a hex-y looking random token for various uses.
1874 * Could be made more cryptographically sure if someone cares.
1875 * @return string
1876 */
1877 function generateToken( $salt = '' ) {
1878 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1879 return md5( $token . $salt );
1880 }
1881
1882 /**
1883 * Check given value against the token value stored in the session.
1884 * A match should confirm that the form was submitted from the
1885 * user's own login session, not a form submission from a third-party
1886 * site.
1887 *
1888 * @param string $val - the input value to compare
1889 * @param string $salt - Optional function-specific data for hash
1890 * @return bool
1891 * @public
1892 */
1893 function matchEditToken( $val, $salt = '' ) {
1894 global $wgMemc;
1895 $sessionToken = $this->editToken( $salt );
1896 if ( $val != $sessionToken ) {
1897 wfDebug( "User::matchEditToken: broken session data\n" );
1898 }
1899 return $val == $sessionToken;
1900 }
1901
1902 /**
1903 * Generate a new e-mail confirmation token and send a confirmation
1904 * mail to the user's given address.
1905 *
1906 * @return mixed True on success, a WikiError object on failure.
1907 */
1908 function sendConfirmationMail() {
1909 global $wgContLang;
1910 $url = $this->confirmationTokenUrl( $expiration );
1911 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1912 wfMsg( 'confirmemail_body',
1913 wfGetIP(),
1914 $this->getName(),
1915 $url,
1916 $wgContLang->timeanddate( $expiration, false ) ) );
1917 }
1918
1919 /**
1920 * Send an e-mail to this user's account. Does not check for
1921 * confirmed status or validity.
1922 *
1923 * @param string $subject
1924 * @param string $body
1925 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1926 * @return mixed True on success, a WikiError object on failure.
1927 */
1928 function sendMail( $subject, $body, $from = null ) {
1929 if( is_null( $from ) ) {
1930 global $wgPasswordSender;
1931 $from = $wgPasswordSender;
1932 }
1933
1934 require_once( 'UserMailer.php' );
1935 $to = new MailAddress( $this );
1936 $sender = new MailAddress( $from );
1937 $error = userMailer( $to, $sender, $subject, $body );
1938
1939 if( $error == '' ) {
1940 return true;
1941 } else {
1942 return new WikiError( $error );
1943 }
1944 }
1945
1946 /**
1947 * Generate, store, and return a new e-mail confirmation code.
1948 * A hash (unsalted since it's used as a key) is stored.
1949 * @param &$expiration mixed output: accepts the expiration time
1950 * @return string
1951 * @private
1952 */
1953 function confirmationToken( &$expiration ) {
1954 $fname = 'User::confirmationToken';
1955
1956 $now = time();
1957 $expires = $now + 7 * 24 * 60 * 60;
1958 $expiration = wfTimestamp( TS_MW, $expires );
1959
1960 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1961 $hash = md5( $token );
1962
1963 $dbw =& wfGetDB( DB_MASTER );
1964 $dbw->update( 'user',
1965 array( 'user_email_token' => $hash,
1966 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1967 array( 'user_id' => $this->mId ),
1968 $fname );
1969
1970 return $token;
1971 }
1972
1973 /**
1974 * Generate and store a new e-mail confirmation token, and return
1975 * the URL the user can use to confirm.
1976 * @param &$expiration mixed output: accepts the expiration time
1977 * @return string
1978 * @private
1979 */
1980 function confirmationTokenUrl( &$expiration ) {
1981 $token = $this->confirmationToken( $expiration );
1982 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1983 return $title->getFullUrl();
1984 }
1985
1986 /**
1987 * Mark the e-mail address confirmed and save.
1988 */
1989 function confirmEmail() {
1990 $this->loadFromDatabase();
1991 $this->mEmailAuthenticated = wfTimestampNow();
1992 $this->saveSettings();
1993 return true;
1994 }
1995
1996 /**
1997 * Is this user allowed to send e-mails within limits of current
1998 * site configuration?
1999 * @return bool
2000 */
2001 function canSendEmail() {
2002 return $this->isEmailConfirmed();
2003 }
2004
2005 /**
2006 * Is this user allowed to receive e-mails within limits of current
2007 * site configuration?
2008 * @return bool
2009 */
2010 function canReceiveEmail() {
2011 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
2012 }
2013
2014 /**
2015 * Is this user's e-mail address valid-looking and confirmed within
2016 * limits of the current site configuration?
2017 *
2018 * If $wgEmailAuthentication is on, this may require the user to have
2019 * confirmed their address by returning a code or using a password
2020 * sent to the address from the wiki.
2021 *
2022 * @return bool
2023 */
2024 function isEmailConfirmed() {
2025 global $wgEmailAuthentication;
2026 $this->loadFromDatabase();
2027 $confirmed = true;
2028 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
2029 if( $this->isAnon() )
2030 return false;
2031 if( !$this->isValidEmailAddr( $this->mEmail ) )
2032 return false;
2033 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
2034 return false;
2035 return true;
2036 } else {
2037 return $confirmed;
2038 }
2039 }
2040
2041 /**
2042 * @param array $groups list of groups
2043 * @return array list of permission key names for given groups combined
2044 * @static
2045 */
2046 static function getGroupPermissions( $groups ) {
2047 global $wgGroupPermissions;
2048 $rights = array();
2049 foreach( $groups as $group ) {
2050 if( isset( $wgGroupPermissions[$group] ) ) {
2051 $rights = array_merge( $rights,
2052 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
2053 }
2054 }
2055 return $rights;
2056 }
2057
2058 /**
2059 * @param string $group key name
2060 * @return string localized descriptive name for group, if provided
2061 * @static
2062 */
2063 static function getGroupName( $group ) {
2064 $key = "group-$group";
2065 $name = wfMsg( $key );
2066 if( $name == '' || wfEmptyMsg( $key, $name ) ) {
2067 return $group;
2068 } else {
2069 return $name;
2070 }
2071 }
2072
2073 /**
2074 * @param string $group key name
2075 * @return string localized descriptive name for member of a group, if provided
2076 * @static
2077 */
2078 static function getGroupMember( $group ) {
2079 $key = "group-$group-member";
2080 $name = wfMsg( $key );
2081 if( $name == '' || wfEmptyMsg( $key, $name ) ) {
2082 return $group;
2083 } else {
2084 return $name;
2085 }
2086 }
2087
2088 /**
2089 * Return the set of defined explicit groups.
2090 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
2091 * groups are not included, as they are defined
2092 * automatically, not in the database.
2093 * @return array
2094 * @static
2095 */
2096 static function getAllGroups() {
2097 global $wgGroupPermissions;
2098 return array_diff(
2099 array_keys( $wgGroupPermissions ),
2100 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
2101 }
2102
2103 /**
2104 * Get the title of a page describing a particular group
2105 *
2106 * @param $group Name of the group
2107 * @return mixed
2108 */
2109 static function getGroupPage( $group ) {
2110 $page = wfMsgForContent( 'grouppage-' . $group );
2111 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
2112 $title = Title::newFromText( $page );
2113 if( is_object( $title ) )
2114 return $title;
2115 }
2116 return false;
2117 }
2118
2119 /**
2120 * Create a link to the group in HTML, if available
2121 *
2122 * @param $group Name of the group
2123 * @param $text The text of the link
2124 * @return mixed
2125 */
2126 static function makeGroupLinkHTML( $group, $text = '' ) {
2127 if( $text == '' ) {
2128 $text = self::getGroupName( $group );
2129 }
2130 $title = self::getGroupPage( $group );
2131 if( $title ) {
2132 global $wgUser;
2133 $sk = $wgUser->getSkin();
2134 return $sk->makeLinkObj( $title, $text );
2135 } else {
2136 return $text;
2137 }
2138 }
2139
2140 /**
2141 * Create a link to the group in Wikitext, if available
2142 *
2143 * @param $group Name of the group
2144 * @param $text The text of the link (by default, the name of the group)
2145 * @return mixed
2146 */
2147 static function makeGroupLinkWiki( $group, $text = '' ) {
2148 if( $text == '' ) {
2149 $text = self::getGroupName( $group );
2150 }
2151 $title = self::getGroupPage( $group );
2152 if( $title ) {
2153 $page = $title->getPrefixedText();
2154 return "[[$page|$text]]";
2155 } else {
2156 return $text;
2157 }
2158 }
2159 }
2160
2161 ?>