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