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