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