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