Groups which won't hit the rate limiter now configurable with $wgRateLimitsExcludedGroups
[lhc/web/wiklou.git] / includes / User.php
1 <?php
2 /**
3 * See user.txt
4 *
5 * @package MediaWiki
6 */
7
8 /**
9 *
10 */
11 require_once( 'WatchedItem.php' );
12
13 # Number of characters in user_token field
14 define( 'USER_TOKEN_LENGTH', 32 );
15
16 # Serialized record version
17 define( 'MW_USER_VERSION', 3 );
18
19 /**
20 *
21 * @package MediaWiki
22 */
23 class User {
24 /*
25 * When adding a new private variable, dont forget to add it to __sleep()
26 */
27 /**@{{
28 * @private
29 */
30 var $mBlockedby; //!<
31 var $mBlockreason; //!<
32 var $mDataLoaded; //!<
33 var $mEmail; //!<
34 var $mEmailAuthenticated; //!<
35 var $mGroups; //!<
36 var $mHash; //!<
37 var $mId; //!<
38 var $mName; //!<
39 var $mNewpassword; //!<
40 var $mNewtalk; //!<
41 var $mOptions; //!<
42 var $mPassword; //!<
43 var $mRealName; //!<
44 var $mRegistration; //!<
45 var $mRights; //!<
46 var $mSkin; //!<
47 var $mToken; //!<
48 var $mTouched; //!<
49 var $mVersion; //!< serialized version
50 /**@}} */
51
52 /** Constructor using User:loadDefaults() */
53 function User() {
54 $this->loadDefaults();
55 $this->mVersion = MW_USER_VERSION;
56 }
57
58 /**
59 * Static factory method
60 * @param string $name Username, validated by Title:newFromText()
61 * @return User
62 * @static
63 */
64 function newFromName( $name ) {
65 # Force usernames to capital
66 global $wgContLang;
67 $name = $wgContLang->ucfirst( $name );
68
69 # Clean up name according to title rules
70 $t = Title::newFromText( $name );
71 if( is_null( $t ) ) {
72 return null;
73 }
74
75 # Reject various classes of invalid names
76 $canonicalName = $t->getText();
77 global $wgAuth;
78 $canonicalName = $wgAuth->getCanonicalName( $t->getText() );
79
80 if( !User::isValidUserName( $canonicalName ) ) {
81 return null;
82 }
83
84 $u = new User();
85 $u->setName( $canonicalName );
86 $u->setId( $u->idFromName( $canonicalName ) );
87 return $u;
88 }
89
90 /**
91 * Factory method to fetch whichever use has a given email confirmation code.
92 * This code is generated when an account is created or its e-mail address
93 * has changed.
94 *
95 * If the code is invalid or has expired, returns NULL.
96 *
97 * @param string $code
98 * @return User
99 * @static
100 */
101 function newFromConfirmationCode( $code ) {
102 $dbr =& wfGetDB( DB_SLAVE );
103 $name = $dbr->selectField( 'user', 'user_name', array(
104 'user_email_token' => md5( $code ),
105 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
106 ) );
107 if( is_string( $name ) ) {
108 return User::newFromName( $name );
109 } else {
110 return null;
111 }
112 }
113
114 /**
115 * Serialze sleep function, for better cache efficiency and avoidance of
116 * silly "incomplete type" errors when skins are cached. The array should
117 * contain names of private variables (see at top of User.php).
118 */
119 function __sleep() {
120 return array(
121 'mBlockedby',
122 'mBlockreason',
123 'mDataLoaded',
124 'mEmail',
125 'mEmailAuthenticated',
126 'mGroups',
127 'mHash',
128 'mId',
129 'mName',
130 'mNewpassword',
131 'mNewtalk',
132 'mOptions',
133 'mPassword',
134 'mRealName',
135 'mRegistration',
136 'mRights',
137 'mToken',
138 'mTouched',
139 'mVersion',
140 );
141 }
142
143 /**
144 * Get username given an id.
145 * @param integer $id Database user id
146 * @return string Nickname of a user
147 * @static
148 */
149 function whoIs( $id ) {
150 $dbr =& wfGetDB( DB_SLAVE );
151 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
152 }
153
154 /**
155 * Get real username given an id.
156 * @param integer $id Database user id
157 * @return string Realname of a user
158 * @static
159 */
160 function whoIsReal( $id ) {
161 $dbr =& wfGetDB( DB_SLAVE );
162 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
163 }
164
165 /**
166 * Get database id given a user name
167 * @param string $name Nickname of a user
168 * @return integer|null Database user id (null: if non existent
169 * @static
170 */
171 function idFromName( $name ) {
172 $fname = "User::idFromName";
173
174 $nt = Title::newFromText( $name );
175 if( is_null( $nt ) ) {
176 # Illegal name
177 return null;
178 }
179 $dbr =& wfGetDB( DB_SLAVE );
180 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
181
182 if ( $s === false ) {
183 return 0;
184 } else {
185 return $s->user_id;
186 }
187 }
188
189 /**
190 * does the string match an anonymous IPv4 address?
191 *
192 * Note: We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
193 * address because the usemod software would "cloak" anonymous IP
194 * addresses like this, if we allowed accounts like this to be created
195 * new users could get the old edits of these anonymous users.
196 *
197 * @bug 3631
198 *
199 * @static
200 * @param string $name Nickname of a user
201 * @return bool
202 */
203 function isIP( $name ) {
204 return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/",$name);
205 /*return preg_match("/^
206 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
207 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
208 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
209 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
210 $/x", $name);*/
211 }
212
213 /**
214 * Is the input a valid username?
215 *
216 * Checks if the input is a valid username, we don't want an empty string,
217 * an IP address, anything that containins slashes (would mess up subpages),
218 * is longer than the maximum allowed username size or doesn't begin with
219 * a capital letter.
220 *
221 * @param string $name
222 * @return bool
223 * @static
224 */
225 function isValidUserName( $name ) {
226 global $wgContLang, $wgMaxNameChars;
227
228 if ( $name == ''
229 || User::isIP( $name )
230 || strpos( $name, '/' ) !== false
231 || strlen( $name ) > $wgMaxNameChars
232 || $name != $wgContLang->ucfirst( $name ) )
233 return false;
234
235 // Ensure that the name can't be misresolved as a different title,
236 // such as with extra namespace keys at the start.
237 $parsed = Title::newFromText( $name );
238 if( is_null( $parsed )
239 || $parsed->getNamespace()
240 || strcmp( $name, $parsed->getPrefixedText() ) )
241 return false;
242
243 // Check an additional blacklist of troublemaker characters.
244 // Should these be merged into the title char list?
245 $unicodeBlacklist = '/[' .
246 '\x{0080}-\x{009f}' . # iso-8859-1 control chars
247 '\x{00a0}' . # non-breaking space
248 '\x{2000}-\x{200f}' . # various whitespace
249 '\x{2028}-\x{202f}' . # breaks and control chars
250 '\x{3000}' . # ideographic space
251 '\x{e000}-\x{f8ff}' . # private use
252 ']/u';
253 if( preg_match( $unicodeBlacklist, $name ) ) {
254 return false;
255 }
256
257 return true;
258 }
259
260 /**
261 * Is the input a valid password?
262 *
263 * @param string $password
264 * @return bool
265 * @static
266 */
267 function isValidPassword( $password ) {
268 global $wgMinimalPasswordLength;
269 return strlen( $password ) >= $wgMinimalPasswordLength;
270 }
271
272 /**
273 * Does the string match roughly an email address ?
274 *
275 * There used to be a regular expression here, it got removed because it
276 * rejected valid addresses. Actually just check if there is '@' somewhere
277 * in the given address.
278 *
279 * @todo Check for RFC 2822 compilance
280 * @bug 959
281 *
282 * @param string $addr email address
283 * @static
284 * @return bool
285 */
286 function isValidEmailAddr ( $addr ) {
287 return ( trim( $addr ) != '' ) &&
288 (false !== strpos( $addr, '@' ) );
289 }
290
291 /**
292 * Count the number of edits of a user
293 *
294 * @param int $uid The user ID to check
295 * @return int
296 */
297 function edits( $uid ) {
298 $fname = 'User::edits';
299
300 $dbr =& wfGetDB( DB_SLAVE );
301 return $dbr->selectField(
302 'revision', 'count(*)',
303 array( 'rev_user' => $uid ),
304 $fname
305 );
306 }
307
308 /**
309 * probably return a random password
310 * @return string probably a random password
311 * @static
312 * @todo Check what is doing really [AV]
313 */
314 function randomPassword() {
315 global $wgMinimalPasswordLength;
316 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
317 $l = strlen( $pwchars ) - 1;
318
319 $pwlength = max( 7, $wgMinimalPasswordLength );
320 $digit = mt_rand(0, $pwlength - 1);
321 $np = '';
322 for ( $i = 0; $i < $pwlength; $i++ ) {
323 $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
324 }
325 return $np;
326 }
327
328 /**
329 * Set properties to default
330 * Used at construction. It will load per language default settings only
331 * if we have an available language object.
332 */
333 function loadDefaults() {
334 static $n=0;
335 $n++;
336 $fname = 'User::loadDefaults' . $n;
337 wfProfileIn( $fname );
338
339 global $wgCookiePrefix;
340 global $wgNamespacesToBeSearchedDefault;
341
342 $this->mId = 0;
343 $this->mNewtalk = -1;
344 $this->mName = false;
345 $this->mRealName = $this->mEmail = '';
346 $this->mEmailAuthenticated = null;
347 $this->mPassword = $this->mNewpassword = '';
348 $this->mRights = array();
349 $this->mGroups = array();
350 $this->mOptions = User::getDefaultOptions();
351
352 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
353 $this->mOptions['searchNs'.$nsnum] = $val;
354 }
355 unset( $this->mSkin );
356 $this->mDataLoaded = false;
357 $this->mBlockedby = -1; # Unset
358 $this->setToken(); # Random
359 $this->mHash = false;
360
361 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
362 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
363 }
364 else {
365 $this->mTouched = '0'; # Allow any pages to be cached
366 }
367
368 $this->mRegistration = wfTimestamp( TS_MW );
369
370 wfProfileOut( $fname );
371 }
372
373 /**
374 * Combine the language default options with any site-specific options
375 * and add the default language variants.
376 *
377 * @return array
378 * @static
379 * @private
380 */
381 function getDefaultOptions() {
382 /**
383 * Site defaults will override the global/language defaults
384 */
385 global $wgContLang, $wgDefaultUserOptions;
386 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptions();
387
388 /**
389 * default language setting
390 */
391 $variant = $wgContLang->getPreferredVariant();
392 $defOpt['variant'] = $variant;
393 $defOpt['language'] = $variant;
394
395 return $defOpt;
396 }
397
398 /**
399 * Get a given default option value.
400 *
401 * @param string $opt
402 * @return string
403 * @static
404 * @public
405 */
406 function getDefaultOption( $opt ) {
407 $defOpts = User::getDefaultOptions();
408 if( isset( $defOpts[$opt] ) ) {
409 return $defOpts[$opt];
410 } else {
411 return '';
412 }
413 }
414
415 /**
416 * Get blocking information
417 * @private
418 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
419 * non-critical checks are done against slaves. Check when actually saving should be done against
420 * master.
421 */
422 function getBlockedStatus( $bFromSlave = true ) {
423 global $wgEnableSorbs, $wgProxyWhitelist;
424
425 if ( -1 != $this->mBlockedby ) {
426 wfDebug( "User::getBlockedStatus: already loaded.\n" );
427 return;
428 }
429
430 $fname = 'User::getBlockedStatus';
431 wfProfileIn( $fname );
432 wfDebug( "$fname: checking...\n" );
433
434 $this->mBlockedby = 0;
435 $ip = wfGetIP();
436
437 # User/IP blocking
438 $block = new Block();
439 $block->fromMaster( !$bFromSlave );
440 if ( $block->load( $ip , $this->mId ) ) {
441 wfDebug( "$fname: Found block.\n" );
442 $this->mBlockedby = $block->mBy;
443 $this->mBlockreason = $block->mReason;
444 if ( $this->isLoggedIn() ) {
445 $this->spreadBlock();
446 }
447 } else {
448 wfDebug( "$fname: No block.\n" );
449 }
450
451 # Proxy blocking
452 # FIXME ? proxyunbannable is to deprecate the old isSysop()
453 if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
454
455 # Local list
456 if ( wfIsLocallyBlockedProxy( $ip ) ) {
457 $this->mBlockedby = wfMsg( 'proxyblocker' );
458 $this->mBlockreason = wfMsg( 'proxyblockreason' );
459 }
460
461 # DNSBL
462 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
463 if ( $this->inSorbsBlacklist( $ip ) ) {
464 $this->mBlockedby = wfMsg( 'sorbs' );
465 $this->mBlockreason = wfMsg( 'sorbsreason' );
466 }
467 }
468 }
469
470 # Extensions
471 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
472
473 wfProfileOut( $fname );
474 }
475
476 function inSorbsBlacklist( $ip ) {
477 global $wgEnableSorbs;
478 return $wgEnableSorbs &&
479 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
480 }
481
482 function inOpmBlacklist( $ip ) {
483 global $wgEnableOpm;
484 return $wgEnableOpm &&
485 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
486 }
487
488 function inDnsBlacklist( $ip, $base ) {
489 $fname = 'User::inDnsBlacklist';
490 wfProfileIn( $fname );
491
492 $found = false;
493 $host = '';
494
495 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
496 # Make hostname
497 for ( $i=4; $i>=1; $i-- ) {
498 $host .= $m[$i] . '.';
499 }
500 $host .= $base;
501
502 # Send query
503 $ipList = gethostbynamel( $host );
504
505 if ( $ipList ) {
506 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
507 $found = true;
508 } else {
509 wfDebug( "Requested $host, not found in $base.\n" );
510 }
511 }
512
513 wfProfileOut( $fname );
514 return $found;
515 }
516
517 /**
518 * Primitive rate limits: enforce maximum actions per time period
519 * to put a brake on flooding.
520 *
521 * Note: when using a shared cache like memcached, IP-address
522 * last-hit counters will be shared across wikis.
523 *
524 * @return bool true if a rate limiter was tripped
525 * @public
526 */
527 function pingLimiter( $action='edit' ) {
528 global $wgRateLimits, $wgRateLimitsExcludedGroups;
529 if( !isset( $wgRateLimits[$action] ) ) {
530 return false;
531 }
532
533 # Some groups shouldn't trigger the ping limiter, ever
534 foreach( $this->getGroups() as $group ) {
535 if( array_search( $group, $wgRateLimitsExcludedGroups ) !== false )
536 return false;
537 }
538
539 global $wgMemc, $wgDBname, $wgRateLimitLog;
540 $fname = 'User::pingLimiter';
541 wfProfileIn( $fname );
542
543 $limits = $wgRateLimits[$action];
544 $keys = array();
545 $id = $this->getId();
546 $ip = wfGetIP();
547
548 if( isset( $limits['anon'] ) && $id == 0 ) {
549 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
550 }
551
552 if( isset( $limits['user'] ) && $id != 0 ) {
553 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
554 }
555 if( $this->isNewbie() ) {
556 if( isset( $limits['newbie'] ) && $id != 0 ) {
557 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
558 }
559 if( isset( $limits['ip'] ) ) {
560 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
561 }
562 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
563 $subnet = $matches[1];
564 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
565 }
566 }
567
568 $triggered = false;
569 foreach( $keys as $key => $limit ) {
570 list( $max, $period ) = $limit;
571 $summary = "(limit $max in {$period}s)";
572 $count = $wgMemc->get( $key );
573 if( $count ) {
574 if( $count > $max ) {
575 wfDebug( "$fname: tripped! $key at $count $summary\n" );
576 if( $wgRateLimitLog ) {
577 @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
578 }
579 $triggered = true;
580 } else {
581 wfDebug( "$fname: ok. $key at $count $summary\n" );
582 }
583 } else {
584 wfDebug( "$fname: adding record for $key $summary\n" );
585 $wgMemc->add( $key, 1, intval( $period ) );
586 }
587 $wgMemc->incr( $key );
588 }
589
590 wfProfileOut( $fname );
591 return $triggered;
592 }
593
594 /**
595 * Check if user is blocked
596 * @return bool True if blocked, false otherwise
597 */
598 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
599 wfDebug( "User::isBlocked: enter\n" );
600 $this->getBlockedStatus( $bFromSlave );
601 return $this->mBlockedby !== 0;
602 }
603
604 /**
605 * Check if user is blocked from editing a particular article
606 */
607 function isBlockedFrom( $title, $bFromSlave = false ) {
608 global $wgBlockAllowsUTEdit;
609 $fname = 'User::isBlockedFrom';
610 wfProfileIn( $fname );
611 wfDebug( "$fname: enter\n" );
612
613 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
614 $title->getNamespace() == NS_USER_TALK )
615 {
616 $blocked = false;
617 wfDebug( "$fname: self-talk page, ignoring any blocks\n" );
618 } else {
619 wfDebug( "$fname: asking isBlocked()\n" );
620 $blocked = $this->isBlocked( $bFromSlave );
621 }
622 wfProfileOut( $fname );
623 return $blocked;
624 }
625
626 /**
627 * Get name of blocker
628 * @return string name of blocker
629 */
630 function blockedBy() {
631 $this->getBlockedStatus();
632 return $this->mBlockedby;
633 }
634
635 /**
636 * Get blocking reason
637 * @return string Blocking reason
638 */
639 function blockedFor() {
640 $this->getBlockedStatus();
641 return $this->mBlockreason;
642 }
643
644 /**
645 * Initialise php session
646 */
647 function SetupSession() {
648 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
649 if( $wgSessionsInMemcached ) {
650 require_once( 'MemcachedSessions.php' );
651 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
652 # If it's left on 'user' or another setting from another
653 # application, it will end up failing. Try to recover.
654 ini_set ( 'session.save_handler', 'files' );
655 }
656 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
657 session_cache_limiter( 'private, must-revalidate' );
658 @session_start();
659 }
660
661 /**
662 * Create a new user object using data from session
663 * @static
664 */
665 function loadFromSession() {
666 global $wgMemc, $wgDBname, $wgCookiePrefix;
667
668 if ( isset( $_SESSION['wsUserID'] ) ) {
669 if ( 0 != $_SESSION['wsUserID'] ) {
670 $sId = $_SESSION['wsUserID'];
671 } else {
672 return new User();
673 }
674 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
675 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
676 $_SESSION['wsUserID'] = $sId;
677 } else {
678 return new User();
679 }
680 if ( isset( $_SESSION['wsUserName'] ) ) {
681 $sName = $_SESSION['wsUserName'];
682 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
683 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
684 $_SESSION['wsUserName'] = $sName;
685 } else {
686 return new User();
687 }
688
689 $passwordCorrect = FALSE;
690 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
691 if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) {
692 # Expire old serialized objects; they may be corrupt.
693 $user = false;
694 }
695 if($makenew = !$user) {
696 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
697 $user = new User();
698 $user->mId = $sId;
699 $user->loadFromDatabase();
700 } else {
701 wfDebug( "User::loadFromSession() got from cache!\n" );
702 }
703
704 if ( isset( $_SESSION['wsToken'] ) ) {
705 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken;
706 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
707 $passwordCorrect = $user->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
708 } else {
709 return new User(); # Can't log in from session
710 }
711
712 if ( ( $sName == $user->mName ) && $passwordCorrect ) {
713 if($makenew) {
714 if($wgMemc->set( $key, $user ))
715 wfDebug( "User::loadFromSession() successfully saved user\n" );
716 else
717 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
718 }
719 return $user;
720 }
721 return new User(); # Can't log in from session
722 }
723
724 /**
725 * Load a user from the database
726 */
727 function loadFromDatabase() {
728 $fname = "User::loadFromDatabase";
729
730 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
731 # loading in a command line script, don't assume all command line scripts need it like this
732 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
733 if ( $this->mDataLoaded ) {
734 return;
735 }
736
737 # Paranoia
738 $this->mId = intval( $this->mId );
739
740 /** Anonymous user */
741 if( !$this->mId ) {
742 /** Get rights */
743 $this->mRights = $this->getGroupPermissions( array( '*' ) );
744 $this->mDataLoaded = true;
745 return;
746 } # the following stuff is for non-anonymous users only
747
748 $dbr =& wfGetDB( DB_SLAVE );
749 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
750 'user_email_authenticated',
751 'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ),
752 array( 'user_id' => $this->mId ), $fname );
753
754 if ( $s !== false ) {
755 $this->mName = $s->user_name;
756 $this->mEmail = $s->user_email;
757 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
758 $this->mRealName = $s->user_real_name;
759 $this->mPassword = $s->user_password;
760 $this->mNewpassword = $s->user_newpassword;
761 $this->decodeOptions( $s->user_options );
762 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
763 $this->mToken = $s->user_token;
764 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
765
766 $res = $dbr->select( 'user_groups',
767 array( 'ug_group' ),
768 array( 'ug_user' => $this->mId ),
769 $fname );
770 $this->mGroups = array();
771 while( $row = $dbr->fetchObject( $res ) ) {
772 $this->mGroups[] = $row->ug_group;
773 }
774 $implicitGroups = array( '*', 'user' );
775
776 global $wgAutoConfirmAge;
777 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
778 if( $accountAge >= $wgAutoConfirmAge ) {
779 $implicitGroups[] = 'autoconfirmed';
780 }
781
782 $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
783 $this->mRights = $this->getGroupPermissions( $effectiveGroups );
784 }
785
786 $this->mDataLoaded = true;
787 }
788
789 function getID() { return $this->mId; }
790 function setID( $v ) {
791 $this->mId = $v;
792 $this->mDataLoaded = false;
793 }
794
795 function getName() {
796 $this->loadFromDatabase();
797 if ( $this->mName === false ) {
798 $this->mName = wfGetIP();
799 }
800 return $this->mName;
801 }
802
803 function setName( $str ) {
804 $this->loadFromDatabase();
805 $this->mName = $str;
806 }
807
808
809 /**
810 * Return the title dbkey form of the name, for eg user pages.
811 * @return string
812 * @public
813 */
814 function getTitleKey() {
815 return str_replace( ' ', '_', $this->getName() );
816 }
817
818 function getNewtalk() {
819 $this->loadFromDatabase();
820
821 # Load the newtalk status if it is unloaded (mNewtalk=-1)
822 if( $this->mNewtalk === -1 ) {
823 $this->mNewtalk = false; # reset talk page status
824
825 # Check memcached separately for anons, who have no
826 # entire User object stored in there.
827 if( !$this->mId ) {
828 global $wgDBname, $wgMemc;
829 $key = "$wgDBname:newtalk:ip:" . $this->getName();
830 $newtalk = $wgMemc->get( $key );
831 if( is_integer( $newtalk ) ) {
832 $this->mNewtalk = (bool)$newtalk;
833 } else {
834 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
835 $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
836 }
837 } else {
838 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
839 }
840 }
841
842 return (bool)$this->mNewtalk;
843 }
844
845 /**
846 * Return the talk page(s) this user has new messages on.
847 */
848 function getNewMessageLinks() {
849 global $wgDBname;
850 $talks = array();
851 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
852 return $talks;
853
854 if (!$this->getNewtalk())
855 return array();
856 $up = $this->getUserPage();
857 $utp = $up->getTalkPage();
858 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
859 }
860
861
862 /**
863 * Perform a user_newtalk check on current slaves; if the memcached data
864 * is funky we don't want newtalk state to get stuck on save, as that's
865 * damn annoying.
866 *
867 * @param string $field
868 * @param mixed $id
869 * @return bool
870 * @private
871 */
872 function checkNewtalk( $field, $id ) {
873 $fname = 'User::checkNewtalk';
874 $dbr =& wfGetDB( DB_SLAVE );
875 $ok = $dbr->selectField( 'user_newtalk', $field,
876 array( $field => $id ), $fname );
877 return $ok !== false;
878 }
879
880 /**
881 * Add or update the
882 * @param string $field
883 * @param mixed $id
884 * @private
885 */
886 function updateNewtalk( $field, $id ) {
887 $fname = 'User::updateNewtalk';
888 if( $this->checkNewtalk( $field, $id ) ) {
889 wfDebug( "$fname already set ($field, $id), ignoring\n" );
890 return false;
891 }
892 $dbw =& wfGetDB( DB_MASTER );
893 $dbw->insert( 'user_newtalk',
894 array( $field => $id ),
895 $fname,
896 'IGNORE' );
897 wfDebug( "$fname: set on ($field, $id)\n" );
898 return true;
899 }
900
901 /**
902 * Clear the new messages flag for the given user
903 * @param string $field
904 * @param mixed $id
905 * @private
906 */
907 function deleteNewtalk( $field, $id ) {
908 $fname = 'User::deleteNewtalk';
909 if( !$this->checkNewtalk( $field, $id ) ) {
910 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
911 return false;
912 }
913 $dbw =& wfGetDB( DB_MASTER );
914 $dbw->delete( 'user_newtalk',
915 array( $field => $id ),
916 $fname );
917 wfDebug( "$fname: killed on ($field, $id)\n" );
918 return true;
919 }
920
921 /**
922 * Update the 'You have new messages!' status.
923 * @param bool $val
924 */
925 function setNewtalk( $val ) {
926 if( wfReadOnly() ) {
927 return;
928 }
929
930 $this->loadFromDatabase();
931 $this->mNewtalk = $val;
932
933 $fname = 'User::setNewtalk';
934
935 if( $this->isAnon() ) {
936 $field = 'user_ip';
937 $id = $this->getName();
938 } else {
939 $field = 'user_id';
940 $id = $this->getId();
941 }
942
943 if( $val ) {
944 $changed = $this->updateNewtalk( $field, $id );
945 } else {
946 $changed = $this->deleteNewtalk( $field, $id );
947 }
948
949 if( $changed ) {
950 if( $this->isAnon() ) {
951 // Anons have a separate memcached space, since
952 // user records aren't kept for them.
953 global $wgDBname, $wgMemc;
954 $key = "$wgDBname:newtalk:ip:$val";
955 $wgMemc->set( $key, $val ? 1 : 0 );
956 } else {
957 if( $val ) {
958 // Make sure the user page is watched, so a notification
959 // will be sent out if enabled.
960 $this->addWatch( $this->getTalkPage() );
961 }
962 }
963 $this->invalidateCache();
964 $this->saveSettings();
965 }
966 }
967
968 function invalidateCache() {
969 global $wgClockSkewFudge;
970 $this->loadFromDatabase();
971 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
972 # Don't forget to save the options after this or
973 # it won't take effect!
974 }
975
976 function validateCache( $timestamp ) {
977 $this->loadFromDatabase();
978 return ($timestamp >= $this->mTouched);
979 }
980
981 /**
982 * Encrypt a password.
983 * It can eventuall salt a password @see User::addSalt()
984 * @param string $p clear Password.
985 * @return string Encrypted password.
986 */
987 function encryptPassword( $p ) {
988 return wfEncryptPassword( $this->mId, $p );
989 }
990
991 # Set the password and reset the random token
992 function setPassword( $str ) {
993 $this->loadFromDatabase();
994 $this->setToken();
995 $this->mPassword = $this->encryptPassword( $str );
996 $this->mNewpassword = '';
997 }
998
999 # Set the random token (used for persistent authentication)
1000 function setToken( $token = false ) {
1001 global $wgSecretKey, $wgProxyKey, $wgDBname;
1002 if ( !$token ) {
1003 if ( $wgSecretKey ) {
1004 $key = $wgSecretKey;
1005 } elseif ( $wgProxyKey ) {
1006 $key = $wgProxyKey;
1007 } else {
1008 $key = microtime();
1009 }
1010 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1011 } else {
1012 $this->mToken = $token;
1013 }
1014 }
1015
1016
1017 function setCookiePassword( $str ) {
1018 $this->loadFromDatabase();
1019 $this->mCookiePassword = md5( $str );
1020 }
1021
1022 function setNewpassword( $str ) {
1023 $this->loadFromDatabase();
1024 $this->mNewpassword = $this->encryptPassword( $str );
1025 }
1026
1027 function getEmail() {
1028 $this->loadFromDatabase();
1029 return $this->mEmail;
1030 }
1031
1032 function getEmailAuthenticationTimestamp() {
1033 $this->loadFromDatabase();
1034 return $this->mEmailAuthenticated;
1035 }
1036
1037 function setEmail( $str ) {
1038 $this->loadFromDatabase();
1039 $this->mEmail = $str;
1040 }
1041
1042 function getRealName() {
1043 $this->loadFromDatabase();
1044 return $this->mRealName;
1045 }
1046
1047 function setRealName( $str ) {
1048 $this->loadFromDatabase();
1049 $this->mRealName = $str;
1050 }
1051
1052 /**
1053 * @param string $oname The option to check
1054 * @return string
1055 */
1056 function getOption( $oname ) {
1057 $this->loadFromDatabase();
1058 if ( array_key_exists( $oname, $this->mOptions ) ) {
1059 return trim( $this->mOptions[$oname] );
1060 } else {
1061 return '';
1062 }
1063 }
1064
1065 /**
1066 * @param string $oname The option to check
1067 * @return bool False if the option is not selected, true if it is
1068 */
1069 function getBoolOption( $oname ) {
1070 return (bool)$this->getOption( $oname );
1071 }
1072
1073 function setOption( $oname, $val ) {
1074 $this->loadFromDatabase();
1075 if ( $oname == 'skin' ) {
1076 # Clear cached skin, so the new one displays immediately in Special:Preferences
1077 unset( $this->mSkin );
1078 }
1079 $this->mOptions[$oname] = $val;
1080 $this->invalidateCache();
1081 }
1082
1083 function getRights() {
1084 $this->loadFromDatabase();
1085 return $this->mRights;
1086 }
1087
1088 /**
1089 * Get the list of explicit group memberships this user has.
1090 * The implicit * and user groups are not included.
1091 * @return array of strings
1092 */
1093 function getGroups() {
1094 $this->loadFromDatabase();
1095 return $this->mGroups;
1096 }
1097
1098 /**
1099 * Get the list of implicit group memberships this user has.
1100 * This includes all explicit groups, plus 'user' if logged in
1101 * and '*' for all accounts.
1102 * @return array of strings
1103 */
1104 function getEffectiveGroups() {
1105 $base = array( '*' );
1106 if( $this->isLoggedIn() ) {
1107 $base[] = 'user';
1108 }
1109 return array_merge( $base, $this->getGroups() );
1110 }
1111
1112 /**
1113 * Add the user to the given group.
1114 * This takes immediate effect.
1115 * @string $group
1116 */
1117 function addGroup( $group ) {
1118 $dbw =& wfGetDB( DB_MASTER );
1119 $dbw->insert( 'user_groups',
1120 array(
1121 'ug_user' => $this->getID(),
1122 'ug_group' => $group,
1123 ),
1124 'User::addGroup',
1125 array( 'IGNORE' ) );
1126
1127 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1128 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1129
1130 $this->invalidateCache();
1131 $this->saveSettings();
1132 }
1133
1134 /**
1135 * Remove the user from the given group.
1136 * This takes immediate effect.
1137 * @string $group
1138 */
1139 function removeGroup( $group ) {
1140 $dbw =& wfGetDB( DB_MASTER );
1141 $dbw->delete( 'user_groups',
1142 array(
1143 'ug_user' => $this->getID(),
1144 'ug_group' => $group,
1145 ),
1146 'User::removeGroup' );
1147
1148 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1149 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1150
1151 $this->invalidateCache();
1152 $this->saveSettings();
1153 }
1154
1155
1156 /**
1157 * A more legible check for non-anonymousness.
1158 * Returns true if the user is not an anonymous visitor.
1159 *
1160 * @return bool
1161 */
1162 function isLoggedIn() {
1163 return( $this->getID() != 0 );
1164 }
1165
1166 /**
1167 * A more legible check for anonymousness.
1168 * Returns true if the user is an anonymous visitor.
1169 *
1170 * @return bool
1171 */
1172 function isAnon() {
1173 return !$this->isLoggedIn();
1174 }
1175
1176 /**
1177 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1178 * @deprecated
1179 */
1180 function isSysop() {
1181 wfDebugDieBacktrace( "Call to deprecated (v1.7) User::isSysop() method\n" );
1182 #return $this->isAllowed( 'protect' );
1183 }
1184
1185 /**
1186 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1187 * @deprecated
1188 */
1189 function isDeveloper() {
1190 wfDebugDieBacktrace( "Call to deprecated (v1.7) User::isDeveloper() method\n" );
1191 #return $this->isAllowed( 'siteadmin' );
1192 }
1193
1194 /**
1195 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1196 * @deprecated
1197 */
1198 function isBureaucrat() {
1199 wfDebugDieBacktrace( "Call to deprecated (v1.7) User::isBureaucrat() method\n" );
1200 #return $this->isAllowed( 'makesysop' );
1201 }
1202
1203 /**
1204 * Whether the user is a bot
1205 * @todo need to be migrated to the new user level management sytem
1206 */
1207 function isBot() {
1208 $this->loadFromDatabase();
1209 return in_array( 'bot', $this->mRights );
1210 }
1211
1212 /**
1213 * Check if user is allowed to access a feature / make an action
1214 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1215 * @return boolean True: action is allowed, False: action should not be allowed
1216 */
1217 function isAllowed($action='') {
1218 if ( $action === '' )
1219 // In the spirit of DWIM
1220 return true;
1221
1222 $this->loadFromDatabase();
1223 return in_array( $action , $this->mRights );
1224 }
1225
1226 /**
1227 * Load a skin if it doesn't exist or return it
1228 * @todo FIXME : need to check the old failback system [AV]
1229 */
1230 function &getSkin() {
1231 global $IP, $wgRequest;
1232 if ( ! isset( $this->mSkin ) ) {
1233 $fname = 'User::getSkin';
1234 wfProfileIn( $fname );
1235
1236 # get the user skin
1237 $userSkin = $this->getOption( 'skin' );
1238 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1239
1240 $this->mSkin =& Skin::newFromKey( $userSkin );
1241 wfProfileOut( $fname );
1242 }
1243 return $this->mSkin;
1244 }
1245
1246 /**#@+
1247 * @param string $title Article title to look at
1248 */
1249
1250 /**
1251 * Check watched status of an article
1252 * @return bool True if article is watched
1253 */
1254 function isWatched( $title ) {
1255 $wl = WatchedItem::fromUserTitle( $this, $title );
1256 return $wl->isWatched();
1257 }
1258
1259 /**
1260 * Watch an article
1261 */
1262 function addWatch( $title ) {
1263 $wl = WatchedItem::fromUserTitle( $this, $title );
1264 $wl->addWatch();
1265 $this->invalidateCache();
1266 }
1267
1268 /**
1269 * Stop watching an article
1270 */
1271 function removeWatch( $title ) {
1272 $wl = WatchedItem::fromUserTitle( $this, $title );
1273 $wl->removeWatch();
1274 $this->invalidateCache();
1275 }
1276
1277 /**
1278 * Clear the user's notification timestamp for the given title.
1279 * If e-notif e-mails are on, they will receive notification mails on
1280 * the next change of the page if it's watched etc.
1281 */
1282 function clearNotification( &$title ) {
1283 global $wgUser, $wgUseEnotif;
1284
1285
1286 if ($title->getNamespace() == NS_USER_TALK &&
1287 $title->getText() == $this->getName() ) {
1288 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1289 return;
1290 $this->setNewtalk( false );
1291 }
1292
1293 if( !$wgUseEnotif ) {
1294 return;
1295 }
1296
1297 if( $this->isAnon() ) {
1298 // Nothing else to do...
1299 return;
1300 }
1301
1302 // Only update the timestamp if the page is being watched.
1303 // The query to find out if it is watched is cached both in memcached and per-invocation,
1304 // and when it does have to be executed, it can be on a slave
1305 // If this is the user's newtalk page, we always update the timestamp
1306 if ($title->getNamespace() == NS_USER_TALK &&
1307 $title->getText() == $wgUser->getName())
1308 {
1309 $watched = true;
1310 } elseif ( $this->getID() == $wgUser->getID() ) {
1311 $watched = $title->userIsWatching();
1312 } else {
1313 $watched = true;
1314 }
1315
1316 // If the page is watched by the user (or may be watched), update the timestamp on any
1317 // any matching rows
1318 if ( $watched ) {
1319 $dbw =& wfGetDB( DB_MASTER );
1320 $success = $dbw->update( 'watchlist',
1321 array( /* SET */
1322 'wl_notificationtimestamp' => NULL
1323 ), array( /* WHERE */
1324 'wl_title' => $title->getDBkey(),
1325 'wl_namespace' => $title->getNamespace(),
1326 'wl_user' => $this->getID()
1327 ), 'User::clearLastVisited'
1328 );
1329 }
1330 }
1331
1332 /**#@-*/
1333
1334 /**
1335 * Resets all of the given user's page-change notification timestamps.
1336 * If e-notif e-mails are on, they will receive notification mails on
1337 * the next change of any watched page.
1338 *
1339 * @param int $currentUser user ID number
1340 * @public
1341 */
1342 function clearAllNotifications( $currentUser ) {
1343 global $wgUseEnotif;
1344 if ( !$wgUseEnotif ) {
1345 $this->setNewtalk( false );
1346 return;
1347 }
1348 if( $currentUser != 0 ) {
1349
1350 $dbw =& wfGetDB( DB_MASTER );
1351 $success = $dbw->update( 'watchlist',
1352 array( /* SET */
1353 'wl_notificationtimestamp' => 0
1354 ), array( /* WHERE */
1355 'wl_user' => $currentUser
1356 ), 'UserMailer::clearAll'
1357 );
1358
1359 # we also need to clear here the "you have new message" notification for the own user_talk page
1360 # This is cleared one page view later in Article::viewUpdates();
1361 }
1362 }
1363
1364 /**
1365 * @private
1366 * @return string Encoding options
1367 */
1368 function encodeOptions() {
1369 $a = array();
1370 foreach ( $this->mOptions as $oname => $oval ) {
1371 array_push( $a, $oname.'='.$oval );
1372 }
1373 $s = implode( "\n", $a );
1374 return $s;
1375 }
1376
1377 /**
1378 * @private
1379 */
1380 function decodeOptions( $str ) {
1381 $a = explode( "\n", $str );
1382 foreach ( $a as $s ) {
1383 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1384 $this->mOptions[$m[1]] = $m[2];
1385 }
1386 }
1387 }
1388
1389 function setCookies() {
1390 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1391 if ( 0 == $this->mId ) return;
1392 $this->loadFromDatabase();
1393 $exp = time() + $wgCookieExpiration;
1394
1395 $_SESSION['wsUserID'] = $this->mId;
1396 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1397
1398 $_SESSION['wsUserName'] = $this->getName();
1399 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1400
1401 $_SESSION['wsToken'] = $this->mToken;
1402 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1403 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1404 } else {
1405 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1406 }
1407 }
1408
1409 /**
1410 * Logout user
1411 * It will clean the session cookie
1412 */
1413 function logout() {
1414 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1415 $this->loadDefaults();
1416 $this->setLoaded( true );
1417
1418 $_SESSION['wsUserID'] = 0;
1419
1420 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1421 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1422
1423 # Remember when user logged out, to prevent seeing cached pages
1424 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1425 }
1426
1427 /**
1428 * Save object settings into database
1429 */
1430 function saveSettings() {
1431 global $wgMemc, $wgDBname;
1432 $fname = 'User::saveSettings';
1433
1434 if ( wfReadOnly() ) { return; }
1435 if ( 0 == $this->mId ) { return; }
1436
1437 $dbw =& wfGetDB( DB_MASTER );
1438 $dbw->update( 'user',
1439 array( /* SET */
1440 'user_name' => $this->mName,
1441 'user_password' => $this->mPassword,
1442 'user_newpassword' => $this->mNewpassword,
1443 'user_real_name' => $this->mRealName,
1444 'user_email' => $this->mEmail,
1445 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1446 'user_options' => $this->encodeOptions(),
1447 'user_touched' => $dbw->timestamp($this->mTouched),
1448 'user_token' => $this->mToken
1449 ), array( /* WHERE */
1450 'user_id' => $this->mId
1451 ), $fname
1452 );
1453 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1454 }
1455
1456
1457 /**
1458 * Checks if a user with the given name exists, returns the ID
1459 */
1460 function idForName() {
1461 $fname = 'User::idForName';
1462
1463 $gotid = 0;
1464 $s = trim( $this->getName() );
1465 if ( 0 == strcmp( '', $s ) ) return 0;
1466
1467 $dbr =& wfGetDB( DB_SLAVE );
1468 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1469 if ( $id === false ) {
1470 $id = 0;
1471 }
1472 return $id;
1473 }
1474
1475 /**
1476 * Add user object to the database
1477 */
1478 function addToDatabase() {
1479 $fname = 'User::addToDatabase';
1480 $dbw =& wfGetDB( DB_MASTER );
1481 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1482 $dbw->insert( 'user',
1483 array(
1484 'user_id' => $seqVal,
1485 'user_name' => $this->mName,
1486 'user_password' => $this->mPassword,
1487 'user_newpassword' => $this->mNewpassword,
1488 'user_email' => $this->mEmail,
1489 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1490 'user_real_name' => $this->mRealName,
1491 'user_options' => $this->encodeOptions(),
1492 'user_token' => $this->mToken,
1493 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1494 ), $fname
1495 );
1496 $this->mId = $dbw->insertId();
1497 }
1498
1499 function spreadBlock() {
1500 # If the (non-anonymous) user is blocked, this function will block any IP address
1501 # that they successfully log on from.
1502 $fname = 'User::spreadBlock';
1503
1504 wfDebug( "User:spreadBlock()\n" );
1505 if ( $this->mId == 0 ) {
1506 return;
1507 }
1508
1509 $userblock = Block::newFromDB( '', $this->mId );
1510 if ( !$userblock->isValid() ) {
1511 return;
1512 }
1513
1514 # Check if this IP address is already blocked
1515 $ipblock = Block::newFromDB( wfGetIP() );
1516 if ( $ipblock->isValid() ) {
1517 # If the user is already blocked. Then check if the autoblock would
1518 # excede the user block. If it would excede, then do nothing, else
1519 # prolong block time
1520 if ($userblock->mExpiry &&
1521 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1522 return;
1523 }
1524 # Just update the timestamp
1525 $ipblock->updateTimestamp();
1526 return;
1527 }
1528
1529 # Make a new block object with the desired properties
1530 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1531 $ipblock->mAddress = wfGetIP();
1532 $ipblock->mUser = 0;
1533 $ipblock->mBy = $userblock->mBy;
1534 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1535 $ipblock->mTimestamp = wfTimestampNow();
1536 $ipblock->mAuto = 1;
1537 # If the user is already blocked with an expiry date, we don't
1538 # want to pile on top of that!
1539 if($userblock->mExpiry) {
1540 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1541 } else {
1542 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1543 }
1544
1545 # Insert it
1546 $ipblock->insert();
1547
1548 }
1549
1550 /**
1551 * Generate a string which will be different for any combination of
1552 * user options which would produce different parser output.
1553 * This will be used as part of the hash key for the parser cache,
1554 * so users will the same options can share the same cached data
1555 * safely.
1556 *
1557 * Extensions which require it should install 'PageRenderingHash' hook,
1558 * which will give them a chance to modify this key based on their own
1559 * settings.
1560 *
1561 * @return string
1562 */
1563 function getPageRenderingHash() {
1564 global $wgContLang;
1565 if( $this->mHash ){
1566 return $this->mHash;
1567 }
1568
1569 // stubthreshold is only included below for completeness,
1570 // it will always be 0 when this function is called by parsercache.
1571
1572 $confstr = $this->getOption( 'math' );
1573 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1574 $confstr .= '!' . $this->getOption( 'date' );
1575 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1576 $confstr .= '!' . $this->getOption( 'language' );
1577 $confstr .= '!' . $this->getOption( 'thumbsize' );
1578 // add in language specific options, if any
1579 $extra = $wgContLang->getExtraHashOptions();
1580 $confstr .= $extra;
1581
1582 // Give a chance for extensions to modify the hash, if they have
1583 // extra options or other effects on the parser cache.
1584 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1585
1586 $this->mHash = $confstr;
1587 return $confstr;
1588 }
1589
1590 function isAllowedToCreateAccount() {
1591 return $this->isAllowed( 'createaccount' ) && !$this->isBlocked();
1592 }
1593
1594 /**
1595 * Set mDataLoaded, return previous value
1596 * Use this to prevent DB access in command-line scripts or similar situations
1597 */
1598 function setLoaded( $loaded ) {
1599 return wfSetVar( $this->mDataLoaded, $loaded );
1600 }
1601
1602 /**
1603 * Get this user's personal page title.
1604 *
1605 * @return Title
1606 * @public
1607 */
1608 function getUserPage() {
1609 return Title::makeTitle( NS_USER, $this->getName() );
1610 }
1611
1612 /**
1613 * Get this user's talk page title.
1614 *
1615 * @return Title
1616 * @public
1617 */
1618 function getTalkPage() {
1619 $title = $this->getUserPage();
1620 return $title->getTalkPage();
1621 }
1622
1623 /**
1624 * @static
1625 */
1626 function getMaxID() {
1627 static $res; // cache
1628
1629 if ( isset( $res ) )
1630 return $res;
1631 else {
1632 $dbr =& wfGetDB( DB_SLAVE );
1633 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1634 }
1635 }
1636
1637 /**
1638 * Determine whether the user is a newbie. Newbies are either
1639 * anonymous IPs, or the most recently created accounts.
1640 * @return bool True if it is a newbie.
1641 */
1642 function isNewbie() {
1643 return !$this->isAllowed( 'autoconfirmed' );
1644 }
1645
1646 /**
1647 * Check to see if the given clear-text password is one of the accepted passwords
1648 * @param string $password User password.
1649 * @return bool True if the given password is correct otherwise False.
1650 */
1651 function checkPassword( $password ) {
1652 global $wgAuth, $wgMinimalPasswordLength;
1653 $this->loadFromDatabase();
1654
1655 // Even though we stop people from creating passwords that
1656 // are shorter than this, doesn't mean people wont be able
1657 // to. Certain authentication plugins do NOT want to save
1658 // domain passwords in a mysql database, so we should
1659 // check this (incase $wgAuth->strict() is false).
1660 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1661 return false;
1662 }
1663
1664 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1665 return true;
1666 } elseif( $wgAuth->strict() ) {
1667 /* Auth plugin doesn't allow local authentication */
1668 return false;
1669 }
1670 $ep = $this->encryptPassword( $password );
1671 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1672 return true;
1673 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1674 return true;
1675 } elseif ( function_exists( 'iconv' ) ) {
1676 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1677 # Check for this with iconv
1678 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1679 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1680 return true;
1681 }
1682 }
1683 return false;
1684 }
1685
1686 /**
1687 * Initialize (if necessary) and return a session token value
1688 * which can be used in edit forms to show that the user's
1689 * login credentials aren't being hijacked with a foreign form
1690 * submission.
1691 *
1692 * @param mixed $salt - Optional function-specific data for hash.
1693 * Use a string or an array of strings.
1694 * @return string
1695 * @public
1696 */
1697 function editToken( $salt = '' ) {
1698 if( !isset( $_SESSION['wsEditToken'] ) ) {
1699 $token = $this->generateToken();
1700 $_SESSION['wsEditToken'] = $token;
1701 } else {
1702 $token = $_SESSION['wsEditToken'];
1703 }
1704 if( is_array( $salt ) ) {
1705 $salt = implode( '|', $salt );
1706 }
1707 return md5( $token . $salt );
1708 }
1709
1710 /**
1711 * Generate a hex-y looking random token for various uses.
1712 * Could be made more cryptographically sure if someone cares.
1713 * @return string
1714 */
1715 function generateToken( $salt = '' ) {
1716 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1717 return md5( $token . $salt );
1718 }
1719
1720 /**
1721 * Check given value against the token value stored in the session.
1722 * A match should confirm that the form was submitted from the
1723 * user's own login session, not a form submission from a third-party
1724 * site.
1725 *
1726 * @param string $val - the input value to compare
1727 * @param string $salt - Optional function-specific data for hash
1728 * @return bool
1729 * @public
1730 */
1731 function matchEditToken( $val, $salt = '' ) {
1732 global $wgMemc;
1733 $sessionToken = $this->editToken( $salt );
1734 if ( $val != $sessionToken ) {
1735 wfDebug( "User::matchEditToken: broken session data\n" );
1736 }
1737 return $val == $sessionToken;
1738 }
1739
1740 /**
1741 * Generate a new e-mail confirmation token and send a confirmation
1742 * mail to the user's given address.
1743 *
1744 * @return mixed True on success, a WikiError object on failure.
1745 */
1746 function sendConfirmationMail() {
1747 global $wgContLang;
1748 $url = $this->confirmationTokenUrl( $expiration );
1749 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1750 wfMsg( 'confirmemail_body',
1751 wfGetIP(),
1752 $this->getName(),
1753 $url,
1754 $wgContLang->timeanddate( $expiration, false ) ) );
1755 }
1756
1757 /**
1758 * Send an e-mail to this user's account. Does not check for
1759 * confirmed status or validity.
1760 *
1761 * @param string $subject
1762 * @param string $body
1763 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1764 * @return mixed True on success, a WikiError object on failure.
1765 */
1766 function sendMail( $subject, $body, $from = null ) {
1767 if( is_null( $from ) ) {
1768 global $wgPasswordSender;
1769 $from = $wgPasswordSender;
1770 }
1771
1772 require_once( 'UserMailer.php' );
1773 $to = new MailAddress( $this );
1774 $sender = new MailAddress( $from );
1775 $error = userMailer( $to, $sender, $subject, $body );
1776
1777 if( $error == '' ) {
1778 return true;
1779 } else {
1780 return new WikiError( $error );
1781 }
1782 }
1783
1784 /**
1785 * Generate, store, and return a new e-mail confirmation code.
1786 * A hash (unsalted since it's used as a key) is stored.
1787 * @param &$expiration mixed output: accepts the expiration time
1788 * @return string
1789 * @private
1790 */
1791 function confirmationToken( &$expiration ) {
1792 $fname = 'User::confirmationToken';
1793
1794 $now = time();
1795 $expires = $now + 7 * 24 * 60 * 60;
1796 $expiration = wfTimestamp( TS_MW, $expires );
1797
1798 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1799 $hash = md5( $token );
1800
1801 $dbw =& wfGetDB( DB_MASTER );
1802 $dbw->update( 'user',
1803 array( 'user_email_token' => $hash,
1804 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1805 array( 'user_id' => $this->mId ),
1806 $fname );
1807
1808 return $token;
1809 }
1810
1811 /**
1812 * Generate and store a new e-mail confirmation token, and return
1813 * the URL the user can use to confirm.
1814 * @param &$expiration mixed output: accepts the expiration time
1815 * @return string
1816 * @private
1817 */
1818 function confirmationTokenUrl( &$expiration ) {
1819 $token = $this->confirmationToken( $expiration );
1820 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1821 return $title->getFullUrl();
1822 }
1823
1824 /**
1825 * Mark the e-mail address confirmed and save.
1826 */
1827 function confirmEmail() {
1828 $this->loadFromDatabase();
1829 $this->mEmailAuthenticated = wfTimestampNow();
1830 $this->saveSettings();
1831 return true;
1832 }
1833
1834 /**
1835 * Is this user allowed to send e-mails within limits of current
1836 * site configuration?
1837 * @return bool
1838 */
1839 function canSendEmail() {
1840 return $this->isEmailConfirmed();
1841 }
1842
1843 /**
1844 * Is this user allowed to receive e-mails within limits of current
1845 * site configuration?
1846 * @return bool
1847 */
1848 function canReceiveEmail() {
1849 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1850 }
1851
1852 /**
1853 * Is this user's e-mail address valid-looking and confirmed within
1854 * limits of the current site configuration?
1855 *
1856 * If $wgEmailAuthentication is on, this may require the user to have
1857 * confirmed their address by returning a code or using a password
1858 * sent to the address from the wiki.
1859 *
1860 * @return bool
1861 */
1862 function isEmailConfirmed() {
1863 global $wgEmailAuthentication;
1864 $this->loadFromDatabase();
1865 $confirmed = true;
1866 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
1867 if( $this->isAnon() )
1868 return false;
1869 if( !$this->isValidEmailAddr( $this->mEmail ) )
1870 return false;
1871 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1872 return false;
1873 return true;
1874 } else {
1875 return $confirmed;
1876 }
1877 }
1878
1879 /**
1880 * @param array $groups list of groups
1881 * @return array list of permission key names for given groups combined
1882 * @static
1883 */
1884 function getGroupPermissions( $groups ) {
1885 global $wgGroupPermissions;
1886 $rights = array();
1887 foreach( $groups as $group ) {
1888 if( isset( $wgGroupPermissions[$group] ) ) {
1889 $rights = array_merge( $rights,
1890 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1891 }
1892 }
1893 return $rights;
1894 }
1895
1896 /**
1897 * @param string $group key name
1898 * @return string localized descriptive name for group, if provided
1899 * @static
1900 */
1901 function getGroupName( $group ) {
1902 $key = "group-$group";
1903 $name = wfMsg( $key );
1904 if( $name == '' || $name == "&lt;$key&gt;" ) {
1905 return $group;
1906 } else {
1907 return $name;
1908 }
1909 }
1910
1911 /**
1912 * @param string $group key name
1913 * @return string localized descriptive name for member of a group, if provided
1914 * @static
1915 */
1916 function getGroupMember( $group ) {
1917 $key = "group-$group-member";
1918 $name = wfMsg( $key );
1919 if( $name == '' || $name == "&lt;$key&gt;" ) {
1920 return $group;
1921 } else {
1922 return $name;
1923 }
1924 }
1925
1926
1927 /**
1928 * Return the set of defined explicit groups.
1929 * The * and 'user' groups are not included.
1930 * @return array
1931 * @static
1932 */
1933 function getAllGroups() {
1934 global $wgGroupPermissions;
1935 return array_diff(
1936 array_keys( $wgGroupPermissions ),
1937 array( '*', 'user', 'autoconfirmed' ) );
1938 }
1939
1940 /**
1941 * Get the title of a page describing a particular group
1942 *
1943 * @param $group Name of the group
1944 * @return mixed
1945 */
1946 function getGroupPage( $group ) {
1947 $page = wfMsgForContent( 'grouppage-' . $group );
1948 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
1949 $title = Title::newFromText( $page );
1950 if( is_object( $title ) )
1951 return $title;
1952 }
1953 return false;
1954 }
1955
1956
1957 }
1958
1959 ?>