* (bug 5338) Reject extra initial colons in title
[mediawiki.git] / includes / User.php
blobbd8b06c4bc25d70413ad8b3ffdacc18a120b891c
1 <?php
2 /**
3 * See user.txt
5 * @package MediaWiki
6 */
8 /**
11 require_once( 'WatchedItem.php' );
13 # Number of characters in user_token field
14 define( 'USER_TOKEN_LENGTH', 32 );
16 # Serialized record version
17 define( 'MW_USER_VERSION', 3 );
19 /**
21 * @package MediaWiki
23 class User {
25 * When adding a new private variable, dont forget to add it to __sleep()
27 /**@{{
28 * @private
30 var $mBlockedby; //!<
31 var $mBlockreason; //!<
32 var $mDataLoaded; //!<
33 var $mEmail; //!<
34 var $mEmailAuthenticated; //!<
35 var $mGroups; //!<
36 var $mHash; //!<
37 var $mId; //!<
38 var $mName; //!<
39 var $mNewpassword; //!<
40 var $mNewtalk; //!<
41 var $mOptions; //!<
42 var $mPassword; //!<
43 var $mRealName; //!<
44 var $mRegistration; //!<
45 var $mRights; //!<
46 var $mSkin; //!<
47 var $mToken; //!<
48 var $mTouched; //!<
49 var $mVersion; //!< serialized version
50 /**@}} */
52 /** Constructor using User:loadDefaults() */
53 function User() {
54 $this->loadDefaults();
55 $this->mVersion = MW_USER_VERSION;
58 /**
59 * Static factory method
60 * @param string $name Username, validated by Title:newFromText()
61 * @return User
62 * @static
64 function newFromName( $name ) {
65 # Force usernames to capital
66 global $wgContLang;
67 $name = $wgContLang->ucfirst( $name );
69 # Clean up name according to title rules
70 $t = Title::newFromText( $name );
71 if( is_null( $t ) ) {
72 return null;
75 # Reject various classes of invalid names
76 $canonicalName = $t->getText();
77 global $wgAuth;
78 $canonicalName = $wgAuth->getCanonicalName( $t->getText() );
80 if( !User::isValidUserName( $canonicalName ) ) {
81 return null;
84 $u = new User();
85 $u->setName( $canonicalName );
86 $u->setId( $u->idFromName( $canonicalName ) );
87 return $u;
90 /**
91 * Factory method to fetch whichever use has a given email confirmation code.
92 * This code is generated when an account is created or its e-mail address
93 * has changed.
95 * If the code is invalid or has expired, returns NULL.
97 * @param string $code
98 * @return User
99 * @static
101 function newFromConfirmationCode( $code ) {
102 $dbr =& wfGetDB( DB_SLAVE );
103 $name = $dbr->selectField( 'user', 'user_name', array(
104 'user_email_token' => md5( $code ),
105 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
106 ) );
107 if( is_string( $name ) ) {
108 return User::newFromName( $name );
109 } else {
110 return null;
115 * Serialze sleep function, for better cache efficiency and avoidance of
116 * silly "incomplete type" errors when skins are cached. The array should
117 * contain names of private variables (see at top of User.php).
119 function __sleep() {
120 return array(
121 'mBlockedby',
122 'mBlockreason',
123 'mDataLoaded',
124 'mEmail',
125 'mEmailAuthenticated',
126 'mGroups',
127 'mHash',
128 'mId',
129 'mName',
130 'mNewpassword',
131 'mNewtalk',
132 'mOptions',
133 'mPassword',
134 'mRealName',
135 'mRegistration',
136 'mRights',
137 'mToken',
138 'mTouched',
139 'mVersion',
144 * Get username given an id.
145 * @param integer $id Database user id
146 * @return string Nickname of a user
147 * @static
149 function whoIs( $id ) {
150 $dbr =& wfGetDB( DB_SLAVE );
151 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
155 * Get real username given an id.
156 * @param integer $id Database user id
157 * @return string Realname of a user
158 * @static
160 function whoIsReal( $id ) {
161 $dbr =& wfGetDB( DB_SLAVE );
162 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' );
166 * Get database id given a user name
167 * @param string $name Nickname of a user
168 * @return integer|null Database user id (null: if non existent
169 * @static
171 function idFromName( $name ) {
172 $fname = "User::idFromName";
174 $nt = Title::newFromText( $name );
175 if( is_null( $nt ) ) {
176 # Illegal name
177 return null;
179 $dbr =& wfGetDB( DB_SLAVE );
180 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
182 if ( $s === false ) {
183 return 0;
184 } else {
185 return $s->user_id;
190 * does the string match an anonymous IPv4 address?
192 * Note: We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP
193 * address because the usemod software would "cloak" anonymous IP
194 * addresses like this, if we allowed accounts like this to be created
195 * new users could get the old edits of these anonymous users.
197 * @bug 3631
199 * @static
200 * @param string $name Nickname of a user
201 * @return bool
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);*/
214 * Is the input a valid username?
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.
221 * @param string $name
222 * @return bool
223 * @static
225 function isValidUserName( $name ) {
226 global $wgContLang, $wgMaxNameChars;
228 if ( $name == ''
229 || User::isIP( $name )
230 || strpos( $name, '/' ) !== false
231 || strlen( $name ) > $wgMaxNameChars
232 || $name != $wgContLang->ucfirst( $name ) )
233 return false;
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;
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;
257 return true;
261 * Is the input a valid password?
263 * @param string $password
264 * @return bool
265 * @static
267 function isValidPassword( $password ) {
268 global $wgMinimalPasswordLength;
269 return strlen( $password ) >= $wgMinimalPasswordLength;
273 * Does the string match roughly an email address ?
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.
279 * @todo Check for RFC 2822 compilance
280 * @bug 959
282 * @param string $addr email address
283 * @static
284 * @return bool
286 function isValidEmailAddr ( $addr ) {
287 return ( trim( $addr ) != '' ) &&
288 (false !== strpos( $addr, '@' ) );
292 * Count the number of edits of a user
294 * @param int $uid The user ID to check
295 * @return int
297 function edits( $uid ) {
298 $fname = 'User::edits';
300 $dbr =& wfGetDB( DB_SLAVE );
301 return $dbr->selectField(
302 'revision', 'count(*)',
303 array( 'rev_user' => $uid ),
304 $fname
309 * probably return a random password
310 * @return string probably a random password
311 * @static
312 * @todo Check what is doing really [AV]
314 function randomPassword() {
315 global $wgMinimalPasswordLength;
316 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
317 $l = strlen( $pwchars ) - 1;
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)};
325 return $np;
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.
333 function loadDefaults() {
334 static $n=0;
335 $n++;
336 $fname = 'User::loadDefaults' . $n;
337 wfProfileIn( $fname );
339 global $wgCookiePrefix;
340 global $wgNamespacesToBeSearchedDefault;
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();
352 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
353 $this->mOptions['searchNs'.$nsnum] = $val;
355 unset( $this->mSkin );
356 $this->mDataLoaded = false;
357 $this->mBlockedby = -1; # Unset
358 $this->setToken(); # Random
359 $this->mHash = false;
361 if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
362 $this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
364 else {
365 $this->mTouched = '0'; # Allow any pages to be cached
368 $this->mRegistration = wfTimestamp( TS_MW );
370 wfProfileOut( $fname );
374 * Combine the language default options with any site-specific options
375 * and add the default language variants.
377 * @return array
378 * @static
379 * @private
381 function getDefaultOptions() {
383 * Site defaults will override the global/language defaults
385 global $wgContLang, $wgDefaultUserOptions;
386 $defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptions();
389 * default language setting
391 $variant = $wgContLang->getPreferredVariant();
392 $defOpt['variant'] = $variant;
393 $defOpt['language'] = $variant;
395 return $defOpt;
399 * Get a given default option value.
401 * @param string $opt
402 * @return string
403 * @static
404 * @public
406 function getDefaultOption( $opt ) {
407 $defOpts = User::getDefaultOptions();
408 if( isset( $defOpts[$opt] ) ) {
409 return $defOpts[$opt];
410 } else {
411 return '';
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.
422 function getBlockedStatus( $bFromSlave = true ) {
423 global $wgEnableSorbs, $wgProxyWhitelist;
425 if ( -1 != $this->mBlockedby ) {
426 wfDebug( "User::getBlockedStatus: already loaded.\n" );
427 return;
430 $fname = 'User::getBlockedStatus';
431 wfProfileIn( $fname );
432 wfDebug( "$fname: checking...\n" );
434 $this->mBlockedby = 0;
435 $ip = wfGetIP();
437 # User/IP blocking
438 $block = new Block();
439 $block->fromMaster( !$bFromSlave );
440 if ( $block->load( $ip , $this->mId ) ) {
441 wfDebug( "$fname: Found block.\n" );
442 $this->mBlockedby = $block->mBy;
443 $this->mBlockreason = $block->mReason;
444 if ( $this->isLoggedIn() ) {
445 $this->spreadBlock();
447 } else {
448 wfDebug( "$fname: No block.\n" );
451 # Proxy blocking
452 if ( !$this->isSysop() && !in_array( $ip, $wgProxyWhitelist ) ) {
454 # Local list
455 if ( wfIsLocallyBlockedProxy( $ip ) ) {
456 $this->mBlockedby = wfMsg( 'proxyblocker' );
457 $this->mBlockreason = wfMsg( 'proxyblockreason' );
460 # DNSBL
461 if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
462 if ( $this->inSorbsBlacklist( $ip ) ) {
463 $this->mBlockedby = wfMsg( 'sorbs' );
464 $this->mBlockreason = wfMsg( 'sorbsreason' );
469 # Extensions
470 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
472 wfProfileOut( $fname );
475 function inSorbsBlacklist( $ip ) {
476 global $wgEnableSorbs;
477 return $wgEnableSorbs &&
478 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
481 function inOpmBlacklist( $ip ) {
482 global $wgEnableOpm;
483 return $wgEnableOpm &&
484 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
487 function inDnsBlacklist( $ip, $base ) {
488 $fname = 'User::inDnsBlacklist';
489 wfProfileIn( $fname );
491 $found = false;
492 $host = '';
494 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
495 # Make hostname
496 for ( $i=4; $i>=1; $i-- ) {
497 $host .= $m[$i] . '.';
499 $host .= $base;
501 # Send query
502 $ipList = gethostbynamel( $host );
504 if ( $ipList ) {
505 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
506 $found = true;
507 } else {
508 wfDebug( "Requested $host, not found in $base.\n" );
512 wfProfileOut( $fname );
513 return $found;
517 * Primitive rate limits: enforce maximum actions per time period
518 * to put a brake on flooding.
520 * Note: when using a shared cache like memcached, IP-address
521 * last-hit counters will be shared across wikis.
523 * @return bool true if a rate limiter was tripped
524 * @public
526 function pingLimiter( $action='edit' ) {
527 global $wgRateLimits;
528 if( !isset( $wgRateLimits[$action] ) ) {
529 return false;
531 if( $this->isAllowed( 'delete' ) ) {
532 // goddam cabal
533 return false;
536 global $wgMemc, $wgDBname, $wgRateLimitLog;
537 $fname = 'User::pingLimiter';
538 wfProfileIn( $fname );
540 $limits = $wgRateLimits[$action];
541 $keys = array();
542 $id = $this->getId();
543 $ip = wfGetIP();
545 if( isset( $limits['anon'] ) && $id == 0 ) {
546 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
549 if( isset( $limits['user'] ) && $id != 0 ) {
550 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
552 if( $this->isNewbie() ) {
553 if( isset( $limits['newbie'] ) && $id != 0 ) {
554 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
556 if( isset( $limits['ip'] ) ) {
557 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
559 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
560 $subnet = $matches[1];
561 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
565 $triggered = false;
566 foreach( $keys as $key => $limit ) {
567 list( $max, $period ) = $limit;
568 $summary = "(limit $max in {$period}s)";
569 $count = $wgMemc->get( $key );
570 if( $count ) {
571 if( $count > $max ) {
572 wfDebug( "$fname: tripped! $key at $count $summary\n" );
573 if( $wgRateLimitLog ) {
574 @error_log( wfTimestamp( TS_MW ) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
576 $triggered = true;
577 } else {
578 wfDebug( "$fname: ok. $key at $count $summary\n" );
580 } else {
581 wfDebug( "$fname: adding record for $key $summary\n" );
582 $wgMemc->add( $key, 1, intval( $period ) );
584 $wgMemc->incr( $key );
587 wfProfileOut( $fname );
588 return $triggered;
592 * Check if user is blocked
593 * @return bool True if blocked, false otherwise
595 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
596 wfDebug( "User::isBlocked: enter\n" );
597 $this->getBlockedStatus( $bFromSlave );
598 return $this->mBlockedby !== 0;
602 * Check if user is blocked from editing a particular article
604 function isBlockedFrom( $title, $bFromSlave = false ) {
605 global $wgBlockAllowsUTEdit;
606 $fname = 'User::isBlockedFrom';
607 wfProfileIn( $fname );
608 wfDebug( "$fname: enter\n" );
610 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
611 $title->getNamespace() == NS_USER_TALK )
613 $blocked = false;
614 wfDebug( "$fname: self-talk page, ignoring any blocks\n" );
615 } else {
616 wfDebug( "$fname: asking isBlocked()\n" );
617 $blocked = $this->isBlocked( $bFromSlave );
619 wfProfileOut( $fname );
620 return $blocked;
624 * Get name of blocker
625 * @return string name of blocker
627 function blockedBy() {
628 $this->getBlockedStatus();
629 return $this->mBlockedby;
633 * Get blocking reason
634 * @return string Blocking reason
636 function blockedFor() {
637 $this->getBlockedStatus();
638 return $this->mBlockreason;
642 * Initialise php session
644 function SetupSession() {
645 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
646 if( $wgSessionsInMemcached ) {
647 require_once( 'MemcachedSessions.php' );
648 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
649 # If it's left on 'user' or another setting from another
650 # application, it will end up failing. Try to recover.
651 ini_set ( 'session.save_handler', 'files' );
653 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
654 session_cache_limiter( 'private, must-revalidate' );
655 @session_start();
659 * Create a new user object using data from session
660 * @static
662 function loadFromSession() {
663 global $wgMemc, $wgDBname, $wgCookiePrefix;
665 if ( isset( $_SESSION['wsUserID'] ) ) {
666 if ( 0 != $_SESSION['wsUserID'] ) {
667 $sId = $_SESSION['wsUserID'];
668 } else {
669 return new User();
671 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
672 $sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
673 $_SESSION['wsUserID'] = $sId;
674 } else {
675 return new User();
677 if ( isset( $_SESSION['wsUserName'] ) ) {
678 $sName = $_SESSION['wsUserName'];
679 } else if ( isset( $_COOKIE["{$wgCookiePrefix}UserName"] ) ) {
680 $sName = $_COOKIE["{$wgCookiePrefix}UserName"];
681 $_SESSION['wsUserName'] = $sName;
682 } else {
683 return new User();
686 $passwordCorrect = FALSE;
687 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
688 if( !is_object( $user ) || $user->mVersion < MW_USER_VERSION ) {
689 # Expire old serialized objects; they may be corrupt.
690 $user = false;
692 if($makenew = !$user) {
693 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
694 $user = new User();
695 $user->mId = $sId;
696 $user->loadFromDatabase();
697 } else {
698 wfDebug( "User::loadFromSession() got from cache!\n" );
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
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" );
716 return $user;
718 return new User(); # Can't log in from session
722 * Load a user from the database
724 function loadFromDatabase() {
725 $fname = "User::loadFromDatabase";
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;
734 # Paranoia
735 $this->mId = intval( $this->mId );
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
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 );
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 );
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;
771 $implicitGroups = array( '*', 'user' );
773 global $wgAutoConfirmAge;
774 $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration );
775 if( $accountAge >= $wgAutoConfirmAge ) {
776 $implicitGroups[] = 'autoconfirmed';
779 $effectiveGroups = array_merge( $implicitGroups, $this->mGroups );
780 $this->mRights = $this->getGroupPermissions( $effectiveGroups );
783 $this->mDataLoaded = true;
786 function getID() { return $this->mId; }
787 function setID( $v ) {
788 $this->mId = $v;
789 $this->mDataLoaded = false;
792 function getName() {
793 $this->loadFromDatabase();
794 if ( $this->mName === false ) {
795 $this->mName = wfGetIP();
797 return $this->mName;
800 function setName( $str ) {
801 $this->loadFromDatabase();
802 $this->mName = $str;
807 * Return the title dbkey form of the name, for eg user pages.
808 * @return string
809 * @public
811 function getTitleKey() {
812 return str_replace( ' ', '_', $this->getName() );
815 function getNewtalk() {
816 $this->loadFromDatabase();
818 # Load the newtalk status if it is unloaded (mNewtalk=-1)
819 if( $this->mNewtalk === -1 ) {
820 $this->mNewtalk = false; # reset talk page status
822 # Check memcached separately for anons, who have no
823 # entire User object stored in there.
824 if( !$this->mId ) {
825 global $wgDBname, $wgMemc;
826 $key = "$wgDBname:newtalk:ip:" . $this->getName();
827 $newtalk = $wgMemc->get( $key );
828 if( is_integer( $newtalk ) ) {
829 $this->mNewtalk = (bool)$newtalk;
830 } else {
831 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName() );
832 $wgMemc->set( $key, $this->mNewtalk, time() ); // + 1800 );
834 } else {
835 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
839 return (bool)$this->mNewtalk;
843 * Return the talk page(s) this user has new messages on.
845 function getNewMessageLinks() {
846 global $wgDBname;
847 $talks = array();
848 if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
849 return $talks;
851 if (!$this->getNewtalk())
852 return array();
853 $up = $this->getUserPage();
854 $utp = $up->getTalkPage();
855 return array(array("wiki" => $wgDBname, "link" => $utp->getLocalURL()));
860 * Perform a user_newtalk check on current slaves; if the memcached data
861 * is funky we don't want newtalk state to get stuck on save, as that's
862 * damn annoying.
864 * @param string $field
865 * @param mixed $id
866 * @return bool
867 * @private
869 function checkNewtalk( $field, $id ) {
870 $fname = 'User::checkNewtalk';
871 $dbr =& wfGetDB( DB_SLAVE );
872 $ok = $dbr->selectField( 'user_newtalk', $field,
873 array( $field => $id ), $fname );
874 return $ok !== false;
878 * Add or update the
879 * @param string $field
880 * @param mixed $id
881 * @private
883 function updateNewtalk( $field, $id ) {
884 $fname = 'User::updateNewtalk';
885 if( $this->checkNewtalk( $field, $id ) ) {
886 wfDebug( "$fname already set ($field, $id), ignoring\n" );
887 return false;
889 $dbw =& wfGetDB( DB_MASTER );
890 $dbw->insert( 'user_newtalk',
891 array( $field => $id ),
892 $fname,
893 'IGNORE' );
894 wfDebug( "$fname: set on ($field, $id)\n" );
895 return true;
899 * Clear the new messages flag for the given user
900 * @param string $field
901 * @param mixed $id
902 * @private
904 function deleteNewtalk( $field, $id ) {
905 $fname = 'User::deleteNewtalk';
906 if( !$this->checkNewtalk( $field, $id ) ) {
907 wfDebug( "$fname: already gone ($field, $id), ignoring\n" );
908 return false;
910 $dbw =& wfGetDB( DB_MASTER );
911 $dbw->delete( 'user_newtalk',
912 array( $field => $id ),
913 $fname );
914 wfDebug( "$fname: killed on ($field, $id)\n" );
915 return true;
919 * Update the 'You have new messages!' status.
920 * @param bool $val
922 function setNewtalk( $val ) {
923 if( wfReadOnly() ) {
924 return;
927 $this->loadFromDatabase();
928 $this->mNewtalk = $val;
930 $fname = 'User::setNewtalk';
932 if( $this->isAnon() ) {
933 $field = 'user_ip';
934 $id = $this->getName();
935 } else {
936 $field = 'user_id';
937 $id = $this->getId();
940 if( $val ) {
941 $changed = $this->updateNewtalk( $field, $id );
942 } else {
943 $changed = $this->deleteNewtalk( $field, $id );
946 if( $changed ) {
947 if( $this->isAnon() ) {
948 // Anons have a separate memcached space, since
949 // user records aren't kept for them.
950 global $wgDBname, $wgMemc;
951 $key = "$wgDBname:newtalk:ip:$val";
952 $wgMemc->set( $key, $val ? 1 : 0 );
953 } else {
954 if( $val ) {
955 // Make sure the user page is watched, so a notification
956 // will be sent out if enabled.
957 $this->addWatch( $this->getTalkPage() );
960 $this->invalidateCache();
961 $this->saveSettings();
965 function invalidateCache() {
966 global $wgClockSkewFudge;
967 $this->loadFromDatabase();
968 $this->mTouched = wfTimestamp(TS_MW, time() + $wgClockSkewFudge );
969 # Don't forget to save the options after this or
970 # it won't take effect!
973 function validateCache( $timestamp ) {
974 $this->loadFromDatabase();
975 return ($timestamp >= $this->mTouched);
979 * Encrypt a password.
980 * It can eventuall salt a password @see User::addSalt()
981 * @param string $p clear Password.
982 * @return string Encrypted password.
984 function encryptPassword( $p ) {
985 return wfEncryptPassword( $this->mId, $p );
988 # Set the password and reset the random token
989 function setPassword( $str ) {
990 $this->loadFromDatabase();
991 $this->setToken();
992 $this->mPassword = $this->encryptPassword( $str );
993 $this->mNewpassword = '';
996 # Set the random token (used for persistent authentication)
997 function setToken( $token = false ) {
998 global $wgSecretKey, $wgProxyKey, $wgDBname;
999 if ( !$token ) {
1000 if ( $wgSecretKey ) {
1001 $key = $wgSecretKey;
1002 } elseif ( $wgProxyKey ) {
1003 $key = $wgProxyKey;
1004 } else {
1005 $key = microtime();
1007 $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId );
1008 } else {
1009 $this->mToken = $token;
1014 function setCookiePassword( $str ) {
1015 $this->loadFromDatabase();
1016 $this->mCookiePassword = md5( $str );
1019 function setNewpassword( $str ) {
1020 $this->loadFromDatabase();
1021 $this->mNewpassword = $this->encryptPassword( $str );
1024 function getEmail() {
1025 $this->loadFromDatabase();
1026 return $this->mEmail;
1029 function getEmailAuthenticationTimestamp() {
1030 $this->loadFromDatabase();
1031 return $this->mEmailAuthenticated;
1034 function setEmail( $str ) {
1035 $this->loadFromDatabase();
1036 $this->mEmail = $str;
1039 function getRealName() {
1040 $this->loadFromDatabase();
1041 return $this->mRealName;
1044 function setRealName( $str ) {
1045 $this->loadFromDatabase();
1046 $this->mRealName = $str;
1050 * @param string $oname The option to check
1051 * @return string
1053 function getOption( $oname ) {
1054 $this->loadFromDatabase();
1055 if ( array_key_exists( $oname, $this->mOptions ) ) {
1056 return trim( $this->mOptions[$oname] );
1057 } else {
1058 return '';
1063 * @param string $oname The option to check
1064 * @return bool False if the option is not selected, true if it is
1066 function getBoolOption( $oname ) {
1067 return (bool)$this->getOption( $oname );
1070 function setOption( $oname, $val ) {
1071 $this->loadFromDatabase();
1072 if ( $oname == 'skin' ) {
1073 # Clear cached skin, so the new one displays immediately in Special:Preferences
1074 unset( $this->mSkin );
1076 $this->mOptions[$oname] = $val;
1077 $this->invalidateCache();
1080 function getRights() {
1081 $this->loadFromDatabase();
1082 return $this->mRights;
1086 * Get the list of explicit group memberships this user has.
1087 * The implicit * and user groups are not included.
1088 * @return array of strings
1090 function getGroups() {
1091 $this->loadFromDatabase();
1092 return $this->mGroups;
1096 * Get the list of implicit group memberships this user has.
1097 * This includes all explicit groups, plus 'user' if logged in
1098 * and '*' for all accounts.
1099 * @return array of strings
1101 function getEffectiveGroups() {
1102 $base = array( '*' );
1103 if( $this->isLoggedIn() ) {
1104 $base[] = 'user';
1106 return array_merge( $base, $this->getGroups() );
1110 * Add the user to the given group.
1111 * This takes immediate effect.
1112 * @string $group
1114 function addGroup( $group ) {
1115 $dbw =& wfGetDB( DB_MASTER );
1116 $dbw->insert( 'user_groups',
1117 array(
1118 'ug_user' => $this->getID(),
1119 'ug_group' => $group,
1121 'User::addGroup',
1122 array( 'IGNORE' ) );
1124 $this->mGroups = array_merge( $this->mGroups, array( $group ) );
1125 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1127 $this->invalidateCache();
1128 $this->saveSettings();
1132 * Remove the user from the given group.
1133 * This takes immediate effect.
1134 * @string $group
1136 function removeGroup( $group ) {
1137 $dbw =& wfGetDB( DB_MASTER );
1138 $dbw->delete( 'user_groups',
1139 array(
1140 'ug_user' => $this->getID(),
1141 'ug_group' => $group,
1143 'User::removeGroup' );
1145 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
1146 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups() );
1148 $this->invalidateCache();
1149 $this->saveSettings();
1154 * A more legible check for non-anonymousness.
1155 * Returns true if the user is not an anonymous visitor.
1157 * @return bool
1159 function isLoggedIn() {
1160 return( $this->getID() != 0 );
1164 * A more legible check for anonymousness.
1165 * Returns true if the user is an anonymous visitor.
1167 * @return bool
1169 function isAnon() {
1170 return !$this->isLoggedIn();
1174 * Check if a user is sysop
1175 * @deprecated
1177 function isSysop() {
1178 return $this->isAllowed( 'protect' );
1181 /** @deprecated */
1182 function isDeveloper() {
1183 return $this->isAllowed( 'siteadmin' );
1186 /** @deprecated */
1187 function isBureaucrat() {
1188 return $this->isAllowed( 'makesysop' );
1192 * Whether the user is a bot
1193 * @todo need to be migrated to the new user level management sytem
1195 function isBot() {
1196 $this->loadFromDatabase();
1197 return in_array( 'bot', $this->mRights );
1201 * Check if user is allowed to access a feature / make an action
1202 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1203 * @return boolean True: action is allowed, False: action should not be allowed
1205 function isAllowed($action='') {
1206 if ( $action === '' )
1207 // In the spirit of DWIM
1208 return true;
1210 $this->loadFromDatabase();
1211 return in_array( $action , $this->mRights );
1215 * Load a skin if it doesn't exist or return it
1216 * @todo FIXME : need to check the old failback system [AV]
1218 function &getSkin() {
1219 global $IP, $wgRequest;
1220 if ( ! isset( $this->mSkin ) ) {
1221 $fname = 'User::getSkin';
1222 wfProfileIn( $fname );
1224 # get the user skin
1225 $userSkin = $this->getOption( 'skin' );
1226 $userSkin = $wgRequest->getVal('useskin', $userSkin);
1228 $this->mSkin =& Skin::newFromKey( $userSkin );
1229 wfProfileOut( $fname );
1231 return $this->mSkin;
1234 /**#@+
1235 * @param string $title Article title to look at
1239 * Check watched status of an article
1240 * @return bool True if article is watched
1242 function isWatched( $title ) {
1243 $wl = WatchedItem::fromUserTitle( $this, $title );
1244 return $wl->isWatched();
1248 * Watch an article
1250 function addWatch( $title ) {
1251 $wl = WatchedItem::fromUserTitle( $this, $title );
1252 $wl->addWatch();
1253 $this->invalidateCache();
1257 * Stop watching an article
1259 function removeWatch( $title ) {
1260 $wl = WatchedItem::fromUserTitle( $this, $title );
1261 $wl->removeWatch();
1262 $this->invalidateCache();
1266 * Clear the user's notification timestamp for the given title.
1267 * If e-notif e-mails are on, they will receive notification mails on
1268 * the next change of the page if it's watched etc.
1270 function clearNotification( &$title ) {
1271 global $wgUser, $wgUseEnotif;
1274 if ($title->getNamespace() == NS_USER_TALK &&
1275 $title->getText() == $this->getName() ) {
1276 if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
1277 return;
1278 $this->setNewtalk( false );
1281 if( !$wgUseEnotif ) {
1282 return;
1285 if( $this->isAnon() ) {
1286 // Nothing else to do...
1287 return;
1290 // Only update the timestamp if the page is being watched.
1291 // The query to find out if it is watched is cached both in memcached and per-invocation,
1292 // and when it does have to be executed, it can be on a slave
1293 // If this is the user's newtalk page, we always update the timestamp
1294 if ($title->getNamespace() == NS_USER_TALK &&
1295 $title->getText() == $wgUser->getName())
1297 $watched = true;
1298 } elseif ( $this->getID() == $wgUser->getID() ) {
1299 $watched = $title->userIsWatching();
1300 } else {
1301 $watched = true;
1304 // If the page is watched by the user (or may be watched), update the timestamp on any
1305 // any matching rows
1306 if ( $watched ) {
1307 $dbw =& wfGetDB( DB_MASTER );
1308 $success = $dbw->update( 'watchlist',
1309 array( /* SET */
1310 'wl_notificationtimestamp' => NULL
1311 ), array( /* WHERE */
1312 'wl_title' => $title->getDBkey(),
1313 'wl_namespace' => $title->getNamespace(),
1314 'wl_user' => $this->getID()
1315 ), 'User::clearLastVisited'
1320 /**#@-*/
1323 * Resets all of the given user's page-change notification timestamps.
1324 * If e-notif e-mails are on, they will receive notification mails on
1325 * the next change of any watched page.
1327 * @param int $currentUser user ID number
1328 * @public
1330 function clearAllNotifications( $currentUser ) {
1331 global $wgUseEnotif;
1332 if ( !$wgUseEnotif ) {
1333 $this->setNewtalk( false );
1334 return;
1336 if( $currentUser != 0 ) {
1338 $dbw =& wfGetDB( DB_MASTER );
1339 $success = $dbw->update( 'watchlist',
1340 array( /* SET */
1341 'wl_notificationtimestamp' => 0
1342 ), array( /* WHERE */
1343 'wl_user' => $currentUser
1344 ), 'UserMailer::clearAll'
1347 # we also need to clear here the "you have new message" notification for the own user_talk page
1348 # This is cleared one page view later in Article::viewUpdates();
1353 * @private
1354 * @return string Encoding options
1356 function encodeOptions() {
1357 $a = array();
1358 foreach ( $this->mOptions as $oname => $oval ) {
1359 array_push( $a, $oname.'='.$oval );
1361 $s = implode( "\n", $a );
1362 return $s;
1366 * @private
1368 function decodeOptions( $str ) {
1369 $a = explode( "\n", $str );
1370 foreach ( $a as $s ) {
1371 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1372 $this->mOptions[$m[1]] = $m[2];
1377 function setCookies() {
1378 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1379 if ( 0 == $this->mId ) return;
1380 $this->loadFromDatabase();
1381 $exp = time() + $wgCookieExpiration;
1383 $_SESSION['wsUserID'] = $this->mId;
1384 setcookie( $wgCookiePrefix.'UserID', $this->mId, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1386 $_SESSION['wsUserName'] = $this->getName();
1387 setcookie( $wgCookiePrefix.'UserName', $this->getName(), $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1389 $_SESSION['wsToken'] = $this->mToken;
1390 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1391 setcookie( $wgCookiePrefix.'Token', $this->mToken, $exp, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1392 } else {
1393 setcookie( $wgCookiePrefix.'Token', '', time() - 3600 );
1398 * Logout user
1399 * It will clean the session cookie
1401 function logout() {
1402 global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookiePrefix;
1403 $this->loadDefaults();
1404 $this->setLoaded( true );
1406 $_SESSION['wsUserID'] = 0;
1408 setcookie( $wgCookiePrefix.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1409 setcookie( $wgCookiePrefix.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1411 # Remember when user logged out, to prevent seeing cached pages
1412 setcookie( $wgCookiePrefix.'LoggedOut', wfTimestampNow(), time() + 86400, $wgCookiePath, $wgCookieDomain, $wgCookieSecure );
1416 * Save object settings into database
1418 function saveSettings() {
1419 global $wgMemc, $wgDBname;
1420 $fname = 'User::saveSettings';
1422 if ( wfReadOnly() ) { return; }
1423 if ( 0 == $this->mId ) { return; }
1425 $dbw =& wfGetDB( DB_MASTER );
1426 $dbw->update( 'user',
1427 array( /* SET */
1428 'user_name' => $this->mName,
1429 'user_password' => $this->mPassword,
1430 'user_newpassword' => $this->mNewpassword,
1431 'user_real_name' => $this->mRealName,
1432 'user_email' => $this->mEmail,
1433 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1434 'user_options' => $this->encodeOptions(),
1435 'user_touched' => $dbw->timestamp($this->mTouched),
1436 'user_token' => $this->mToken
1437 ), array( /* WHERE */
1438 'user_id' => $this->mId
1439 ), $fname
1441 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1446 * Checks if a user with the given name exists, returns the ID
1448 function idForName() {
1449 $fname = 'User::idForName';
1451 $gotid = 0;
1452 $s = trim( $this->getName() );
1453 if ( 0 == strcmp( '', $s ) ) return 0;
1455 $dbr =& wfGetDB( DB_SLAVE );
1456 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1457 if ( $id === false ) {
1458 $id = 0;
1460 return $id;
1464 * Add user object to the database
1466 function addToDatabase() {
1467 $fname = 'User::addToDatabase';
1468 $dbw =& wfGetDB( DB_MASTER );
1469 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1470 $dbw->insert( 'user',
1471 array(
1472 'user_id' => $seqVal,
1473 'user_name' => $this->mName,
1474 'user_password' => $this->mPassword,
1475 'user_newpassword' => $this->mNewpassword,
1476 'user_email' => $this->mEmail,
1477 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
1478 'user_real_name' => $this->mRealName,
1479 'user_options' => $this->encodeOptions(),
1480 'user_token' => $this->mToken,
1481 'user_registration' => $dbw->timestamp( $this->mRegistration ),
1482 ), $fname
1484 $this->mId = $dbw->insertId();
1487 function spreadBlock() {
1488 # If the (non-anonymous) user is blocked, this function will block any IP address
1489 # that they successfully log on from.
1490 $fname = 'User::spreadBlock';
1492 wfDebug( "User:spreadBlock()\n" );
1493 if ( $this->mId == 0 ) {
1494 return;
1497 $userblock = Block::newFromDB( '', $this->mId );
1498 if ( !$userblock->isValid() ) {
1499 return;
1502 # Check if this IP address is already blocked
1503 $ipblock = Block::newFromDB( wfGetIP() );
1504 if ( $ipblock->isValid() ) {
1505 # If the user is already blocked. Then check if the autoblock would
1506 # excede the user block. If it would excede, then do nothing, else
1507 # prolong block time
1508 if ($userblock->mExpiry &&
1509 ($userblock->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
1510 return;
1512 # Just update the timestamp
1513 $ipblock->updateTimestamp();
1514 return;
1517 # Make a new block object with the desired properties
1518 wfDebug( "Autoblocking {$this->mName}@" . wfGetIP() . "\n" );
1519 $ipblock->mAddress = wfGetIP();
1520 $ipblock->mUser = 0;
1521 $ipblock->mBy = $userblock->mBy;
1522 $ipblock->mReason = wfMsg( 'autoblocker', $this->getName(), $userblock->mReason );
1523 $ipblock->mTimestamp = wfTimestampNow();
1524 $ipblock->mAuto = 1;
1525 # If the user is already blocked with an expiry date, we don't
1526 # want to pile on top of that!
1527 if($userblock->mExpiry) {
1528 $ipblock->mExpiry = min ( $userblock->mExpiry, Block::getAutoblockExpiry( $ipblock->mTimestamp ));
1529 } else {
1530 $ipblock->mExpiry = Block::getAutoblockExpiry( $ipblock->mTimestamp );
1533 # Insert it
1534 $ipblock->insert();
1539 * Generate a string which will be different for any combination of
1540 * user options which would produce different parser output.
1541 * This will be used as part of the hash key for the parser cache,
1542 * so users will the same options can share the same cached data
1543 * safely.
1545 * Extensions which require it should install 'PageRenderingHash' hook,
1546 * which will give them a chance to modify this key based on their own
1547 * settings.
1549 * @return string
1551 function getPageRenderingHash() {
1552 global $wgContLang;
1553 if( $this->mHash ){
1554 return $this->mHash;
1557 // stubthreshold is only included below for completeness,
1558 // it will always be 0 when this function is called by parsercache.
1560 $confstr = $this->getOption( 'math' );
1561 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1562 $confstr .= '!' . $this->getOption( 'date' );
1563 $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
1564 $confstr .= '!' . $this->getOption( 'language' );
1565 $confstr .= '!' . $this->getOption( 'thumbsize' );
1566 // add in language specific options, if any
1567 $extra = $wgContLang->getExtraHashOptions();
1568 $confstr .= $extra;
1570 // Give a chance for extensions to modify the hash, if they have
1571 // extra options or other effects on the parser cache.
1572 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
1574 $this->mHash = $confstr;
1575 return $confstr;
1578 function isAllowedToCreateAccount() {
1579 return $this->isAllowed( 'createaccount' ) && !$this->isBlocked();
1583 * Set mDataLoaded, return previous value
1584 * Use this to prevent DB access in command-line scripts or similar situations
1586 function setLoaded( $loaded ) {
1587 return wfSetVar( $this->mDataLoaded, $loaded );
1591 * Get this user's personal page title.
1593 * @return Title
1594 * @public
1596 function getUserPage() {
1597 return Title::makeTitle( NS_USER, $this->getName() );
1601 * Get this user's talk page title.
1603 * @return Title
1604 * @public
1606 function getTalkPage() {
1607 $title = $this->getUserPage();
1608 return $title->getTalkPage();
1612 * @static
1614 function getMaxID() {
1615 static $res; // cache
1617 if ( isset( $res ) )
1618 return $res;
1619 else {
1620 $dbr =& wfGetDB( DB_SLAVE );
1621 return $res = $dbr->selectField( 'user', 'max(user_id)', false, 'User::getMaxID' );
1626 * Determine whether the user is a newbie. Newbies are either
1627 * anonymous IPs, or the most recently created accounts.
1628 * @return bool True if it is a newbie.
1630 function isNewbie() {
1631 return !$this->isAllowed( 'autoconfirmed' );
1635 * Check to see if the given clear-text password is one of the accepted passwords
1636 * @param string $password User password.
1637 * @return bool True if the given password is correct otherwise False.
1639 function checkPassword( $password ) {
1640 global $wgAuth, $wgMinimalPasswordLength;
1641 $this->loadFromDatabase();
1643 // Even though we stop people from creating passwords that
1644 // are shorter than this, doesn't mean people wont be able
1645 // to. Certain authentication plugins do NOT want to save
1646 // domain passwords in a mysql database, so we should
1647 // check this (incase $wgAuth->strict() is false).
1648 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1649 return false;
1652 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1653 return true;
1654 } elseif( $wgAuth->strict() ) {
1655 /* Auth plugin doesn't allow local authentication */
1656 return false;
1658 $ep = $this->encryptPassword( $password );
1659 if ( 0 == strcmp( $ep, $this->mPassword ) ) {
1660 return true;
1661 } elseif ( ($this->mNewpassword != '') && (0 == strcmp( $ep, $this->mNewpassword )) ) {
1662 return true;
1663 } elseif ( function_exists( 'iconv' ) ) {
1664 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1665 # Check for this with iconv
1666 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1667 if ( 0 == strcmp( $cp1252hash, $this->mPassword ) ) {
1668 return true;
1671 return false;
1675 * Initialize (if necessary) and return a session token value
1676 * which can be used in edit forms to show that the user's
1677 * login credentials aren't being hijacked with a foreign form
1678 * submission.
1680 * @param mixed $salt - Optional function-specific data for hash.
1681 * Use a string or an array of strings.
1682 * @return string
1683 * @public
1685 function editToken( $salt = '' ) {
1686 if( !isset( $_SESSION['wsEditToken'] ) ) {
1687 $token = $this->generateToken();
1688 $_SESSION['wsEditToken'] = $token;
1689 } else {
1690 $token = $_SESSION['wsEditToken'];
1692 if( is_array( $salt ) ) {
1693 $salt = implode( '|', $salt );
1695 return md5( $token . $salt );
1699 * Generate a hex-y looking random token for various uses.
1700 * Could be made more cryptographically sure if someone cares.
1701 * @return string
1703 function generateToken( $salt = '' ) {
1704 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1705 return md5( $token . $salt );
1709 * Check given value against the token value stored in the session.
1710 * A match should confirm that the form was submitted from the
1711 * user's own login session, not a form submission from a third-party
1712 * site.
1714 * @param string $val - the input value to compare
1715 * @param string $salt - Optional function-specific data for hash
1716 * @return bool
1717 * @public
1719 function matchEditToken( $val, $salt = '' ) {
1720 global $wgMemc;
1721 $sessionToken = $this->editToken( $salt );
1722 if ( $val != $sessionToken ) {
1723 wfDebug( "User::matchEditToken: broken session data\n" );
1725 return $val == $sessionToken;
1729 * Generate a new e-mail confirmation token and send a confirmation
1730 * mail to the user's given address.
1732 * @return mixed True on success, a WikiError object on failure.
1734 function sendConfirmationMail() {
1735 global $wgContLang;
1736 $url = $this->confirmationTokenUrl( $expiration );
1737 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1738 wfMsg( 'confirmemail_body',
1739 wfGetIP(),
1740 $this->getName(),
1741 $url,
1742 $wgContLang->timeanddate( $expiration, false ) ) );
1746 * Send an e-mail to this user's account. Does not check for
1747 * confirmed status or validity.
1749 * @param string $subject
1750 * @param string $body
1751 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1752 * @return mixed True on success, a WikiError object on failure.
1754 function sendMail( $subject, $body, $from = null ) {
1755 if( is_null( $from ) ) {
1756 global $wgPasswordSender;
1757 $from = $wgPasswordSender;
1760 require_once( 'UserMailer.php' );
1761 $to = new MailAddress( $this );
1762 $sender = new MailAddress( $from );
1763 $error = userMailer( $to, $sender, $subject, $body );
1765 if( $error == '' ) {
1766 return true;
1767 } else {
1768 return new WikiError( $error );
1773 * Generate, store, and return a new e-mail confirmation code.
1774 * A hash (unsalted since it's used as a key) is stored.
1775 * @param &$expiration mixed output: accepts the expiration time
1776 * @return string
1777 * @private
1779 function confirmationToken( &$expiration ) {
1780 $fname = 'User::confirmationToken';
1782 $now = time();
1783 $expires = $now + 7 * 24 * 60 * 60;
1784 $expiration = wfTimestamp( TS_MW, $expires );
1786 $token = $this->generateToken( $this->mId . $this->mEmail . $expires );
1787 $hash = md5( $token );
1789 $dbw =& wfGetDB( DB_MASTER );
1790 $dbw->update( 'user',
1791 array( 'user_email_token' => $hash,
1792 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1793 array( 'user_id' => $this->mId ),
1794 $fname );
1796 return $token;
1800 * Generate and store a new e-mail confirmation token, and return
1801 * the URL the user can use to confirm.
1802 * @param &$expiration mixed output: accepts the expiration time
1803 * @return string
1804 * @private
1806 function confirmationTokenUrl( &$expiration ) {
1807 $token = $this->confirmationToken( $expiration );
1808 $title = Title::makeTitle( NS_SPECIAL, 'Confirmemail/' . $token );
1809 return $title->getFullUrl();
1813 * Mark the e-mail address confirmed and save.
1815 function confirmEmail() {
1816 $this->loadFromDatabase();
1817 $this->mEmailAuthenticated = wfTimestampNow();
1818 $this->saveSettings();
1819 return true;
1823 * Is this user allowed to send e-mails within limits of current
1824 * site configuration?
1825 * @return bool
1827 function canSendEmail() {
1828 return $this->isEmailConfirmed();
1832 * Is this user allowed to receive e-mails within limits of current
1833 * site configuration?
1834 * @return bool
1836 function canReceiveEmail() {
1837 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1841 * Is this user's e-mail address valid-looking and confirmed within
1842 * limits of the current site configuration?
1844 * If $wgEmailAuthentication is on, this may require the user to have
1845 * confirmed their address by returning a code or using a password
1846 * sent to the address from the wiki.
1848 * @return bool
1850 function isEmailConfirmed() {
1851 global $wgEmailAuthentication;
1852 $this->loadFromDatabase();
1853 if( $this->isAnon() )
1854 return false;
1855 if( !$this->isValidEmailAddr( $this->mEmail ) )
1856 return false;
1857 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1858 return false;
1859 return true;
1863 * @param array $groups list of groups
1864 * @return array list of permission key names for given groups combined
1865 * @static
1867 function getGroupPermissions( $groups ) {
1868 global $wgGroupPermissions;
1869 $rights = array();
1870 foreach( $groups as $group ) {
1871 if( isset( $wgGroupPermissions[$group] ) ) {
1872 $rights = array_merge( $rights,
1873 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1876 return $rights;
1880 * @param string $group key name
1881 * @return string localized descriptive name, if provided
1882 * @static
1884 function getGroupName( $group ) {
1885 $key = "group-$group-name";
1886 $name = wfMsg( $key );
1887 if( $name == '' || $name == "&lt;$key&gt;" ) {
1888 return $group;
1889 } else {
1890 return $name;
1895 * Return the set of defined explicit groups.
1896 * The * and 'user' groups are not included.
1897 * @return array
1898 * @static
1900 function getAllGroups() {
1901 global $wgGroupPermissions;
1902 return array_diff(
1903 array_keys( $wgGroupPermissions ),
1904 array( '*', 'user', 'autoconfirmed' ) );