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