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