Fix #5728: mVersion missing from User::__sleep() leading to constant cache miss
[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 if ( !$this->isSysop() && !in_array( $ip, $wgProxyWhitelist ) ) {
453
454 # Local list
455 if ( wfIsLocallyBlockedProxy( $ip ) ) {
456 $this->mBlockedby = wfMsg( 'proxyblocker' );
457 $this->mBlockreason = wfMsg( 'proxyblockreason' );
458 }
459
460 # DNSBL
461 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
462 if ( $this->inSorbsBlacklist( $ip ) ) {
463 $this->mBlockedby = wfMsg( 'sorbs' );
464 $this->mBlockreason = wfMsg( 'sorbsreason' );
465 }
466 }
467 }
468
469 # Extensions
470 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
471
472 wfProfileOut( $fname );
473 }
474
475 function inSorbsBlacklist( $ip ) {
476 global $wgEnableSorbs;
477 return $wgEnableSorbs &&
478 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
479 }
480
481 function inOpmBlacklist( $ip ) {
482 global $wgEnableOpm;
483 return $wgEnableOpm &&
484 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
485 }
486
487 function inDnsBlacklist( $ip, $base ) {
488 $fname = 'User::inDnsBlacklist';
489 wfProfileIn( $fname );
490
491 $found = false;
492 $host = '';
493
494 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
495 # Make hostname
496 for ( $i=4; $i>=1; $i-- ) {
497 $host .= $m[$i] . '.';
498 }
499 $host .= $base;
500
501 # Send query
502 $ipList = gethostbynamel( $host );
503
504 if ( $ipList ) {
505 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
506 $found = true;
507 } else {
508 wfDebug( "Requested $host, not found in $base.\n" );
509 }
510 }
511
512 wfProfileOut( $fname );
513 return $found;
514 }
515
516 /**
517 * Primitive rate limits: enforce maximum actions per time period
518 * to put a brake on flooding.
519 *
520 * Note: when using a shared cache like memcached, IP-address
521 * last-hit counters will be shared across wikis.
522 *
523 * @return bool true if a rate limiter was tripped
524 * @public
525 */
526 function pingLimiter( $action='edit' ) {
527 global $wgRateLimits;
528 if( !isset( $wgRateLimits[$action] ) ) {
529 return false;
530 }
531 if( $this->isAllowed( 'delete' ) ) {
532 // goddam cabal
533 return false;
534 }
535
536 global $wgMemc, $wgDBname, $wgRateLimitLog;
537 $fname = 'User::pingLimiter';
538 wfProfileIn( $fname );
539
540 $limits = $wgRateLimits[$action];
541 $keys = array();
542 $id = $this->getId();
543 $ip = wfGetIP();
544
545 if( isset( $limits['anon'] ) && $id == 0 ) {
546 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
547 }
548
549 if( isset( $limits['user'] ) && $id != 0 ) {
550 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
551 }
552 if( $this->isNewbie() ) {
553 if( isset( $limits['newbie'] ) && $id != 0 ) {
554 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
555 }
556 if( isset( $limits['ip'] ) ) {
557 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
558 }
559 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
560 $subnet = $matches[1];
561 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
562 }
563 }
564
565 $triggered = false;
566 foreach( $keys as $key => $limit ) {
567 list( $max, $period ) = $limit;
568 $summary = "(limit $max in {$period}s)";
569 $count = $wgMemc->get( $key );
570 if( $count ) {
571 if( $count > $max ) {
572 wfDebug( "$fname: tripped! $key at $count $summary\n" );
573 if( $wgRateLimitLog ) {
574 @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
575 }
576 $triggered = true;
577 } else {
578 wfDebug( "$fname: ok. $key at $count $summary\n" );
579 }
580 } else {
581 wfDebug( "$fname: adding record for $key $summary\n" );
582 $wgMemc->add( $key, 1, intval( $period ) );
583 }
584 $wgMemc->incr( $key );
585 }
586
587 wfProfileOut( $fname );
588 return $triggered;
589 }
590
591 /**
592 * Check if user is blocked
593 * @return bool True if blocked, false otherwise
594 */
595 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
596 wfDebug( "User::isBlocked: enter\n" );
597 $this->getBlockedStatus( $bFromSlave );
598 return $this->mBlockedby !== 0;
599 }
600
601 /**
602 * Check if user is blocked from editing a particular article
603 */
604 function isBlockedFrom( $title, $bFromSlave = false ) {
605 global $wgBlockAllowsUTEdit;
606 $fname = 'User::isBlockedFrom';
607 wfProfileIn( $fname );
608 wfDebug( "$fname: enter\n" );
609
610 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
611 $title->getNamespace() == NS_USER_TALK )
612 {
613 $blocked = false;
614 wfDebug( "$fname: self-talk page, ignoring any blocks\n" );
615 } else {
616 wfDebug( "$fname: asking isBlocked()\n" );
617 $blocked = $this->isBlocked( $bFromSlave );
618 }
619 wfProfileOut( $fname );
620 return $blocked;
621 }
622
623 /**
624 * Get name of blocker
625 * @return string name of blocker
626 */
627 function blockedBy() {
628 $this->getBlockedStatus();
629 return $this->mBlockedby;
630 }
631
632 /**
633 * Get blocking reason
634 * @return string Blocking reason
635 */
636 function blockedFor() {
637 $this->getBlockedStatus();
638 return $this->mBlockreason;
639 }
640
641 /**
642 * Initialise php session
643 */
644 function SetupSession() {
645 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
646 if( $wgSessionsInMemcached ) {
647 require_once( 'MemcachedSessions.php' );
648 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
649 # If it's left on 'user' or another setting from another
650 # application, it will end up failing. Try to recover.
651 ini_set ( 'session.save_handler', 'files' );
652 }
653 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
654 session_cache_limiter( 'private, must-revalidate' );
655 @session_start();
656 }
657
658 /**
659 * Create a new user object using data from session
660 * @static
661 */
662 function loadFromSession() {
663 global $wgMemc, $wgDBname, $wgCookiePrefix;
664
665 if ( isset( $_SESSION['wsUserID'] ) ) {
666 if ( 0 != $_SESSION['wsUserID'] ) {
667 $sId = $_SESSION['wsUserID'];
668 } else {
669 return new User();
670 }
671 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
672 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
673 $_SESSION['wsUserID'] = $sId;
674 } else {
675 return new User();
676 }
677 if ( isset( $_SESSION['wsUserName'] ) ) {
678 $sName = $_SESSION['wsUserName'];
679 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
680 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
681 $_SESSION['wsUserName'] = $sName;
682 } else {
683 return new User();
684 }
685
686 $passwordCorrect = FALSE;
687 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
688 if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) {
689 # Expire old serialized objects; they may be corrupt.
690 $user = false;
691 }
692 if($makenew = !$user) {
693 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
694 $user = new User();
695 $user->mId = $sId;
696 $user->loadFromDatabase();
697 } else {
698 wfDebug( "User::loadFromSession() got from cache!\n" );
699 }
700
701 if ( isset( $_SESSION['wsToken'] ) ) {
702 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken;
703 } else if ( isset( $_COOKIE["{$wgCookiePrefix}Token"] ) ) {
704 $passwordCorrect = $user->mToken == $_COOKIE["{$wgCookiePrefix}Token"];
705 } else {
706 return new User(); # Can't log in from session
707 }
708
709 if ( ( $sName == $user->mName ) && $passwordCorrect ) {
710 if($makenew) {
711 if($wgMemc->set( $key, $user ))
712 wfDebug( "User::loadFromSession() successfully saved user\n" );
713 else
714 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
715 }
716 return $user;
717 }
718 return new User(); # Can't log in from session
719 }
720
721 /**
722 * Load a user from the database
723 */
724 function loadFromDatabase() {
725 $fname = "User::loadFromDatabase";
726
727 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
728 # loading in a command line script, don't assume all command line scripts need it like this
729 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
730 if ( $this->mDataLoaded ) {
731 return;
732 }
733
734 # Paranoia
735 $this->mId = intval( $this->mId );
736
737 /** Anonymous user */
738 if( !$this->mId ) {
739 /** Get rights */
740 $this->mRights = $this->getGroupPermissions( array( '*' ) );
741 $this->mDataLoaded = true;
742 return;
743 } # the following stuff is for non-anonymous users only
744
745 $dbr =& wfGetDB( DB_SLAVE );
746 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
747 'user_email_authenticated',
748 'user_real_name','user_options','user_touched', 'user_token', 'user_registration' ),
749 array( 'user_id' => $this->mId ), $fname );
750
751 if ( $s !== false ) {
752 $this->mName = $s->user_name;
753 $this->mEmail = $s->user_email;
754 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $s->user_email_authenticated );
755 $this->mRealName = $s->user_real_name;
756 $this->mPassword = $s->user_password;
757 $this->mNewpassword = $s->user_newpassword;
758 $this->decodeOptions( $s->user_options );
759 $this->mTouched = wfTimestamp(TS_MW,$s->user_touched);
760 $this->mToken = $s->user_token;
761 $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration );
762
763 $res = $dbr->select( 'user_groups',
764 array( 'ug_group' ),
765 array( 'ug_user' => $this->mId ),
766 $fname );
767 $this->mGroups = array();
768 while( $row = $dbr->fetchObject( $res ) ) {
769 $this->mGroups[] = $row->ug_group;
770 }
771 $implicitGroups = array( '*', 'user' );
772
773 global $wgAutoConfirmAge;
774 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
775 if( $accountAge >= $wgAutoConfirmAge ) {
776 $implicitGroups[] = 'autoconfirmed';
777 }
778
779 $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
780 $this->mRights = $this->getGroupPermissions( $effectiveGroups );
781 }
782
783 $this->mDataLoaded = true;
784 }
785
786 function getID() { return $this->mId; }
787 function setID( $v ) {
788 $this->mId = $v;
789 $this->mDataLoaded = false;
790 }
791
792 function getName() {
793 $this->loadFromDatabase();
794 if ( $this->mName === false ) {
795 $this->mName = wfGetIP();
796 }
797 return $this->mName;
798 }
799
800 function setName( $str ) {
801 $this->loadFromDatabase();
802 $this->mName = $str;
803 }
804
805
806 /**
807 * Return the title dbkey form of the name, for eg user pages.
808 * @return string
809 * @public
810 */
811 function getTitleKey() {
812 return str_replace( ' ', '_', $this->getName() );
813 }
814
815 function getNewtalk() {
816 $this->loadFromDatabase();
817
818 # Load the newtalk status if it is unloaded (mNewtalk=-1)
819 if( $this->mNewtalk === -1 ) {
820 $this->mNewtalk = false; # reset talk page status
821
822 # Check memcached separately for anons, who have no
823 # entire User object stored in there.
824 if( !$this->mId ) {
825 global $wgDBname, $wgMemc;
826 $key = "$wgDBname:newtalk:ip:" . $this->getName();
827 $newtalk = $wgMemc->get( $key );
828 if( is_integer( $newtalk ) ) {
829 $this->mNewtalk = (bool)$newtalk;
830 } else {
831 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
832 $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
833 }
834 } else {
835 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
836 }
837 }
838
839 return (bool)$this->mNewtalk;
840 }
841
842 /**
843 * Return the talk page(s) this user has new messages on.
844 */
845 function getNewMessageLinks() {
846 global $wgDBname;
847 $talks = array();
848 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
849 return $talks;
850
851 if (!$this->getNewtalk())
852 return array();
853 $up = $this->getUserPage();
854 $utp = $up->getTalkPage();
855 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
856 }
857
858
859 /**
860 * Perform a user_newtalk check on current slaves; if the memcached data
861 * is funky we don't want newtalk state to get stuck on save, as that's
862 * damn annoying.
863 *
864 * @param string $field
865 * @param mixed $id
866 * @return bool
867 * @private
868 */
869 function checkNewtalk( $field, $id ) {
870 $fname = 'User::checkNewtalk';
871 $dbr =& wfGetDB( DB_SLAVE );
872 $ok = $dbr->selectField( 'user_newtalk', $field,
873 array( $field => $id ), $fname );
874 return $ok !== false;
875 }
876
877 /**
878 * Add or update the
879 * @param string $field
880 * @param mixed $id
881 * @private
882 */
883 function updateNewtalk( $field, $id ) {
884 $fname = 'User::updateNewtalk';
885 if( $this->checkNewtalk( $field, $id ) ) {
886 wfDebug( "$fname already set ($field, $id), ignoring\n" );
887 return false;
888 }
889 $dbw =& wfGetDB( DB_MASTER );
890 $dbw->insert( 'user_newtalk',
891 array( $field => $id ),
892 $fname,
893 'IGNORE' );
894 wfDebug( "$fname: set on ($field, $id)\n" );
895 return true;
896 }
897
898 /**
899 * Clear the new messages flag for the given user
900 * @param string $field
901 * @param mixed $id
902 * @private
903 */
904 function deleteNewtalk( $field, $id ) {
905 $fname = 'User::deleteNewtalk';
906 if( !$this->checkNewtalk( $field, $id ) ) {
907 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
908 return false;
909 }
910 $dbw =& wfGetDB( DB_MASTER );
911 $dbw->delete( 'user_newtalk',
912 array( $field => $id ),
913 $fname );
914 wfDebug( "$fname: killed on ($field, $id)\n" );
915 return true;
916 }
917
918 /**
919 * Update the 'You have new messages!' status.
920 * @param bool $val
921 */
922 function setNewtalk( $val ) {
923 if( wfReadOnly() ) {
924 return;
925 }
926
927 $this->loadFromDatabase();
928 $this->mNewtalk = $val;
929
930 $fname = 'User::setNewtalk';
931
932 if( $this->isAnon() ) {
933 $field = 'user_ip';
934 $id = $this->getName();
935 } else {
936 $field = 'user_id';
937 $id = $this->getId();
938 }
939
940 if( $val ) {
941 $changed = $this->updateNewtalk( $field, $id );
942 } else {
943 $changed = $this->deleteNewtalk( $field, $id );
944 }
945
946 if( $changed ) {
947 if( $this->isAnon() ) {
948 // Anons have a separate memcached space, since
949 // user records aren't kept for them.
950 global $wgDBname, $wgMemc;
951 $key = "$wgDBname:newtalk:ip:$val";
952 $wgMemc->set( $key, $val ? 1 : 0 );
953 } else {
954 if( $val ) {
955 // Make sure the user page is watched, so a notification
956 // will be sent out if enabled.
957 $this->addWatch( $this->getTalkPage() );
958 }
959 }
960 $this->invalidateCache();
961 $this->saveSettings();
962 }
963 }
964
965 function invalidateCache() {
966 global $wgClockSkewFudge;
967 $this->loadFromDatabase();
968 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
969 # Don't forget to save the options after this or
970 # it won't take effect!
971 }
972
973 function validateCache( $timestamp ) {
974 $this->loadFromDatabase();
975 return ($timestamp >= $this->mTouched);
976 }
977
978 /**
979 * Encrypt a password.
980 * It can eventuall salt a password @see User::addSalt()
981 * @param string $p clear Password.
982 * @return string Encrypted password.
983 */
984 function encryptPassword( $p ) {
985 return wfEncryptPassword( $this->mId, $p );
986 }
987
988 # Set the password and reset the random token
989 function setPassword( $str ) {
990 $this->loadFromDatabase();
991 $this->setToken();
992 $this->mPassword = $this->encryptPassword( $str );
993 $this->mNewpassword = '';
994 }
995
996 # Set the random token (used for persistent authentication)
997 function setToken( $token = false ) {
998 global $wgSecretKey, $wgProxyKey, $wgDBname;
999 if ( !$token ) {
1000 if ( $wgSecretKey ) {
1001 $key = $wgSecretKey;
1002 } elseif ( $wgProxyKey ) {
1003 $key = $wgProxyKey;
1004 } else {
1005 $key = microtime();
1006 }
1007 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1008 } else {
1009 $this->mToken = $token;
1010 }
1011 }
1012
1013
1014 function setCookiePassword( $str ) {
1015 $this->loadFromDatabase();
1016 $this->mCookiePassword = md5( $str );
1017 }
1018
1019 function setNewpassword( $str ) {
1020 $this->loadFromDatabase();
1021 $this->mNewpassword = $this->encryptPassword( $str );
1022 }
1023
1024 function getEmail() {
1025 $this->loadFromDatabase();
1026 return $this->mEmail;
1027 }
1028
1029 function getEmailAuthenticationTimestamp() {
1030 $this->loadFromDatabase();
1031 return $this->mEmailAuthenticated;
1032 }
1033
1034 function setEmail( $str ) {
1035 $this->loadFromDatabase();
1036 $this->mEmail = $str;
1037 }
1038
1039 function getRealName() {
1040 $this->loadFromDatabase();
1041 return $this->mRealName;
1042 }
1043
1044 function setRealName( $str ) {
1045 $this->loadFromDatabase();
1046 $this->mRealName = $str;
1047 }
1048
1049 /**
1050 * @param string $oname The option to check
1051 * @return string
1052 */
1053 function getOption( $oname ) {
1054 $this->loadFromDatabase();
1055 if ( array_key_exists( $oname, $this->mOptions ) ) {
1056 return trim( $this->mOptions[$oname] );
1057 } else {
1058 return '';
1059 }
1060 }
1061
1062 /**
1063 * @param string $oname The option to check
1064 * @return bool False if the option is not selected, true if it is
1065 */
1066 function getBoolOption( $oname ) {
1067 return (bool)$this->getOption( $oname );
1068 }
1069
1070 function setOption( $oname, $val ) {
1071 $this->loadFromDatabase();
1072 if ( $oname == 'skin' ) {
1073 # Clear cached skin, so the new one displays immediately in Special:Preferences
1074 unset( $this->mSkin );
1075 }
1076 $this->mOptions[$oname] = $val;
1077 $this->invalidateCache();
1078 }
1079
1080 function getRights() {
1081 $this->loadFromDatabase();
1082 return $this->mRights;
1083 }
1084
1085 /**
1086 * Get the list of explicit group memberships this user has.
1087 * The implicit * and user groups are not included.
1088 * @return array of strings
1089 */
1090 function getGroups() {
1091 $this->loadFromDatabase();
1092 return $this->mGroups;
1093 }
1094
1095 /**
1096 * Get the list of implicit group memberships this user has.
1097 * This includes all explicit groups, plus 'user' if logged in
1098 * and '*' for all accounts.
1099 * @return array of strings
1100 */
1101 function getEffectiveGroups() {
1102 $base = array( '*' );
1103 if( $this->isLoggedIn() ) {
1104 $base[] = 'user';
1105 }
1106 return array_merge( $base, $this->getGroups() );
1107 }
1108
1109 /**
1110 * Add the user to the given group.
1111 * This takes immediate effect.
1112 * @string $group
1113 */
1114 function addGroup( $group ) {
1115 $dbw =& wfGetDB( DB_MASTER );
1116 $dbw->insert( 'user_groups',
1117 array(
1118 'ug_user' => $this->getID(),
1119 'ug_group' => $group,
1120 ),
1121 'User::addGroup',
1122 array( 'IGNORE' ) );
1123
1124 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1125 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1126
1127 $this->invalidateCache();
1128 $this->saveSettings();
1129 }
1130
1131 /**
1132 * Remove the user from the given group.
1133 * This takes immediate effect.
1134 * @string $group
1135 */
1136 function removeGroup( $group ) {
1137 $dbw =& wfGetDB( DB_MASTER );
1138 $dbw->delete( 'user_groups',
1139 array(
1140 'ug_user' => $this->getID(),
1141 'ug_group' => $group,
1142 ),
1143 'User::removeGroup' );
1144
1145 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1146 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1147
1148 $this->invalidateCache();
1149 $this->saveSettings();
1150 }
1151
1152
1153 /**
1154 * A more legible check for non-anonymousness.
1155 * Returns true if the user is not an anonymous visitor.
1156 *
1157 * @return bool
1158 */
1159 function isLoggedIn() {
1160 return( $this->getID() != 0 );
1161 }
1162
1163 /**
1164 * A more legible check for anonymousness.
1165 * Returns true if the user is an anonymous visitor.
1166 *
1167 * @return bool
1168 */
1169 function isAnon() {
1170 return !$this->isLoggedIn();
1171 }
1172
1173 /**
1174 * Check if a user is sysop
1175 * @deprecated
1176 */
1177 function isSysop() {
1178 return $this->isAllowed( 'protect' );
1179 }
1180
1181 /** @deprecated */
1182 function isDeveloper() {
1183 return $this->isAllowed( 'siteadmin' );
1184 }
1185
1186 /** @deprecated */
1187 function isBureaucrat() {
1188 return $this->isAllowed( 'makesysop' );
1189 }
1190
1191 /**
1192 * Whether the user is a bot
1193 * @todo need to be migrated to the new user level management sytem
1194 */
1195 function isBot() {
1196 $this->loadFromDatabase();
1197 return in_array( 'bot', $this->mRights );
1198 }
1199
1200 /**
1201 * Check if user is allowed to access a feature / make an action
1202 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1203 * @return boolean True: action is allowed, False: action should not be allowed
1204 */
1205 function isAllowed($action='') {
1206 if ( $action === '' )
1207 // In the spirit of DWIM
1208 return true;
1209
1210 $this->loadFromDatabase();
1211 return in_array( $action , $this->mRights );
1212 }
1213
1214 /**
1215 * Load a skin if it doesn't exist or return it
1216 * @todo FIXME : need to check the old failback system [AV]
1217 */
1218 function &getSkin() {
1219 global $IP, $wgRequest;
1220 if ( ! isset( $this->mSkin ) ) {
1221 $fname = 'User::getSkin';
1222 wfProfileIn( $fname );
1223
1224 # get the user skin
1225 $userSkin = $this->getOption( 'skin' );
1226 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1227
1228 $this->mSkin =& Skin::newFromKey( $userSkin );
1229 wfProfileOut( $fname );
1230 }
1231 return $this->mSkin;
1232 }
1233
1234 /**#@+
1235 * @param string $title Article title to look at
1236 */
1237
1238 /**
1239 * Check watched status of an article
1240 * @return bool True if article is watched
1241 */
1242 function isWatched( $title ) {
1243 $wl = WatchedItem::fromUserTitle( $this, $title );
1244 return $wl->isWatched();
1245 }
1246
1247 /**
1248 * Watch an article
1249 */
1250 function addWatch( $title ) {
1251 $wl = WatchedItem::fromUserTitle( $this, $title );
1252 $wl->addWatch();
1253 $this->invalidateCache();
1254 }
1255
1256 /**
1257 * Stop watching an article
1258 */
1259 function removeWatch( $title ) {
1260 $wl = WatchedItem::fromUserTitle( $this, $title );
1261 $wl->removeWatch();
1262 $this->invalidateCache();
1263 }
1264
1265 /**
1266 * Clear the user's notification timestamp for the given title.
1267 * If e-notif e-mails are on, they will receive notification mails on
1268 * the next change of the page if it's watched etc.
1269 */
1270 function clearNotification( &$title ) {
1271 global $wgUser, $wgUseEnotif;
1272
1273
1274 if ($title->getNamespace() == NS_USER_TALK &&
1275 $title->getText() == $this->getName() ) {
1276 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1277 return;
1278 $this->setNewtalk( false );
1279 }
1280
1281 if( !$wgUseEnotif ) {
1282 return;
1283 }
1284
1285 if( $this->isAnon() ) {
1286 // Nothing else to do...
1287 return;
1288 }
1289
1290 // Only update the timestamp if the page is being watched.
1291 // The query to find out if it is watched is cached both in memcached and per-invocation,
1292 // and when it does have to be executed, it can be on a slave
1293 // If this is the user's newtalk page, we always update the timestamp
1294 if ($title->getNamespace() == NS_USER_TALK &&
1295 $title->getText() == $wgUser->getName())
1296 {
1297 $watched = true;
1298 } elseif ( $this->getID() == $wgUser->getID() ) {
1299 $watched = $title->userIsWatching();
1300 } else {
1301 $watched = true;
1302 }
1303
1304 // If the page is watched by the user (or may be watched), update the timestamp on any
1305 // any matching rows
1306 if ( $watched ) {
1307 $dbw =& wfGetDB( DB_MASTER );
1308 $success = $dbw->update( 'watchlist',
1309 array( /* SET */
1310 'wl_notificationtimestamp' => NULL
1311 ), array( /* WHERE */
1312 'wl_title' => $title->getDBkey(),
1313 'wl_namespace' => $title->getNamespace(),
1314 'wl_user' => $this->getID()
1315 ), 'User::clearLastVisited'
1316 );
1317 }
1318 }
1319
1320 /**#@-*/
1321
1322 /**
1323 * Resets all of the given user's page-change notification timestamps.
1324 * If e-notif e-mails are on, they will receive notification mails on
1325 * the next change of any watched page.
1326 *
1327 * @param int $currentUser user ID number
1328 * @public
1329 */
1330 function clearAllNotifications( $currentUser ) {
1331 global $wgUseEnotif;
1332 if ( !$wgUseEnotif ) {
1333 $this->setNewtalk( false );
1334 return;
1335 }
1336 if( $currentUser != 0 ) {
1337
1338 $dbw =& wfGetDB( DB_MASTER );
1339 $success = $dbw->update( 'watchlist',
1340 array( /* SET */
1341 'wl_notificationtimestamp' => 0
1342 ), array( /* WHERE */
1343 'wl_user' => $currentUser
1344 ), 'UserMailer::clearAll'
1345 );
1346
1347 # we also need to clear here the "you have new message" notification for the own user_talk page
1348 # This is cleared one page view later in Article::viewUpdates();
1349 }
1350 }
1351
1352 /**
1353 * @private
1354 * @return string Encoding options
1355 */
1356 function encodeOptions() {
1357 $a = array();
1358 foreach ( $this->mOptions as $oname => $oval ) {
1359 array_push( $a, $oname.'='.$oval );
1360 }
1361 $s = implode( "\n", $a );
1362 return $s;
1363 }
1364
1365 /**
1366 * @private
1367 */
1368 function decodeOptions( $str ) {
1369 $a = explode( "\n", $str );
1370 foreach ( $a as $s ) {
1371 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1372 $this->mOptions[$m[1]] = $m[2];
1373 }
1374 }
1375 }
1376
1377 function setCookies() {
1378 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1379 if ( 0 == $this->mId ) return;
1380 $this->loadFromDatabase();
1381 $exp = time() + $wgCookieExpiration;
1382
1383 $_SESSION['wsUserID'] = $this->mId;
1384 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1385
1386 $_SESSION['wsUserName'] = $this->getName();
1387 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1388
1389 $_SESSION['wsToken'] = $this->mToken;
1390 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1391 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1392 } else {
1393 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1394 }
1395 }
1396
1397 /**
1398 * Logout user
1399 * It will clean the session cookie
1400 */
1401 function logout() {
1402 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1403 $this->loadDefaults();
1404 $this->setLoaded( true );
1405
1406 $_SESSION['wsUserID'] = 0;
1407
1408 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1409 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1410
1411 # Remember when user logged out, to prevent seeing cached pages
1412 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1413 }
1414
1415 /**
1416 * Save object settings into database
1417 */
1418 function saveSettings() {
1419 global $wgMemc, $wgDBname;
1420 $fname = 'User::saveSettings';
1421
1422 if ( wfReadOnly() ) { return; }
1423 if ( 0 == $this->mId ) { return; }
1424
1425 $dbw =& wfGetDB( DB_MASTER );
1426 $dbw->update( 'user',
1427 array( /* SET */
1428 'user_name' => $this->mName,
1429 'user_password' => $this->mPassword,
1430 'user_newpassword' => $this->mNewpassword,
1431 'user_real_name' => $this->mRealName,
1432 'user_email' => $this->mEmail,
1433 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1434 'user_options' => $this->encodeOptions(),
1435 'user_touched' => $dbw->timestamp($this->mTouched),
1436 'user_token' => $this->mToken
1437 ), array( /* WHERE */
1438 'user_id' => $this->mId
1439 ), $fname
1440 );
1441 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1442 }
1443
1444
1445 /**
1446 * Checks if a user with the given name exists, returns the ID
1447 */
1448 function idForName() {
1449 $fname = 'User::idForName';
1450
1451 $gotid = 0;
1452 $s = trim( $this->getName() );
1453 if ( 0 == strcmp( '', $s ) ) return 0;
1454
1455 $dbr =& wfGetDB( DB_SLAVE );
1456 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1457 if ( $id === false ) {
1458 $id = 0;
1459 }
1460 return $id;
1461 }
1462
1463 /**
1464 * Add user object to the database
1465 */
1466 function addToDatabase() {
1467 $fname = 'User::addToDatabase';
1468 $dbw =& wfGetDB( DB_MASTER );
1469 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1470 $dbw->insert( 'user',
1471 array(
1472 'user_id' => $seqVal,
1473 'user_name' => $this->mName,
1474 'user_password' => $this->mPassword,
1475 'user_newpassword' => $this->mNewpassword,
1476 'user_email' => $this->mEmail,
1477 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1478 'user_real_name' => $this->mRealName,
1479 'user_options' => $this->encodeOptions(),
1480 'user_token' => $this->mToken,
1481 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1482 ), $fname
1483 );
1484 $this->mId = $dbw->insertId();
1485 }
1486
1487 function spreadBlock() {
1488 # If the (non-anonymous) user is blocked, this function will block any IP address
1489 # that they successfully log on from.
1490 $fname = 'User::spreadBlock';
1491
1492 wfDebug( "User:spreadBlock()\n" );
1493 if ( $this->mId == 0 ) {
1494 return;
1495 }
1496
1497 $userblock = Block::newFromDB( '', $this->mId );
1498 if ( !$userblock->isValid() ) {
1499 return;
1500 }
1501
1502 # Check if this IP address is already blocked
1503 $ipblock = Block::newFromDB( wfGetIP() );
1504 if ( $ipblock->isValid() ) {
1505 # If the user is already blocked. Then check if the autoblock would
1506 # excede the user block. If it would excede, then do nothing, else
1507 # prolong block time
1508 if ($userblock->mExpiry &&
1509 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1510 return;
1511 }
1512 # Just update the timestamp
1513 $ipblock->updateTimestamp();
1514 return;
1515 }
1516
1517 # Make a new block object with the desired properties
1518 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1519 $ipblock->mAddress = wfGetIP();
1520 $ipblock->mUser = 0;
1521 $ipblock->mBy = $userblock->mBy;
1522 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1523 $ipblock->mTimestamp = wfTimestampNow();
1524 $ipblock->mAuto = 1;
1525 # If the user is already blocked with an expiry date, we don't
1526 # want to pile on top of that!
1527 if($userblock->mExpiry) {
1528 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1529 } else {
1530 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1531 }
1532
1533 # Insert it
1534 $ipblock->insert();
1535
1536 }
1537
1538 /**
1539 * Generate a string which will be different for any combination of
1540 * user options which would produce different parser output.
1541 * This will be used as part of the hash key for the parser cache,
1542 * so users will the same options can share the same cached data
1543 * safely.
1544 *
1545 * Extensions which require it should install 'PageRenderingHash' hook,
1546 * which will give them a chance to modify this key based on their own
1547 * settings.
1548 *
1549 * @return string
1550 */
1551 function getPageRenderingHash() {
1552 global $wgContLang;
1553 if( $this->mHash ){
1554 return $this->mHash;
1555 }
1556
1557 // stubthreshold is only included below for completeness,
1558 // it will always be 0 when this function is called by parsercache.
1559
1560 $confstr = $this->getOption( 'math' );
1561 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1562 $confstr .= '!' . $this->getOption( 'date' );
1563 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1564 $confstr .= '!' . $this->getOption( 'language' );
1565 $confstr .= '!' . $this->getOption( 'thumbsize' );
1566 // add in language specific options, if any
1567 $extra = $wgContLang->getExtraHashOptions();
1568 $confstr .= $extra;
1569
1570 // Give a chance for extensions to modify the hash, if they have
1571 // extra options or other effects on the parser cache.
1572 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1573
1574 $this->mHash = $confstr;
1575 return $confstr;
1576 }
1577
1578 function isAllowedToCreateAccount() {
1579 return $this->isAllowed( 'createaccount' ) && !$this->isBlocked();
1580 }
1581
1582 /**
1583 * Set mDataLoaded, return previous value
1584 * Use this to prevent DB access in command-line scripts or similar situations
1585 */
1586 function setLoaded( $loaded ) {
1587 return wfSetVar( $this->mDataLoaded, $loaded );
1588 }
1589
1590 /**
1591 * Get this user's personal page title.
1592 *
1593 * @return Title
1594 * @public
1595 */
1596 function getUserPage() {
1597 return Title::makeTitle( NS_USER, $this->getName() );
1598 }
1599
1600 /**
1601 * Get this user's talk page title.
1602 *
1603 * @return Title
1604 * @public
1605 */
1606 function getTalkPage() {
1607 $title = $this->getUserPage();
1608 return $title->getTalkPage();
1609 }
1610
1611 /**
1612 * @static
1613 */
1614 function getMaxID() {
1615 static $res; // cache
1616
1617 if ( isset( $res ) )
1618 return $res;
1619 else {
1620 $dbr =& wfGetDB( DB_SLAVE );
1621 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1622 }
1623 }
1624
1625 /**
1626 * Determine whether the user is a newbie. Newbies are either
1627 * anonymous IPs, or the most recently created accounts.
1628 * @return bool True if it is a newbie.
1629 */
1630 function isNewbie() {
1631 return !$this->isAllowed( 'autoconfirmed' );
1632 }
1633
1634 /**
1635 * Check to see if the given clear-text password is one of the accepted passwords
1636 * @param string $password User password.
1637 * @return bool True if the given password is correct otherwise False.
1638 */
1639 function checkPassword( $password ) {
1640 global $wgAuth, $wgMinimalPasswordLength;
1641 $this->loadFromDatabase();
1642
1643 // Even though we stop people from creating passwords that
1644 // are shorter than this, doesn't mean people wont be able
1645 // to. Certain authentication plugins do NOT want to save
1646 // domain passwords in a mysql database, so we should
1647 // check this (incase $wgAuth->strict() is false).
1648 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1649 return false;
1650 }
1651
1652 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1653 return true;
1654 } elseif( $wgAuth->strict() ) {
1655 /* Auth plugin doesn't allow local authentication */
1656 return false;
1657 }
1658 $ep = $this->encryptPassword( $password );
1659 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1660 return true;
1661 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1662 return true;
1663 } elseif ( function_exists( 'iconv' ) ) {
1664 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1665 # Check for this with iconv
1666 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1667 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1668 return true;
1669 }
1670 }
1671 return false;
1672 }
1673
1674 /**
1675 * Initialize (if necessary) and return a session token value
1676 * which can be used in edit forms to show that the user's
1677 * login credentials aren't being hijacked with a foreign form
1678 * submission.
1679 *
1680 * @param mixed $salt - Optional function-specific data for hash.
1681 * Use a string or an array of strings.
1682 * @return string
1683 * @public
1684 */
1685 function editToken( $salt = '' ) {
1686 if( !isset( $_SESSION['wsEditToken'] ) ) {
1687 $token = $this->generateToken();
1688 $_SESSION['wsEditToken'] = $token;
1689 } else {
1690 $token = $_SESSION['wsEditToken'];
1691 }
1692 if( is_array( $salt ) ) {
1693 $salt = implode( '|', $salt );
1694 }
1695 return md5( $token . $salt );
1696 }
1697
1698 /**
1699 * Generate a hex-y looking random token for various uses.
1700 * Could be made more cryptographically sure if someone cares.
1701 * @return string
1702 */
1703 function generateToken( $salt = '' ) {
1704 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1705 return md5( $token . $salt );
1706 }
1707
1708 /**
1709 * Check given value against the token value stored in the session.
1710 * A match should confirm that the form was submitted from the
1711 * user's own login session, not a form submission from a third-party
1712 * site.
1713 *
1714 * @param string $val - the input value to compare
1715 * @param string $salt - Optional function-specific data for hash
1716 * @return bool
1717 * @public
1718 */
1719 function matchEditToken( $val, $salt = '' ) {
1720 global $wgMemc;
1721 $sessionToken = $this->editToken( $salt );
1722 if ( $val != $sessionToken ) {
1723 wfDebug( "User::matchEditToken: broken session data\n" );
1724 }
1725 return $val == $sessionToken;
1726 }
1727
1728 /**
1729 * Generate a new e-mail confirmation token and send a confirmation
1730 * mail to the user's given address.
1731 *
1732 * @return mixed True on success, a WikiError object on failure.
1733 */
1734 function sendConfirmationMail() {
1735 global $wgContLang;
1736 $url = $this->confirmationTokenUrl( $expiration );
1737 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1738 wfMsg( 'confirmemail_body',
1739 wfGetIP(),
1740 $this->getName(),
1741 $url,
1742 $wgContLang->timeanddate( $expiration, false ) ) );
1743 }
1744
1745 /**
1746 * Send an e-mail to this user's account. Does not check for
1747 * confirmed status or validity.
1748 *
1749 * @param string $subject
1750 * @param string $body
1751 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1752 * @return mixed True on success, a WikiError object on failure.
1753 */
1754 function sendMail( $subject, $body, $from = null ) {
1755 if( is_null( $from ) ) {
1756 global $wgPasswordSender;
1757 $from = $wgPasswordSender;
1758 }
1759
1760 require_once( 'UserMailer.php' );
1761 $to = new MailAddress( $this );
1762 $sender = new MailAddress( $from );
1763 $error = userMailer( $to, $sender, $subject, $body );
1764
1765 if( $error == '' ) {
1766 return true;
1767 } else {
1768 return new WikiError( $error );
1769 }
1770 }
1771
1772 /**
1773 * Generate, store, and return a new e-mail confirmation code.
1774 * A hash (unsalted since it's used as a key) is stored.
1775 * @param &$expiration mixed output: accepts the expiration time
1776 * @return string
1777 * @private
1778 */
1779 function confirmationToken( &$expiration ) {
1780 $fname = 'User::confirmationToken';
1781
1782 $now = time();
1783 $expires = $now + 7 * 24 * 60 * 60;
1784 $expiration = wfTimestamp( TS_MW, $expires );
1785
1786 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1787 $hash = md5( $token );
1788
1789 $dbw =& wfGetDB( DB_MASTER );
1790 $dbw->update( 'user',
1791 array( 'user_email_token' => $hash,
1792 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1793 array( 'user_id' => $this->mId ),
1794 $fname );
1795
1796 return $token;
1797 }
1798
1799 /**
1800 * Generate and store a new e-mail confirmation token, and return
1801 * the URL the user can use to confirm.
1802 * @param &$expiration mixed output: accepts the expiration time
1803 * @return string
1804 * @private
1805 */
1806 function confirmationTokenUrl( &$expiration ) {
1807 $token = $this->confirmationToken( $expiration );
1808 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1809 return $title->getFullUrl();
1810 }
1811
1812 /**
1813 * Mark the e-mail address confirmed and save.
1814 */
1815 function confirmEmail() {
1816 $this->loadFromDatabase();
1817 $this->mEmailAuthenticated = wfTimestampNow();
1818 $this->saveSettings();
1819 return true;
1820 }
1821
1822 /**
1823 * Is this user allowed to send e-mails within limits of current
1824 * site configuration?
1825 * @return bool
1826 */
1827 function canSendEmail() {
1828 return $this->isEmailConfirmed();
1829 }
1830
1831 /**
1832 * Is this user allowed to receive e-mails within limits of current
1833 * site configuration?
1834 * @return bool
1835 */
1836 function canReceiveEmail() {
1837 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1838 }
1839
1840 /**
1841 * Is this user's e-mail address valid-looking and confirmed within
1842 * limits of the current site configuration?
1843 *
1844 * If $wgEmailAuthentication is on, this may require the user to have
1845 * confirmed their address by returning a code or using a password
1846 * sent to the address from the wiki.
1847 *
1848 * @return bool
1849 */
1850 function isEmailConfirmed() {
1851 global $wgEmailAuthentication;
1852 $this->loadFromDatabase();
1853 if( $this->isAnon() )
1854 return false;
1855 if( !$this->isValidEmailAddr( $this->mEmail ) )
1856 return false;
1857 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1858 return false;
1859 return true;
1860 }
1861
1862 /**
1863 * @param array $groups list of groups
1864 * @return array list of permission key names for given groups combined
1865 * @static
1866 */
1867 function getGroupPermissions( $groups ) {
1868 global $wgGroupPermissions;
1869 $rights = array();
1870 foreach( $groups as $group ) {
1871 if( isset( $wgGroupPermissions[$group] ) ) {
1872 $rights = array_merge( $rights,
1873 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1874 }
1875 }
1876 return $rights;
1877 }
1878
1879 /**
1880 * @param string $group key name
1881 * @return string localized descriptive name, if provided
1882 * @static
1883 */
1884 function getGroupName( $group ) {
1885 $key = "group-$group-name";
1886 $name = wfMsg( $key );
1887 if( $name == '' || $name == "&lt;$key&gt;" ) {
1888 return $group;
1889 } else {
1890 return $name;
1891 }
1892 }
1893
1894 /**
1895 * Return the set of defined explicit groups.
1896 * The * and 'user' groups are not included.
1897 * @return array
1898 * @static
1899 */
1900 function getAllGroups() {
1901 global $wgGroupPermissions;
1902 return array_diff(
1903 array_keys( $wgGroupPermissions ),
1904 array( '*', 'user', 'autoconfirmed' ) );
1905 }
1906 }
1907
1908 ?>