Got rid of the MagicWord indexing constants (MAG_xxx), replaced them by string indexi...
[lhc/web/wiklou.git] / includes / User.php
1 <?php
2 /**
3 * See user.txt
4 *
5 * @package MediaWiki
6 */
7
8 # Number of characters in user_token field
9 define( 'USER_TOKEN_LENGTH', 32 );
10
11 # Serialized record version
12 define( 'MW_USER_VERSION', 3 );
13
14 /**
15 *
16 * @package MediaWiki
17 */
18 class User {
19 /*
20 * When adding a new private variable, dont forget to add it to __sleep()
21 */
22 /**@{{
23 * @private
24 */
25 var $mBlockedby; //!<
26 var $mBlockreason; //!<
27 var $mBlock; //!<
28 var $mDataLoaded; //!<
29 var $mEmail; //!<
30 var $mEmailAuthenticated; //!<
31 var $mGroups; //!<
32 var $mHash; //!<
33 var $mId; //!<
34 var $mName; //!<
35 var $mNewpassword; //!<
36 var $mNewtalk; //!<
37 var $mOptions; //!<
38 var $mPassword; //!<
39 var $mRealName; //!<
40 var $mRegistration; //!<
41 var $mRights; //!<
42 var $mSkin; //!<
43 var $mToken; //!<
44 var $mTouched; //!<
45 var $mVersion; //!< serialized version
46 /**@}} */
47
48 /** Constructor using User:loadDefaults() */
49 function User() {
50 $this->loadDefaults();
51 $this->mVersion = MW_USER_VERSION;
52 }
53
54 /**
55 * Static factory method
56 * @param string $name Username, validated by Title:newFromText()
57 * @param bool $validate Validate username
58 * @return User
59 * @static
60 */
61 function newFromName( $name, $validate = true ) {
62 # Force usernames to capital
63 global $wgContLang;
64 $name = $wgContLang->ucfirst( $name );
65
66 # Clean up name according to title rules
67 $t = Title::newFromText( $name );
68 if( is_null( $t ) ) {
69 return null;
70 }
71
72 # Reject various classes of invalid names
73 $canonicalName = $t->getText();
74 global $wgAuth;
75 $canonicalName = $wgAuth->getCanonicalName( $t->getText() );
76
77 if( $validate && !User::isValidUserName( $canonicalName ) ) {
78 return null;
79 }
80
81 $u = new User();
82 $u->setName( $canonicalName );
83 $u->setId( $u->idFromName( $canonicalName ) );
84 return $u;
85 }
86
87 /**
88 * Factory method to fetch whichever use has a given email confirmation code.
89 * This code is generated when an account is created or its e-mail address
90 * has changed.
91 *
92 * If the code is invalid or has expired, returns NULL.
93 *
94 * @param string $code
95 * @return User
96 * @static
97 */
98 function newFromConfirmationCode( $code ) {
99 $dbr =& wfGetDB( DB_SLAVE );
100 $name = $dbr->selectField( 'user', 'user_name', array(
101 'user_email_token' => md5( $code ),
102 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
103 ) );
104 if( is_string( $name ) ) {
105 return User::newFromName( $name );
106 } else {
107 return null;
108 }
109 }
110
111 /**
112 * Serialze sleep function, for better cache efficiency and avoidance of
113 * silly "incomplete type" errors when skins are cached. The array should
114 * contain names of private variables (see at top of User.php).
115 */
116 function __sleep() {
117 return array(
118 'mDataLoaded',
119 'mEmail',
120 'mEmailAuthenticated',
121 'mGroups',
122 'mHash',
123 'mId',
124 'mName',
125 'mNewpassword',
126 'mNewtalk',
127 'mOptions',
128 'mPassword',
129 'mRealName',
130 'mRegistration',
131 'mRights',
132 'mToken',
133 'mTouched',
134 'mVersion',
135 );
136 }
137
138 /**
139 * Get username given an id.
140 * @param integer $id Database user id
141 * @return string Nickname of a user
142 * @static
143 */
144 function whoIs( $id ) {
145 $dbr =& wfGetDB( DB_SLAVE );
146 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
147 }
148
149 /**
150 * Get real username given an id.
151 * @param integer $id Database user id
152 * @return string Realname of a user
153 * @static
154 */
155 function whoIsReal( $id ) {
156 $dbr =& wfGetDB( DB_SLAVE );
157 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
158 }
159
160 /**
161 * Get database id given a user name
162 * @param string $name Nickname of a user
163 * @return integer|null Database user id (null: if non existent
164 * @static
165 */
166 function idFromName( $name ) {
167 $fname = "User::idFromName";
168
169 $nt = Title::newFromText( $name );
170 if( is_null( $nt ) ) {
171 # Illegal name
172 return null;
173 }
174 $dbr =& wfGetDB( DB_SLAVE );
175 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
176
177 if ( $s === false ) {
178 return 0;
179 } else {
180 return $s->user_id;
181 }
182 }
183
184 /**
185 * Does the string match an anonymous IPv4 address?
186 *
187 * This function exists for username validation, in order to reject
188 * usernames which are similar in form to IP addresses. Strings such
189 * as 300.300.300.300 will return true because it looks like an IP
190 * address, despite not being strictly valid.
191 *
192 * 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 $this->mBlock = new Block();
439 $this->mBlock->fromMaster( !$bFromSlave );
440 if ( $this->mBlock->load( $ip , $this->mId ) ) {
441 wfDebug( "$fname: Found block.\n" );
442 $this->mBlockedby = $this->mBlock->mBy;
443 $this->mBlockreason = $this->mBlock->mReason;
444 if ( $this->isLoggedIn() ) {
445 $this->spreadBlock();
446 }
447 } else {
448 $this->mBlock = null;
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 # Set block status to unloaded, that should be loaded every time
698 $user->mBlockedby = -1;
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 # Implicit group for users whose email addresses are confirmed
780 global $wgEmailAuthentication;
781 if( $this->isValidEmailAddr( $this->mEmail ) ) {
782 if( $wgEmailAuthentication ) {
783 if( $this->mEmailAuthenticated )
784 $implicitGroups[] = 'emailconfirmed';
785 } else {
786 $implicitGroups[] = 'emailconfirmed';
787 }
788 }
789
790 $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
791 $this->mRights = $this->getGroupPermissions( $effectiveGroups );
792 }
793
794 $this->mDataLoaded = true;
795 }
796
797 function getID() { return $this->mId; }
798 function setID( $v ) {
799 $this->mId = $v;
800 $this->mDataLoaded = false;
801 }
802
803 function getName() {
804 $this->loadFromDatabase();
805 if ( $this->mName === false ) {
806 $this->mName = wfGetIP();
807 }
808 return $this->mName;
809 }
810
811 function setName( $str ) {
812 $this->loadFromDatabase();
813 $this->mName = $str;
814 }
815
816
817 /**
818 * Return the title dbkey form of the name, for eg user pages.
819 * @return string
820 * @public
821 */
822 function getTitleKey() {
823 return str_replace( ' ', '_', $this->getName() );
824 }
825
826 function getNewtalk() {
827 $this->loadFromDatabase();
828
829 # Load the newtalk status if it is unloaded (mNewtalk=-1)
830 if( $this->mNewtalk === -1 ) {
831 $this->mNewtalk = false; # reset talk page status
832
833 # Check memcached separately for anons, who have no
834 # entire User object stored in there.
835 if( !$this->mId ) {
836 global $wgDBname, $wgMemc;
837 $key = "$wgDBname:newtalk:ip:" . $this->getName();
838 $newtalk = $wgMemc->get( $key );
839 if( is_integer( $newtalk ) ) {
840 $this->mNewtalk = (bool)$newtalk;
841 } else {
842 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
843 $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
844 }
845 } else {
846 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
847 }
848 }
849
850 return (bool)$this->mNewtalk;
851 }
852
853 /**
854 * Return the talk page(s) this user has new messages on.
855 */
856 function getNewMessageLinks() {
857 global $wgDBname;
858 $talks = array();
859 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
860 return $talks;
861
862 if (!$this->getNewtalk())
863 return array();
864 $up = $this->getUserPage();
865 $utp = $up->getTalkPage();
866 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
867 }
868
869
870 /**
871 * Perform a user_newtalk check on current slaves; if the memcached data
872 * is funky we don't want newtalk state to get stuck on save, as that's
873 * damn annoying.
874 *
875 * @param string $field
876 * @param mixed $id
877 * @return bool
878 * @private
879 */
880 function checkNewtalk( $field, $id ) {
881 $fname = 'User::checkNewtalk';
882 $dbr =& wfGetDB( DB_SLAVE );
883 $ok = $dbr->selectField( 'user_newtalk', $field,
884 array( $field => $id ), $fname );
885 return $ok !== false;
886 }
887
888 /**
889 * Add or update the
890 * @param string $field
891 * @param mixed $id
892 * @private
893 */
894 function updateNewtalk( $field, $id ) {
895 $fname = 'User::updateNewtalk';
896 if( $this->checkNewtalk( $field, $id ) ) {
897 wfDebug( "$fname already set ($field, $id), ignoring\n" );
898 return false;
899 }
900 $dbw =& wfGetDB( DB_MASTER );
901 $dbw->insert( 'user_newtalk',
902 array( $field => $id ),
903 $fname,
904 'IGNORE' );
905 wfDebug( "$fname: set on ($field, $id)\n" );
906 return true;
907 }
908
909 /**
910 * Clear the new messages flag for the given user
911 * @param string $field
912 * @param mixed $id
913 * @private
914 */
915 function deleteNewtalk( $field, $id ) {
916 $fname = 'User::deleteNewtalk';
917 if( !$this->checkNewtalk( $field, $id ) ) {
918 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
919 return false;
920 }
921 $dbw =& wfGetDB( DB_MASTER );
922 $dbw->delete( 'user_newtalk',
923 array( $field => $id ),
924 $fname );
925 wfDebug( "$fname: killed on ($field, $id)\n" );
926 return true;
927 }
928
929 /**
930 * Update the 'You have new messages!' status.
931 * @param bool $val
932 */
933 function setNewtalk( $val ) {
934 if( wfReadOnly() ) {
935 return;
936 }
937
938 $this->loadFromDatabase();
939 $this->mNewtalk = $val;
940
941 $fname = 'User::setNewtalk';
942
943 if( $this->isAnon() ) {
944 $field = 'user_ip';
945 $id = $this->getName();
946 } else {
947 $field = 'user_id';
948 $id = $this->getId();
949 }
950
951 if( $val ) {
952 $changed = $this->updateNewtalk( $field, $id );
953 } else {
954 $changed = $this->deleteNewtalk( $field, $id );
955 }
956
957 if( $changed ) {
958 if( $this->isAnon() ) {
959 // Anons have a separate memcached space, since
960 // user records aren't kept for them.
961 global $wgDBname, $wgMemc;
962 $key = "$wgDBname:newtalk:ip:$val";
963 $wgMemc->set( $key, $val ? 1 : 0 );
964 } else {
965 if( $val ) {
966 // Make sure the user page is watched, so a notification
967 // will be sent out if enabled.
968 $this->addWatch( $this->getTalkPage() );
969 }
970 }
971 $this->invalidateCache();
972 $this->saveSettings();
973 }
974 }
975
976 function invalidateCache() {
977 global $wgClockSkewFudge;
978 $this->loadFromDatabase();
979 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
980 # Don't forget to save the options after this or
981 # it won't take effect!
982 }
983
984 function validateCache( $timestamp ) {
985 $this->loadFromDatabase();
986 return ($timestamp >= $this->mTouched);
987 }
988
989 /**
990 * Encrypt a password.
991 * It can eventuall salt a password @see User::addSalt()
992 * @param string $p clear Password.
993 * @return string Encrypted password.
994 */
995 function encryptPassword( $p ) {
996 return wfEncryptPassword( $this->mId, $p );
997 }
998
999 # Set the password and reset the random token
1000 function setPassword( $str ) {
1001 $this->loadFromDatabase();
1002 $this->setToken();
1003 $this->mPassword = $this->encryptPassword( $str );
1004 $this->mNewpassword = '';
1005 }
1006
1007 # Set the random token (used for persistent authentication)
1008 function setToken( $token = false ) {
1009 global $wgSecretKey, $wgProxyKey, $wgDBname;
1010 if ( !$token ) {
1011 if ( $wgSecretKey ) {
1012 $key = $wgSecretKey;
1013 } elseif ( $wgProxyKey ) {
1014 $key = $wgProxyKey;
1015 } else {
1016 $key = microtime();
1017 }
1018 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1019 } else {
1020 $this->mToken = $token;
1021 }
1022 }
1023
1024
1025 function setCookiePassword( $str ) {
1026 $this->loadFromDatabase();
1027 $this->mCookiePassword = md5( $str );
1028 }
1029
1030 function setNewpassword( $str ) {
1031 $this->loadFromDatabase();
1032 $this->mNewpassword = $this->encryptPassword( $str );
1033 }
1034
1035 function getEmail() {
1036 $this->loadFromDatabase();
1037 return $this->mEmail;
1038 }
1039
1040 function getEmailAuthenticationTimestamp() {
1041 $this->loadFromDatabase();
1042 return $this->mEmailAuthenticated;
1043 }
1044
1045 function setEmail( $str ) {
1046 $this->loadFromDatabase();
1047 $this->mEmail = $str;
1048 }
1049
1050 function getRealName() {
1051 $this->loadFromDatabase();
1052 return $this->mRealName;
1053 }
1054
1055 function setRealName( $str ) {
1056 $this->loadFromDatabase();
1057 $this->mRealName = $str;
1058 }
1059
1060 /**
1061 * @param string $oname The option to check
1062 * @return string
1063 */
1064 function getOption( $oname ) {
1065 $this->loadFromDatabase();
1066 if ( array_key_exists( $oname, $this->mOptions ) ) {
1067 return trim( $this->mOptions[$oname] );
1068 } else {
1069 return '';
1070 }
1071 }
1072
1073 /**
1074 * @param string $oname The option to check
1075 * @return bool False if the option is not selected, true if it is
1076 */
1077 function getBoolOption( $oname ) {
1078 return (bool)$this->getOption( $oname );
1079 }
1080
1081 /**
1082 * Get an option as an integer value from the source string.
1083 * @param string $oname The option to check
1084 * @param int $default Optional value to return if option is unset/blank.
1085 * @return int
1086 */
1087 function getIntOption( $oname, $default=0 ) {
1088 $val = $this->getOption( $oname );
1089 if( $val == '' ) {
1090 $val = $default;
1091 }
1092 return intval( $val );
1093 }
1094
1095 function setOption( $oname, $val ) {
1096 $this->loadFromDatabase();
1097 if ( $oname == 'skin' ) {
1098 # Clear cached skin, so the new one displays immediately in Special:Preferences
1099 unset( $this->mSkin );
1100 }
1101 // Filter out any newlines that may have passed through input validation.
1102 // Newlines are used to separate items in the options blob.
1103 $val = str_replace( "\r\n", "\n", $val );
1104 $val = str_replace( "\r", "\n", $val );
1105 $val = str_replace( "\n", " ", $val );
1106 $this->mOptions[$oname] = $val;
1107 $this->invalidateCache();
1108 }
1109
1110 function getRights() {
1111 $this->loadFromDatabase();
1112 return $this->mRights;
1113 }
1114
1115 /**
1116 * Get the list of explicit group memberships this user has.
1117 * The implicit * and user groups are not included.
1118 * @return array of strings
1119 */
1120 function getGroups() {
1121 $this->loadFromDatabase();
1122 return $this->mGroups;
1123 }
1124
1125 /**
1126 * Get the list of implicit group memberships this user has.
1127 * This includes all explicit groups, plus 'user' if logged in
1128 * and '*' for all accounts.
1129 * @return array of strings
1130 */
1131 function getEffectiveGroups() {
1132 $base = array( '*' );
1133 if( $this->isLoggedIn() ) {
1134 $base[] = 'user';
1135 }
1136 return array_merge( $base, $this->getGroups() );
1137 }
1138
1139 /**
1140 * Add the user to the given group.
1141 * This takes immediate effect.
1142 * @string $group
1143 */
1144 function addGroup( $group ) {
1145 $dbw =& wfGetDB( DB_MASTER );
1146 $dbw->insert( 'user_groups',
1147 array(
1148 'ug_user' => $this->getID(),
1149 'ug_group' => $group,
1150 ),
1151 'User::addGroup',
1152 array( 'IGNORE' ) );
1153
1154 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1155 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1156
1157 $this->invalidateCache();
1158 $this->saveSettings();
1159 }
1160
1161 /**
1162 * Remove the user from the given group.
1163 * This takes immediate effect.
1164 * @string $group
1165 */
1166 function removeGroup( $group ) {
1167 $dbw =& wfGetDB( DB_MASTER );
1168 $dbw->delete( 'user_groups',
1169 array(
1170 'ug_user' => $this->getID(),
1171 'ug_group' => $group,
1172 ),
1173 'User::removeGroup' );
1174
1175 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1176 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1177
1178 $this->invalidateCache();
1179 $this->saveSettings();
1180 }
1181
1182
1183 /**
1184 * A more legible check for non-anonymousness.
1185 * Returns true if the user is not an anonymous visitor.
1186 *
1187 * @return bool
1188 */
1189 function isLoggedIn() {
1190 return( $this->getID() != 0 );
1191 }
1192
1193 /**
1194 * A more legible check for anonymousness.
1195 * Returns true if the user is an anonymous visitor.
1196 *
1197 * @return bool
1198 */
1199 function isAnon() {
1200 return !$this->isLoggedIn();
1201 }
1202
1203 /**
1204 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1205 * @deprecated
1206 */
1207 function isSysop() {
1208 throw new MWException( "Call to deprecated (v1.7) User::isSysop() method\n" );
1209 #return $this->isAllowed( 'protect' );
1210 }
1211
1212 /**
1213 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1214 * @deprecated
1215 */
1216 function isDeveloper() {
1217 throw new MWException( "Call to deprecated (v1.7) User::isDeveloper() method\n" );
1218 #return $this->isAllowed( 'siteadmin' );
1219 }
1220
1221 /**
1222 * Deprecated in 1.6, die in 1.7, to be removed in 1.8
1223 * @deprecated
1224 */
1225 function isBureaucrat() {
1226 throw new MWException( "Call to deprecated (v1.7) User::isBureaucrat() method\n" );
1227 #return $this->isAllowed( 'makesysop' );
1228 }
1229
1230 /**
1231 * Whether the user is a bot
1232 * @todo need to be migrated to the new user level management sytem
1233 */
1234 function isBot() {
1235 $this->loadFromDatabase();
1236 return in_array( 'bot', $this->mRights );
1237 }
1238
1239 /**
1240 * Check if user is allowed to access a feature / make an action
1241 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1242 * @return boolean True: action is allowed, False: action should not be allowed
1243 */
1244 function isAllowed($action='') {
1245 if ( $action === '' )
1246 // In the spirit of DWIM
1247 return true;
1248
1249 $this->loadFromDatabase();
1250 return in_array( $action , $this->mRights );
1251 }
1252
1253 /**
1254 * Load a skin if it doesn't exist or return it
1255 * @todo FIXME : need to check the old failback system [AV]
1256 */
1257 function &getSkin() {
1258 global $IP, $wgRequest;
1259 if ( ! isset( $this->mSkin ) ) {
1260 $fname = 'User::getSkin';
1261 wfProfileIn( $fname );
1262
1263 # get the user skin
1264 $userSkin = $this->getOption( 'skin' );
1265 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1266
1267 $this->mSkin =& Skin::newFromKey( $userSkin );
1268 wfProfileOut( $fname );
1269 }
1270 return $this->mSkin;
1271 }
1272
1273 /**#@+
1274 * @param string $title Article title to look at
1275 */
1276
1277 /**
1278 * Check watched status of an article
1279 * @return bool True if article is watched
1280 */
1281 function isWatched( $title ) {
1282 $wl = WatchedItem::fromUserTitle( $this, $title );
1283 return $wl->isWatched();
1284 }
1285
1286 /**
1287 * Watch an article
1288 */
1289 function addWatch( $title ) {
1290 $wl = WatchedItem::fromUserTitle( $this, $title );
1291 $wl->addWatch();
1292 $this->invalidateCache();
1293 }
1294
1295 /**
1296 * Stop watching an article
1297 */
1298 function removeWatch( $title ) {
1299 $wl = WatchedItem::fromUserTitle( $this, $title );
1300 $wl->removeWatch();
1301 $this->invalidateCache();
1302 }
1303
1304 /**
1305 * Clear the user's notification timestamp for the given title.
1306 * If e-notif e-mails are on, they will receive notification mails on
1307 * the next change of the page if it's watched etc.
1308 */
1309 function clearNotification( &$title ) {
1310 global $wgUser, $wgUseEnotif;
1311
1312
1313 if ($title->getNamespace() == NS_USER_TALK &&
1314 $title->getText() == $this->getName() ) {
1315 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1316 return;
1317 $this->setNewtalk( false );
1318 }
1319
1320 if( !$wgUseEnotif ) {
1321 return;
1322 }
1323
1324 if( $this->isAnon() ) {
1325 // Nothing else to do...
1326 return;
1327 }
1328
1329 // Only update the timestamp if the page is being watched.
1330 // The query to find out if it is watched is cached both in memcached and per-invocation,
1331 // and when it does have to be executed, it can be on a slave
1332 // If this is the user's newtalk page, we always update the timestamp
1333 if ($title->getNamespace() == NS_USER_TALK &&
1334 $title->getText() == $wgUser->getName())
1335 {
1336 $watched = true;
1337 } elseif ( $this->getID() == $wgUser->getID() ) {
1338 $watched = $title->userIsWatching();
1339 } else {
1340 $watched = true;
1341 }
1342
1343 // If the page is watched by the user (or may be watched), update the timestamp on any
1344 // any matching rows
1345 if ( $watched ) {
1346 $dbw =& wfGetDB( DB_MASTER );
1347 $success = $dbw->update( 'watchlist',
1348 array( /* SET */
1349 'wl_notificationtimestamp' => NULL
1350 ), array( /* WHERE */
1351 'wl_title' => $title->getDBkey(),
1352 'wl_namespace' => $title->getNamespace(),
1353 'wl_user' => $this->getID()
1354 ), 'User::clearLastVisited'
1355 );
1356 }
1357 }
1358
1359 /**#@-*/
1360
1361 /**
1362 * Resets all of the given user's page-change notification timestamps.
1363 * If e-notif e-mails are on, they will receive notification mails on
1364 * the next change of any watched page.
1365 *
1366 * @param int $currentUser user ID number
1367 * @public
1368 */
1369 function clearAllNotifications( $currentUser ) {
1370 global $wgUseEnotif;
1371 if ( !$wgUseEnotif ) {
1372 $this->setNewtalk( false );
1373 return;
1374 }
1375 if( $currentUser != 0 ) {
1376
1377 $dbw =& wfGetDB( DB_MASTER );
1378 $success = $dbw->update( 'watchlist',
1379 array( /* SET */
1380 'wl_notificationtimestamp' => 0
1381 ), array( /* WHERE */
1382 'wl_user' => $currentUser
1383 ), 'UserMailer::clearAll'
1384 );
1385
1386 # we also need to clear here the "you have new message" notification for the own user_talk page
1387 # This is cleared one page view later in Article::viewUpdates();
1388 }
1389 }
1390
1391 /**
1392 * @private
1393 * @return string Encoding options
1394 */
1395 function encodeOptions() {
1396 $a = array();
1397 foreach ( $this->mOptions as $oname => $oval ) {
1398 array_push( $a, $oname.'='.$oval );
1399 }
1400 $s = implode( "\n", $a );
1401 return $s;
1402 }
1403
1404 /**
1405 * @private
1406 */
1407 function decodeOptions( $str ) {
1408 $a = explode( "\n", $str );
1409 foreach ( $a as $s ) {
1410 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1411 $this->mOptions[$m[1]] = $m[2];
1412 }
1413 }
1414 }
1415
1416 function setCookies() {
1417 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1418 if ( 0 == $this->mId ) return;
1419 $this->loadFromDatabase();
1420 $exp = time() + $wgCookieExpiration;
1421
1422 $_SESSION['wsUserID'] = $this->mId;
1423 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1424
1425 $_SESSION['wsUserName'] = $this->getName();
1426 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1427
1428 $_SESSION['wsToken'] = $this->mToken;
1429 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1430 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1431 } else {
1432 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1433 }
1434 }
1435
1436 /**
1437 * Logout user
1438 * It will clean the session cookie
1439 */
1440 function logout() {
1441 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1442 $this->loadDefaults();
1443 $this->setLoaded( true );
1444
1445 $_SESSION['wsUserID'] = 0;
1446
1447 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1448 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1449
1450 # Remember when user logged out, to prevent seeing cached pages
1451 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1452 }
1453
1454 /**
1455 * Save object settings into database
1456 */
1457 function saveSettings() {
1458 global $wgMemc, $wgDBname;
1459 $fname = 'User::saveSettings';
1460
1461 if ( wfReadOnly() ) { return; }
1462 if ( 0 == $this->mId ) { return; }
1463
1464 $dbw =& wfGetDB( DB_MASTER );
1465 $dbw->update( 'user',
1466 array( /* SET */
1467 'user_name' => $this->mName,
1468 'user_password' => $this->mPassword,
1469 'user_newpassword' => $this->mNewpassword,
1470 'user_real_name' => $this->mRealName,
1471 'user_email' => $this->mEmail,
1472 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1473 'user_options' => $this->encodeOptions(),
1474 'user_touched' => $dbw->timestamp($this->mTouched),
1475 'user_token' => $this->mToken
1476 ), array( /* WHERE */
1477 'user_id' => $this->mId
1478 ), $fname
1479 );
1480 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1481 }
1482
1483
1484 /**
1485 * Checks if a user with the given name exists, returns the ID
1486 */
1487 function idForName() {
1488 $fname = 'User::idForName';
1489
1490 $gotid = 0;
1491 $s = trim( $this->getName() );
1492 if ( 0 == strcmp( '', $s ) ) return 0;
1493
1494 $dbr =& wfGetDB( DB_SLAVE );
1495 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1496 if ( $id === false ) {
1497 $id = 0;
1498 }
1499 return $id;
1500 }
1501
1502 /**
1503 * Add user object to the database
1504 */
1505 function addToDatabase() {
1506 $fname = 'User::addToDatabase';
1507 $dbw =& wfGetDB( DB_MASTER );
1508 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1509 $dbw->insert( 'user',
1510 array(
1511 'user_id' => $seqVal,
1512 'user_name' => $this->mName,
1513 'user_password' => $this->mPassword,
1514 'user_newpassword' => $this->mNewpassword,
1515 'user_email' => $this->mEmail,
1516 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1517 'user_real_name' => $this->mRealName,
1518 'user_options' => $this->encodeOptions(),
1519 'user_token' => $this->mToken,
1520 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1521 ), $fname
1522 );
1523 $this->mId = $dbw->insertId();
1524 }
1525
1526 function spreadBlock() {
1527 # If the (non-anonymous) user is blocked, this function will block any IP address
1528 # that they successfully log on from.
1529 $fname = 'User::spreadBlock';
1530
1531 wfDebug( "User:spreadBlock()\n" );
1532 if ( $this->mId == 0 ) {
1533 return;
1534 }
1535
1536 $userblock = Block::newFromDB( '', $this->mId );
1537 if ( !$userblock ) {
1538 return;
1539 }
1540
1541 # Check if this IP address is already blocked
1542 $ipblock = Block::newFromDB( wfGetIP() );
1543 if ( $ipblock ) {
1544 # If the user is already blocked. Then check if the autoblock would
1545 # excede the user block. If it would excede, then do nothing, else
1546 # prolong block time
1547 if ($userblock->mExpiry &&
1548 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1549 return;
1550 }
1551 # Just update the timestamp
1552 $ipblock->updateTimestamp();
1553 return;
1554 } else {
1555 $ipblock = new Block;
1556 }
1557
1558 # Make a new block object with the desired properties
1559 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1560 $ipblock->mAddress = wfGetIP();
1561 $ipblock->mUser = 0;
1562 $ipblock->mBy = $userblock->mBy;
1563 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1564 $ipblock->mTimestamp = wfTimestampNow();
1565 $ipblock->mAuto = 1;
1566 # If the user is already blocked with an expiry date, we don't
1567 # want to pile on top of that!
1568 if($userblock->mExpiry) {
1569 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1570 } else {
1571 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1572 }
1573
1574 # Insert it
1575 $ipblock->insert();
1576
1577 }
1578
1579 /**
1580 * Generate a string which will be different for any combination of
1581 * user options which would produce different parser output.
1582 * This will be used as part of the hash key for the parser cache,
1583 * so users will the same options can share the same cached data
1584 * safely.
1585 *
1586 * Extensions which require it should install 'PageRenderingHash' hook,
1587 * which will give them a chance to modify this key based on their own
1588 * settings.
1589 *
1590 * @return string
1591 */
1592 function getPageRenderingHash() {
1593 global $wgContLang;
1594 if( $this->mHash ){
1595 return $this->mHash;
1596 }
1597
1598 // stubthreshold is only included below for completeness,
1599 // it will always be 0 when this function is called by parsercache.
1600
1601 $confstr = $this->getOption( 'math' );
1602 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1603 $confstr .= '!' . $this->getOption( 'date' );
1604 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1605 $confstr .= '!' . $this->getOption( 'language' );
1606 $confstr .= '!' . $this->getOption( 'thumbsize' );
1607 // add in language specific options, if any
1608 $extra = $wgContLang->getExtraHashOptions();
1609 $confstr .= $extra;
1610
1611 // Give a chance for extensions to modify the hash, if they have
1612 // extra options or other effects on the parser cache.
1613 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1614
1615 $this->mHash = $confstr;
1616 return $confstr;
1617 }
1618
1619 function isBlockedFromCreateAccount() {
1620 $this->getBlockedStatus();
1621 return $this->mBlock && $this->mBlock->mCreateAccount;
1622 }
1623
1624 function isAllowedToCreateAccount() {
1625 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
1626 }
1627
1628 /**
1629 * Set mDataLoaded, return previous value
1630 * Use this to prevent DB access in command-line scripts or similar situations
1631 */
1632 function setLoaded( $loaded ) {
1633 return wfSetVar( $this->mDataLoaded, $loaded );
1634 }
1635
1636 /**
1637 * Get this user's personal page title.
1638 *
1639 * @return Title
1640 * @public
1641 */
1642 function getUserPage() {
1643 return Title::makeTitle( NS_USER, $this->getName() );
1644 }
1645
1646 /**
1647 * Get this user's talk page title.
1648 *
1649 * @return Title
1650 * @public
1651 */
1652 function getTalkPage() {
1653 $title = $this->getUserPage();
1654 return $title->getTalkPage();
1655 }
1656
1657 /**
1658 * @static
1659 */
1660 function getMaxID() {
1661 static $res; // cache
1662
1663 if ( isset( $res ) )
1664 return $res;
1665 else {
1666 $dbr =& wfGetDB( DB_SLAVE );
1667 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1668 }
1669 }
1670
1671 /**
1672 * Determine whether the user is a newbie. Newbies are either
1673 * anonymous IPs, or the most recently created accounts.
1674 * @return bool True if it is a newbie.
1675 */
1676 function isNewbie() {
1677 return !$this->isAllowed( 'autoconfirmed' );
1678 }
1679
1680 /**
1681 * Check to see if the given clear-text password is one of the accepted passwords
1682 * @param string $password User password.
1683 * @return bool True if the given password is correct otherwise False.
1684 */
1685 function checkPassword( $password ) {
1686 global $wgAuth, $wgMinimalPasswordLength;
1687 $this->loadFromDatabase();
1688
1689 // Even though we stop people from creating passwords that
1690 // are shorter than this, doesn't mean people wont be able
1691 // to. Certain authentication plugins do NOT want to save
1692 // domain passwords in a mysql database, so we should
1693 // check this (incase $wgAuth->strict() is false).
1694 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1695 return false;
1696 }
1697
1698 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1699 return true;
1700 } elseif( $wgAuth->strict() ) {
1701 /* Auth plugin doesn't allow local authentication */
1702 return false;
1703 }
1704 $ep = $this->encryptPassword( $password );
1705 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1706 return true;
1707 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1708 return true;
1709 } elseif ( function_exists( 'iconv' ) ) {
1710 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1711 # Check for this with iconv
1712 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1713 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1714 return true;
1715 }
1716 }
1717 return false;
1718 }
1719
1720 /**
1721 * Initialize (if necessary) and return a session token value
1722 * which can be used in edit forms to show that the user's
1723 * login credentials aren't being hijacked with a foreign form
1724 * submission.
1725 *
1726 * @param mixed $salt - Optional function-specific data for hash.
1727 * Use a string or an array of strings.
1728 * @return string
1729 * @public
1730 */
1731 function editToken( $salt = '' ) {
1732 if( !isset( $_SESSION['wsEditToken'] ) ) {
1733 $token = $this->generateToken();
1734 $_SESSION['wsEditToken'] = $token;
1735 } else {
1736 $token = $_SESSION['wsEditToken'];
1737 }
1738 if( is_array( $salt ) ) {
1739 $salt = implode( '|', $salt );
1740 }
1741 return md5( $token . $salt );
1742 }
1743
1744 /**
1745 * Generate a hex-y looking random token for various uses.
1746 * Could be made more cryptographically sure if someone cares.
1747 * @return string
1748 */
1749 function generateToken( $salt = '' ) {
1750 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1751 return md5( $token . $salt );
1752 }
1753
1754 /**
1755 * Check given value against the token value stored in the session.
1756 * A match should confirm that the form was submitted from the
1757 * user's own login session, not a form submission from a third-party
1758 * site.
1759 *
1760 * @param string $val - the input value to compare
1761 * @param string $salt - Optional function-specific data for hash
1762 * @return bool
1763 * @public
1764 */
1765 function matchEditToken( $val, $salt = '' ) {
1766 global $wgMemc;
1767 $sessionToken = $this->editToken( $salt );
1768 if ( $val != $sessionToken ) {
1769 wfDebug( "User::matchEditToken: broken session data\n" );
1770 }
1771 return $val == $sessionToken;
1772 }
1773
1774 /**
1775 * Generate a new e-mail confirmation token and send a confirmation
1776 * mail to the user's given address.
1777 *
1778 * @return mixed True on success, a WikiError object on failure.
1779 */
1780 function sendConfirmationMail() {
1781 global $wgContLang;
1782 $url = $this->confirmationTokenUrl( $expiration );
1783 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1784 wfMsg( 'confirmemail_body',
1785 wfGetIP(),
1786 $this->getName(),
1787 $url,
1788 $wgContLang->timeanddate( $expiration, false ) ) );
1789 }
1790
1791 /**
1792 * Send an e-mail to this user's account. Does not check for
1793 * confirmed status or validity.
1794 *
1795 * @param string $subject
1796 * @param string $body
1797 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1798 * @return mixed True on success, a WikiError object on failure.
1799 */
1800 function sendMail( $subject, $body, $from = null ) {
1801 if( is_null( $from ) ) {
1802 global $wgPasswordSender;
1803 $from = $wgPasswordSender;
1804 }
1805
1806 require_once( 'UserMailer.php' );
1807 $to = new MailAddress( $this );
1808 $sender = new MailAddress( $from );
1809 $error = userMailer( $to, $sender, $subject, $body );
1810
1811 if( $error == '' ) {
1812 return true;
1813 } else {
1814 return new WikiError( $error );
1815 }
1816 }
1817
1818 /**
1819 * Generate, store, and return a new e-mail confirmation code.
1820 * A hash (unsalted since it's used as a key) is stored.
1821 * @param &$expiration mixed output: accepts the expiration time
1822 * @return string
1823 * @private
1824 */
1825 function confirmationToken( &$expiration ) {
1826 $fname = 'User::confirmationToken';
1827
1828 $now = time();
1829 $expires = $now + 7 * 24 * 60 * 60;
1830 $expiration = wfTimestamp( TS_MW, $expires );
1831
1832 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1833 $hash = md5( $token );
1834
1835 $dbw =& wfGetDB( DB_MASTER );
1836 $dbw->update( 'user',
1837 array( 'user_email_token' => $hash,
1838 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1839 array( 'user_id' => $this->mId ),
1840 $fname );
1841
1842 return $token;
1843 }
1844
1845 /**
1846 * Generate and store a new e-mail confirmation token, and return
1847 * the URL the user can use to confirm.
1848 * @param &$expiration mixed output: accepts the expiration time
1849 * @return string
1850 * @private
1851 */
1852 function confirmationTokenUrl( &$expiration ) {
1853 $token = $this->confirmationToken( $expiration );
1854 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1855 return $title->getFullUrl();
1856 }
1857
1858 /**
1859 * Mark the e-mail address confirmed and save.
1860 */
1861 function confirmEmail() {
1862 $this->loadFromDatabase();
1863 $this->mEmailAuthenticated = wfTimestampNow();
1864 $this->saveSettings();
1865 return true;
1866 }
1867
1868 /**
1869 * Is this user allowed to send e-mails within limits of current
1870 * site configuration?
1871 * @return bool
1872 */
1873 function canSendEmail() {
1874 return $this->isEmailConfirmed();
1875 }
1876
1877 /**
1878 * Is this user allowed to receive e-mails within limits of current
1879 * site configuration?
1880 * @return bool
1881 */
1882 function canReceiveEmail() {
1883 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1884 }
1885
1886 /**
1887 * Is this user's e-mail address valid-looking and confirmed within
1888 * limits of the current site configuration?
1889 *
1890 * If $wgEmailAuthentication is on, this may require the user to have
1891 * confirmed their address by returning a code or using a password
1892 * sent to the address from the wiki.
1893 *
1894 * @return bool
1895 */
1896 function isEmailConfirmed() {
1897 global $wgEmailAuthentication;
1898 $this->loadFromDatabase();
1899 $confirmed = true;
1900 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
1901 if( $this->isAnon() )
1902 return false;
1903 if( !$this->isValidEmailAddr( $this->mEmail ) )
1904 return false;
1905 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1906 return false;
1907 return true;
1908 } else {
1909 return $confirmed;
1910 }
1911 }
1912
1913 /**
1914 * @param array $groups list of groups
1915 * @return array list of permission key names for given groups combined
1916 * @static
1917 */
1918 function getGroupPermissions( $groups ) {
1919 global $wgGroupPermissions;
1920 $rights = array();
1921 foreach( $groups as $group ) {
1922 if( isset( $wgGroupPermissions[$group] ) ) {
1923 $rights = array_merge( $rights,
1924 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1925 }
1926 }
1927 return $rights;
1928 }
1929
1930 /**
1931 * @param string $group key name
1932 * @return string localized descriptive name for group, if provided
1933 * @static
1934 */
1935 function getGroupName( $group ) {
1936 $key = "group-$group";
1937 $name = wfMsg( $key );
1938 if( $name == '' || $name == "&lt;$key&gt;" ) {
1939 return $group;
1940 } else {
1941 return $name;
1942 }
1943 }
1944
1945 /**
1946 * @param string $group key name
1947 * @return string localized descriptive name for member of a group, if provided
1948 * @static
1949 */
1950 function getGroupMember( $group ) {
1951 $key = "group-$group-member";
1952 $name = wfMsg( $key );
1953 if( $name == '' || $name == "&lt;$key&gt;" ) {
1954 return $group;
1955 } else {
1956 return $name;
1957 }
1958 }
1959
1960
1961 /**
1962 * Return the set of defined explicit groups.
1963 * The *, 'user', 'autoconfirmed' and 'emailconfirmed'
1964 * groups are not included, as they are defined
1965 * automatically, not in the database.
1966 * @return array
1967 * @static
1968 */
1969 function getAllGroups() {
1970 global $wgGroupPermissions;
1971 return array_diff(
1972 array_keys( $wgGroupPermissions ),
1973 array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) );
1974 }
1975
1976 /**
1977 * Get the title of a page describing a particular group
1978 *
1979 * @param $group Name of the group
1980 * @return mixed
1981 */
1982 function getGroupPage( $group ) {
1983 $page = wfMsgForContent( 'grouppage-' . $group );
1984 if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) {
1985 $title = Title::newFromText( $page );
1986 if( is_object( $title ) )
1987 return $title;
1988 }
1989 return false;
1990 }
1991
1992
1993 }
1994
1995 ?>