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', 2 );
27 var $mId, $mName, $mPassword, $mEmail, $mNewtalk;
28 var $mEmailAuthenticated;
29 var $mRights, $mOptions;
30 var $mDataLoaded, $mNewpassword;
32 var $mBlockedby, $mBlockreason;
38 var $mVersion; // serialized version
40 /** Construct using User:loadDefaults() */
42 $this->loadDefaults();
43 $this->mVersion
= MW_USER_VERSION
;
47 * Static factory method
48 * @param string $name Username, validated by Title:newFromText()
52 function newFromName( $name ) {
55 # Force usernames to capital
57 $name = $wgContLang->ucfirst( $name );
59 # Clean up name according to title rules
60 $t = Title
::newFromText( $name );
65 # Reject various classes of invalid names
66 $canonicalName = $t->getText();
67 if( !User
::isValidUserName( $canonicalName ) ) {
71 $u->setName( $canonicalName );
72 $u->setId( $u->idFromName( $t->getText() ) );
77 * Factory method to fetch whichever use has a given email confirmation code.
78 * This code is generated when an account is created or its e-mail address
81 * If the code is invalid or has expired, returns NULL.
87 function newFromConfirmationCode( $code ) {
88 $dbr =& wfGetDB( DB_SLAVE
);
89 $name = $dbr->selectField( 'user', 'user_name', array(
90 'user_email_token' => md5( $code ),
91 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
93 if( is_string( $name ) ) {
94 return User
::newFromName( $name );
101 * Serialze sleep function, for better cache efficiency and avoidance of
102 * silly "incomplete type" errors when skins are cached
105 return array( 'mId', 'mName', 'mPassword', 'mEmail', 'mNewtalk',
106 'mEmailAuthenticated', 'mRights', 'mOptions', 'mDataLoaded',
107 'mNewpassword', 'mBlockedby', 'mBlockreason', 'mTouched',
108 'mToken', 'mRealName', 'mHash', 'mGroups' );
112 * Get username given an id.
113 * @param integer $id Database user id
114 * @return string Nickname of a user
117 function whoIs( $id ) {
118 $dbr =& wfGetDB( DB_SLAVE
);
119 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ) );
123 * Get real username given an id.
124 * @param integer $id Database user id
125 * @return string Realname of a user
128 function whoIsReal( $id ) {
129 $dbr =& wfGetDB( DB_SLAVE
);
130 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ) );
134 * Get database id given a user name
135 * @param string $name Nickname of a user
136 * @return integer|null Database user id (null: if non existent
139 function idFromName( $name ) {
140 $fname = "User::idFromName";
142 $nt = Title
::newFromText( $name );
143 if( is_null( $nt ) ) {
147 $dbr =& wfGetDB( DB_SLAVE
);
148 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
150 if ( $s === false ) {
158 * does the string match an anonymous IPv4 address?
161 * @param string $name Nickname of a user
164 function isIP( $name ) {
165 return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/",$name);
166 /*return preg_match("/^
167 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
168 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
169 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
170 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
175 * Is the input a valid username?
177 * Checks if the input is a valid username, we don't want an empty string,
178 * an IP address, anything that containins slashes (would mess up subpages),
179 * is longer than the maximum allowed username size or doesn't begin with
182 * @param string $name
186 function isValidUserName( $name ) {
187 global $wgContLang, $wgMaxNameChars;
190 || User
::isIP( $name )
191 ||
strpos( $name, '/' ) !== false
192 ||
strlen( $name ) > $wgMaxNameChars
193 ||
$name != $wgContLang->ucfirst( $name ) )
200 * Is the input a valid password?
202 * @param string $password
206 function isValidPassword( $password ) {
207 global $wgMinimalPasswordLength;
208 return strlen( $password ) >= $wgMinimalPasswordLength;
212 * does the string match roughly an email address ?
214 * @todo Check for RFC 2822 compilance
217 * @param string $addr email address
221 function isValidEmailAddr ( $addr ) {
222 # There used to be a regular expression here, it got removed because it
223 # rejected valid addresses.
224 return ( trim( $addr ) != '' ) &&
225 (false !== strpos( $addr, '@' ) );
229 * probably return a random password
230 * @return string probably a random password
232 * @todo Check what is doing really [AV]
234 function randomPassword() {
235 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
236 $l = strlen( $pwchars ) - 1;
238 $np = $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
239 $pwchars{mt_rand( 0, $l )} . chr( mt_rand(48, 57) ) .
240 $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
241 $pwchars{mt_rand( 0, $l )};
246 * Set properties to default
247 * Used at construction. It will load per language default settings only
248 * if we have an available language object.
250 function loadDefaults() {
253 $fname = 'User::loadDefaults' . $n;
254 wfProfileIn( $fname );
256 global $wgContLang, $wgIP, $wgDBname;
257 global $wgNamespacesToBeSearchedDefault;
260 $this->mNewtalk
= -1;
261 $this->mName
= $wgIP;
262 $this->mRealName
= $this->mEmail
= '';
263 $this->mEmailAuthenticated
= null;
264 $this->mPassword
= $this->mNewpassword
= '';
265 $this->mRights
= array();
266 $this->mGroups
= array();
267 $this->mOptions
= User
::getDefaultOptions();
269 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
270 $this->mOptions
['searchNs'.$nsnum] = $val;
272 unset( $this->mSkin
);
273 $this->mDataLoaded
= false;
274 $this->mBlockedby
= -1; # Unset
275 $this->setToken(); # Random
276 $this->mHash
= false;
278 if ( isset( $_COOKIE[$wgDBname.'LoggedOut'] ) ) {
279 $this->mTouched
= wfTimestamp( TS_MW
, $_COOKIE[$wgDBname.'LoggedOut'] );
282 $this->mTouched
= '0'; # Allow any pages to be cached
285 wfProfileOut( $fname );
289 * Combine the language default options with any site-specific options
290 * and add the default language variants.
296 function getDefaultOptions() {
298 * Site defaults will override the global/language defaults
300 global $wgContLang, $wgDefaultUserOptions;
301 $defOpt = $wgDefaultUserOptions +
$wgContLang->getDefaultUserOptions();
304 * default language setting
306 $variant = $wgContLang->getPreferredVariant();
307 $defOpt['variant'] = $variant;
308 $defOpt['language'] = $variant;
314 * Get a given default option value.
321 function getDefaultOption( $opt ) {
322 $defOpts = User
::getDefaultOptions();
323 if( isset( $defOpts[$opt] ) ) {
324 return $defOpts[$opt];
331 * Get blocking information
333 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
334 * non-critical checks are done against slaves. Check when actually saving should be done against
337 * Note that even if $bFromSlave is false, the check is done first against slave, then master.
338 * The logic is that if blocked on slave, we'll assume it's either blocked on master or
339 * just slightly outta sync and soon corrected - safer to block slightly more that less.
340 * And it's cheaper to check slave first, then master if needed, than master always.
342 function getBlockedStatus( $bFromSlave = true ) {
343 global $wgIP, $wgBlockCache, $wgProxyList, $wgEnableSorbs, $wgProxyWhitelist;
345 if ( -1 != $this->mBlockedby
) { return; }
347 $this->mBlockedby
= 0;
351 $block = new Block();
352 $block->forUpdate( $bFromSlave );
353 if ( $block->load( $wgIP , $this->mId
) ) {
354 $this->mBlockedby
= $block->mBy
;
355 $this->mBlockreason
= $block->mReason
;
356 $this->spreadBlock();
361 if ( !$this->mBlockedby
) {
362 # Check first against slave, and optionally from master.
363 $block = $wgBlockCache->get( $wgIP, true );
364 if ( !$block && !$bFromSlave )
366 # Not blocked: check against master, to make sure.
367 $wgBlockCache->clearLocal( );
368 $block = $wgBlockCache->get( $wgIP, false );
370 if ( $block !== false ) {
371 $this->mBlockedby
= $block->mBy
;
372 $this->mBlockreason
= $block->mReason
;
377 if ( !$this->isSysop() && !in_array( $wgIP, $wgProxyWhitelist ) ) {
380 if ( array_key_exists( $wgIP, $wgProxyList ) ) {
381 $this->mBlockedby
= wfMsg( 'proxyblocker' );
382 $this->mBlockreason
= wfMsg( 'proxyblockreason' );
386 if ( !$this->mBlockedby
&& $wgEnableSorbs && !$this->getID() ) {
387 if ( $this->inSorbsBlacklist( $wgIP ) ) {
388 $this->mBlockedby
= wfMsg( 'sorbs' );
389 $this->mBlockreason
= wfMsg( 'sorbsreason' );
395 function inSorbsBlacklist( $ip ) {
396 global $wgEnableSorbs;
397 return $wgEnableSorbs &&
398 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
401 function inOpmBlacklist( $ip ) {
403 return $wgEnableOpm &&
404 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
407 function inDnsBlacklist( $ip, $base ) {
408 $fname = 'User::inDnsBlacklist';
409 wfProfileIn( $fname );
414 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
416 for ( $i=4; $i>=1; $i-- ) {
417 $host .= $m[$i] . '.';
422 $ipList = gethostbynamel( $host );
425 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
428 wfDebug( "Requested $host, not found in $base.\n" );
432 wfProfileOut( $fname );
437 * Primitive rate limits: enforce maximum actions per time period
438 * to put a brake on flooding.
440 * Note: when using a shared cache like memcached, IP-address
441 * last-hit counters will be shared across wikis.
443 * @return bool true if a rate limiter was tripped
446 function pingLimiter( $action='edit' ) {
447 global $wgRateLimits;
448 if( !isset( $wgRateLimits[$action] ) ) {
451 if( $this->isAllowed( 'delete' ) ) {
456 global $wgMemc, $wgIP, $wgDBname, $wgRateLimitLog;
457 $fname = 'User::pingLimiter';
458 $limits = $wgRateLimits[$action];
460 $id = $this->getId();
462 if( isset( $limits['anon'] ) && $id == 0 ) {
463 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
466 if( isset( $limits['user'] ) && $id != 0 ) {
467 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
469 if( $this->isNewbie() ) {
470 if( isset( $limits['newbie'] ) && $id != 0 ) {
471 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
473 if( isset( $limits['ip'] ) ) {
474 $keys["mediawiki:limiter:$action:ip:$wgIP"] = $limits['ip'];
476 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $wgIP, $matches ) ) {
477 $subnet = $matches[1];
478 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
483 foreach( $keys as $key => $limit ) {
484 list( $max, $period ) = $limit;
485 $summary = "(limit $max in {$period}s)";
486 $count = $wgMemc->get( $key );
488 if( $count > $max ) {
489 wfDebug( "$fname: tripped! $key at $count $summary\n" );
490 if( $wgRateLimitLog ) {
491 @error_log
( wfTimestamp( TS_MW
) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
495 wfDebug( "$fname: ok. $key at $count $summary\n" );
498 wfDebug( "$fname: adding record for $key $summary\n" );
499 $wgMemc->add( $key, 1, IntVal( $period ) );
501 $wgMemc->incr( $key );
508 * Check if user is blocked
509 * @return bool True if blocked, false otherwise
511 function isBlocked( $bFromSlave = false ) {
512 $this->getBlockedStatus( $bFromSlave );
513 return $this->mBlockedby
!== 0;
517 * Check if user is blocked from editing a particular article
519 function isBlockedFrom( $title, $bFromSlave = false ) {
520 global $wgBlockAllowsUTEdit;
521 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
522 $title->getNamespace() == NS_USER_TALK
)
526 return $this->isBlocked( $bFromSlave );
531 * Get name of blocker
532 * @return string name of blocker
534 function blockedBy() {
535 $this->getBlockedStatus();
536 return $this->mBlockedby
;
540 * Get blocking reason
541 * @return string Blocking reason
543 function blockedFor() {
544 $this->getBlockedStatus();
545 return $this->mBlockreason
;
549 * Initialise php session
551 function SetupSession() {
552 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
553 if( $wgSessionsInMemcached ) {
554 require_once( 'MemcachedSessions.php' );
555 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
556 # If it's left on 'user' or another setting from another
557 # application, it will end up failing. Try to recover.
558 ini_set ( 'session.save_handler', 'files' );
560 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
561 session_cache_limiter( 'private, must-revalidate' );
566 * Read datas from session
569 function loadFromSession() {
570 global $wgMemc, $wgDBname;
572 if ( isset( $_SESSION['wsUserID'] ) ) {
573 if ( 0 != $_SESSION['wsUserID'] ) {
574 $sId = $_SESSION['wsUserID'];
578 } else if ( isset( $_COOKIE["{$wgDBname}UserID"] ) ) {
579 $sId = IntVal( $_COOKIE["{$wgDBname}UserID"] );
580 $_SESSION['wsUserID'] = $sId;
584 if ( isset( $_SESSION['wsUserName'] ) ) {
585 $sName = $_SESSION['wsUserName'];
586 } else if ( isset( $_COOKIE["{$wgDBname}UserName"] ) ) {
587 $sName = $_COOKIE["{$wgDBname}UserName"];
588 $_SESSION['wsUserName'] = $sName;
593 $passwordCorrect = FALSE;
594 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
595 if( !is_object( $user ) ||
$user->mVersion
< MW_USER_VERSION
) {
596 # Expire old serialized objects; they may be corrupt.
599 if($makenew = !$user) {
600 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
603 $user->loadFromDatabase();
605 wfDebug( "User::loadFromSession() got from cache!\n" );
608 if ( isset( $_SESSION['wsToken'] ) ) {
609 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken
;
610 } else if ( isset( $_COOKIE["{$wgDBname}Token"] ) ) {
611 $passwordCorrect = $user->mToken
== $_COOKIE["{$wgDBname}Token"];
613 return new User(); # Can't log in from session
616 if ( ( $sName == $user->mName
) && $passwordCorrect ) {
618 if($wgMemc->set( $key, $user ))
619 wfDebug( "User::loadFromSession() successfully saved user\n" );
621 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
625 return new User(); # Can't log in from session
629 * Load a user from the database
631 function loadFromDatabase() {
632 global $wgCommandLineMode;
633 $fname = "User::loadFromDatabase";
635 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
636 # loading in a command line script, don't assume all command line scripts need it like this
637 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
638 if ( $this->mDataLoaded
) {
643 $this->mId
= IntVal( $this->mId
);
645 /** Anonymous user */
648 $this->mRights
= $this->getGroupPermissions( array( '*' ) );
649 $this->mDataLoaded
= true;
651 } # the following stuff is for non-anonymous users only
653 $dbr =& wfGetDB( DB_SLAVE
);
654 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
655 'user_email_authenticated',
656 'user_real_name','user_options','user_touched', 'user_token' ),
657 array( 'user_id' => $this->mId
), $fname );
659 if ( $s !== false ) {
660 $this->mName
= $s->user_name
;
661 $this->mEmail
= $s->user_email
;
662 $this->mEmailAuthenticated
= wfTimestampOrNull( TS_MW
, $s->user_email_authenticated
);
663 $this->mRealName
= $s->user_real_name
;
664 $this->mPassword
= $s->user_password
;
665 $this->mNewpassword
= $s->user_newpassword
;
666 $this->decodeOptions( $s->user_options
);
667 $this->mTouched
= wfTimestamp(TS_MW
,$s->user_touched
);
668 $this->mToken
= $s->user_token
;
670 $res = $dbr->select( 'user_groups',
672 array( 'ug_user' => $this->mId
),
674 $this->mGroups
= array();
675 while( $row = $dbr->fetchObject( $res ) ) {
676 $this->mGroups
[] = $row->ug_group
;
678 $effectiveGroups = array_merge( array( '*', 'user' ), $this->mGroups
);
679 $this->mRights
= $this->getGroupPermissions( $effectiveGroups );
682 $this->mDataLoaded
= true;
685 function getID() { return $this->mId
; }
686 function setID( $v ) {
688 $this->mDataLoaded
= false;
692 $this->loadFromDatabase();
696 function setName( $str ) {
697 $this->loadFromDatabase();
703 * Return the title dbkey form of the name, for eg user pages.
707 function getTitleKey() {
708 return str_replace( ' ', '_', $this->getName() );
711 function getNewtalk() {
713 $fname = 'User::getNewtalk';
714 $this->loadFromDatabase();
716 # Load the newtalk status if it is unloaded (mNewtalk=-1)
717 if( $this->mNewtalk
== -1 ) {
718 $this->mNewtalk
= 0; # reset talk page status
720 # Check memcached separately for anons, who have no
721 # entire User object stored in there.
723 global $wgDBname, $wgMemc;
724 $key = "$wgDBname:newtalk:ip:{$this->mName}";
725 $newtalk = $wgMemc->get( $key );
726 if( is_integer( $newtalk ) ) {
727 $this->mNewtalk
= $newtalk ?
1 : 0;
728 return (bool)$this->mNewtalk
;
732 $dbr =& wfGetDB( DB_SLAVE
);
733 if ( $wgUseEnotif ) {
734 $res = $dbr->select( 'watchlist',
736 array( 'wl_title' => $this->getTitleKey(),
737 'wl_namespace' => NS_USER_TALK
,
738 'wl_user' => $this->mId
,
739 'wl_notificationtimestamp != 0' ),
740 'User::getNewtalk' );
741 if( $dbr->numRows($res) > 0 ) {
744 $dbr->freeResult( $res );
745 } elseif ( $this->mId
) {
746 $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId
), $fname );
748 if ( $dbr->numRows($res)>0 ) {
751 $dbr->freeResult( $res );
753 $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->mName
), $fname );
754 $this->mNewtalk
= $dbr->numRows( $res ) > 0 ?
1 : 0;
755 $dbr->freeResult( $res );
759 $wgMemc->set( $key, $this->mNewtalk
, time() ); // + 1800 );
763 return ( 0 != $this->mNewtalk
);
766 function setNewtalk( $val ) {
767 $this->loadFromDatabase();
768 $this->mNewtalk
= $val;
769 $this->invalidateCache();
772 function invalidateCache() {
773 global $wgClockSkewFudge;
774 $this->loadFromDatabase();
775 $this->mTouched
= wfTimestamp(TS_MW
, time() +
$wgClockSkewFudge );
776 # Don't forget to save the options after this or
777 # it won't take effect!
780 function validateCache( $timestamp ) {
781 $this->loadFromDatabase();
782 return ($timestamp >= $this->mTouched
);
787 * Will only be salted if $wgPasswordSalt is true
788 * @param string Password.
789 * @return string Salted password or clear password.
791 function addSalt( $p ) {
792 global $wgPasswordSalt;
794 return md5( "{$this->mId}-{$p}" );
800 * Encrypt a password.
801 * It can eventuall salt a password @see User::addSalt()
802 * @param string $p clear Password.
803 * @param string Encrypted password.
805 function encryptPassword( $p ) {
806 return $this->addSalt( md5( $p ) );
809 # Set the password and reset the random token
810 function setPassword( $str ) {
811 $this->loadFromDatabase();
813 $this->mPassword
= $this->encryptPassword( $str );
814 $this->mNewpassword
= '';
817 # Set the random token (used for persistent authentication)
818 function setToken( $token = false ) {
819 global $wgSecretKey, $wgProxyKey, $wgDBname;
821 if ( $wgSecretKey ) {
823 } elseif ( $wgProxyKey ) {
828 $this->mToken
= md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId
);
830 $this->mToken
= $token;
835 function setCookiePassword( $str ) {
836 $this->loadFromDatabase();
837 $this->mCookiePassword
= md5( $str );
840 function setNewpassword( $str ) {
841 $this->loadFromDatabase();
842 $this->mNewpassword
= $this->encryptPassword( $str );
845 function getEmail() {
846 $this->loadFromDatabase();
847 return $this->mEmail
;
850 function getEmailAuthenticationTimestamp() {
851 $this->loadFromDatabase();
852 return $this->mEmailAuthenticated
;
855 function setEmail( $str ) {
856 $this->loadFromDatabase();
857 $this->mEmail
= $str;
860 function getRealName() {
861 $this->loadFromDatabase();
862 return $this->mRealName
;
865 function setRealName( $str ) {
866 $this->loadFromDatabase();
867 $this->mRealName
= $str;
870 function getOption( $oname ) {
871 $this->loadFromDatabase();
872 if ( array_key_exists( $oname, $this->mOptions
) ) {
873 return trim( $this->mOptions
[$oname] );
879 function setOption( $oname, $val ) {
880 $this->loadFromDatabase();
881 if ( $oname == 'skin' ) {
882 # Clear cached skin, so the new one displays immediately in Special:Preferences
883 unset( $this->mSkin
);
885 $this->mOptions
[$oname] = $val;
886 $this->invalidateCache();
889 function getRights() {
890 $this->loadFromDatabase();
891 return $this->mRights
;
895 * Get the list of explicit group memberships this user has.
896 * The implicit * and user groups are not included.
897 * @return array of strings
899 function getGroups() {
900 $this->loadFromDatabase();
901 return $this->mGroups
;
905 * Get the list of implicit group memberships this user has.
906 * This includes all explicit groups, plus 'user' if logged in
907 * and '*' for all accounts.
908 * @return array of strings
910 function getEffectiveGroups() {
911 $base = array( '*' );
912 if( $this->isLoggedIn() ) {
915 return array_merge( $base, $this->getGroups() );
919 * Remove the user from the given group.
920 * This takes immediate effect.
923 function addGroup( $group ) {
924 $dbw =& wfGetDB( DB_MASTER
);
925 $dbw->insert( 'user_groups',
927 'ug_user' => $this->getID(),
928 'ug_group' => $group,
933 $this->mGroups
= array_merge( $this->mGroups
, array( $group ) );
934 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
936 $this->invalidateCache();
937 $this->saveSettings();
941 * Remove the user from the given group.
942 * This takes immediate effect.
945 function removeGroup( $group ) {
946 $dbw =& wfGetDB( DB_MASTER
);
947 $dbw->delete( 'user_groups',
949 'ug_user' => $this->getID(),
950 'ug_group' => $group,
952 'User::removeGroup' );
954 $this->mGroups
= array_diff( $this->mGroups
, array( $group ) );
955 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
957 $this->invalidateCache();
958 $this->saveSettings();
963 * A more legible check for non-anonymousness.
964 * Returns true if the user is not an anonymous visitor.
968 function isLoggedIn() {
969 return( $this->getID() != 0 );
973 * A more legible check for anonymousness.
974 * Returns true if the user is an anonymous visitor.
979 return !$this->isLoggedIn();
983 * Check if a user is sysop
984 * Die with backtrace. Use User:isAllowed() instead.
988 return $this->isAllowed( 'protect' );
992 function isDeveloper() {
993 return $this->isAllowed( 'siteadmin' );
997 function isBureaucrat() {
998 return $this->isAllowed( 'makesysop' );
1002 * Whether the user is a bot
1003 * @todo need to be migrated to the new user level management sytem
1006 $this->loadFromDatabase();
1007 return in_array( 'bot', $this->mRights
);
1011 * Check if user is allowed to access a feature / make an action
1012 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1013 * @return boolean True: action is allowed, False: action should not be allowed
1015 function isAllowed($action='') {
1016 $this->loadFromDatabase();
1017 return in_array( $action , $this->mRights
);
1021 * Load a skin if it doesn't exist or return it
1022 * @todo FIXME : need to check the old failback system [AV]
1024 function &getSkin() {
1026 if ( ! isset( $this->mSkin
) ) {
1027 $fname = 'User::getSkin';
1028 wfProfileIn( $fname );
1030 # get all skin names available
1031 $skinNames = Skin
::getSkinNames();
1034 $userSkin = $this->getOption( 'skin' );
1035 if ( $userSkin == '' ) { $userSkin = 'standard'; }
1037 if ( !isset( $skinNames[$userSkin] ) ) {
1038 # in case the user skin could not be found find a replacement
1042 2 => 'CologneBlue');
1043 # if phptal is enabled we should have monobook skin that
1044 # superseed the good old SkinStandard.
1045 if ( isset( $skinNames['monobook'] ) ) {
1046 $fallback[0] = 'MonoBook';
1049 if(is_numeric($userSkin) && isset( $fallback[$userSkin]) ){
1050 $sn = $fallback[$userSkin];
1055 # The user skin is available
1056 $sn = $skinNames[$userSkin];
1059 # Grab the skin class and initialise it. Each skin checks for PHPTal
1060 # and will not load if it's not enabled.
1061 require_once( $IP.'/skins/'.$sn.'.php' );
1063 # Check if we got if not failback to default skin
1064 $className = 'Skin'.$sn;
1065 if( !class_exists( $className ) ) {
1066 # DO NOT die if the class isn't found. This breaks maintenance
1067 # scripts and can cause a user account to be unrecoverable
1068 # except by SQL manipulation if a previously valid skin name
1069 # is no longer valid.
1070 $className = 'SkinStandard';
1071 require_once( $IP.'/skins/Standard.php' );
1073 $this->mSkin
=& new $className;
1074 wfProfileOut( $fname );
1076 return $this->mSkin
;
1080 * @param string $title Article title to look at
1084 * Check watched status of an article
1085 * @return bool True if article is watched
1087 function isWatched( $title ) {
1088 $wl = WatchedItem
::fromUserTitle( $this, $title );
1089 return $wl->isWatched();
1095 function addWatch( $title ) {
1096 $wl = WatchedItem
::fromUserTitle( $this, $title );
1098 $this->invalidateCache();
1102 * Stop watching an article
1104 function removeWatch( $title ) {
1105 $wl = WatchedItem
::fromUserTitle( $this, $title );
1107 $this->invalidateCache();
1111 * Clear the user's notification timestamp for the given title.
1112 * If e-notif e-mails are on, they will receive notification mails on
1113 * the next change of the page if it's watched etc.
1115 function clearNotification( &$title ) {
1116 global $wgUser, $wgUseEnotif;
1118 if ( !$wgUseEnotif ) {
1122 $userid = $this->getID();
1127 // Only update the timestamp if the page is being watched.
1128 // The query to find out if it is watched is cached both in memcached and per-invocation,
1129 // and when it does have to be executed, it can be on a slave
1130 // If this is the user's newtalk page, we always update the timestamp
1131 if ($title->getNamespace() == NS_USER_TALK
&&
1132 $title->getText() == $wgUser->getName())
1135 } elseif ( $this->getID() == $wgUser->getID() ) {
1136 $watched = $title->userIsWatching();
1141 // If the page is watched by the user (or may be watched), update the timestamp on any
1142 // any matching rows
1144 $dbw =& wfGetDB( DB_MASTER
);
1145 $success = $dbw->update( 'watchlist',
1147 'wl_notificationtimestamp' => 0
1148 ), array( /* WHERE */
1149 'wl_title' => $title->getDBkey(),
1150 'wl_namespace' => $title->getNamespace(),
1151 'wl_user' => $this->getID()
1152 ), 'User::clearLastVisited'
1160 * Resets all of the given user's page-change notification timestamps.
1161 * If e-notif e-mails are on, they will receive notification mails on
1162 * the next change of any watched page.
1164 * @param int $currentUser user ID number
1167 function clearAllNotifications( $currentUser ) {
1168 global $wgUseEnotif;
1169 if ( !$wgUseEnotif ) {
1172 if( $currentUser != 0 ) {
1174 $dbw =& wfGetDB( DB_MASTER
);
1175 $success = $dbw->update( 'watchlist',
1177 'wl_notificationtimestamp' => 0
1178 ), array( /* WHERE */
1179 'wl_user' => $currentUser
1180 ), 'UserMailer::clearAll'
1183 # we also need to clear here the "you have new message" notification for the own user_talk page
1184 # This is cleared one page view later in Article::viewUpdates();
1190 * @return string Encoding options
1192 function encodeOptions() {
1194 foreach ( $this->mOptions
as $oname => $oval ) {
1195 array_push( $a, $oname.'='.$oval );
1197 $s = implode( "\n", $a );
1204 function decodeOptions( $str ) {
1205 $a = explode( "\n", $str );
1206 foreach ( $a as $s ) {
1207 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1208 $this->mOptions
[$m[1]] = $m[2];
1213 function setCookies() {
1214 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgDBname;
1215 if ( 0 == $this->mId
) return;
1216 $this->loadFromDatabase();
1217 $exp = time() +
$wgCookieExpiration;
1219 $_SESSION['wsUserID'] = $this->mId
;
1220 setcookie( $wgDBname.'UserID', $this->mId
, $exp, $wgCookiePath, $wgCookieDomain );
1222 $_SESSION['wsUserName'] = $this->mName
;
1223 setcookie( $wgDBname.'UserName', $this->mName
, $exp, $wgCookiePath, $wgCookieDomain );
1225 $_SESSION['wsToken'] = $this->mToken
;
1226 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1227 setcookie( $wgDBname.'Token', $this->mToken
, $exp, $wgCookiePath, $wgCookieDomain );
1229 setcookie( $wgDBname.'Token', '', time() - 3600 );
1235 * It will clean the session cookie
1238 global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP;
1239 $this->loadDefaults();
1240 $this->setLoaded( true );
1242 $_SESSION['wsUserID'] = 0;
1244 setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1245 setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1247 # Remember when user logged out, to prevent seeing cached pages
1248 setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() +
86400, $wgCookiePath, $wgCookieDomain );
1252 * Save object settings into database
1254 function saveSettings() {
1255 global $wgMemc, $wgDBname, $wgUseEnotif;
1256 $fname = 'User::saveSettings';
1258 if ( wfReadOnly() ) { return; }
1259 $this->saveNewtalk();
1260 if ( 0 == $this->mId
) { return; }
1262 $dbw =& wfGetDB( DB_MASTER
);
1263 $dbw->update( 'user',
1265 'user_name' => $this->mName
,
1266 'user_password' => $this->mPassword
,
1267 'user_newpassword' => $this->mNewpassword
,
1268 'user_real_name' => $this->mRealName
,
1269 'user_email' => $this->mEmail
,
1270 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1271 'user_options' => $this->encodeOptions(),
1272 'user_touched' => $dbw->timestamp($this->mTouched
),
1273 'user_token' => $this->mToken
1274 ), array( /* WHERE */
1275 'user_id' => $this->mId
1278 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1282 * Save value of new talk flag.
1284 function saveNewtalk() {
1285 global $wgDBname, $wgMemc, $wgUseEnotif;
1287 $fname = 'User::saveNewtalk';
1291 if ( wfReadOnly() ) { return ; }
1292 $dbr =& wfGetDB( DB_SLAVE
);
1293 $dbw =& wfGetDB( DB_MASTER
);
1295 if ( $wgUseEnotif ) {
1296 if ( ! $this->getNewtalk() ) {
1297 # Delete the watchlist entry for user_talk page X watched by user X
1298 $dbw->delete( 'watchlist',
1299 array( 'wl_user' => $this->mId
,
1300 'wl_title' => $this->getTitleKey(),
1301 'wl_namespace' => NS_USER_TALK
),
1303 if ( $dbw->affectedRows() ) {
1307 # Anon users have a separate memcache space for newtalk
1308 # since they don't store their own info. Trim...
1309 $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" );
1313 if ($this->getID() != 0) {
1315 $value = $this->getID();
1319 $value = $this->mName
;
1320 $key = "$wgDBname:newtalk:ip:$this->mName";
1323 $dbr =& wfGetDB( DB_SLAVE
);
1324 $dbw =& wfGetDB( DB_MASTER
);
1326 $res = $dbr->selectField('user_newtalk', $field,
1327 array($field => $value), $fname);
1330 if ($res !== false && $this->mNewtalk
== 0) {
1331 $dbw->delete('user_newtalk', array($field => $value), $fname);
1333 $wgMemc->set( $key, 0 );
1335 } else if ($res === false && $this->mNewtalk
== 1) {
1336 $dbw->insert('user_newtalk', array($field => $value), $fname);
1338 $wgMemc->set( $key, 1 );
1345 # Update user_touched, so that newtalk notifications in the client cache are invalidated
1346 if ( $changed && $this->getID() ) {
1347 $dbw->update('user',
1348 /*SET*/ array( 'user_touched' => $this->mTouched
),
1349 /*WHERE*/ array( 'user_id' => $this->getID() ),
1351 $wgMemc->set( "$wgDBname:user:id:{$this->mId}", $this, 86400 );
1356 * Checks if a user with the given name exists, returns the ID
1358 function idForName() {
1359 $fname = 'User::idForName';
1362 $s = trim( $this->mName
);
1363 if ( 0 == strcmp( '', $s ) ) return 0;
1365 $dbr =& wfGetDB( DB_SLAVE
);
1366 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1367 if ( $id === false ) {
1374 * Add user object to the database
1376 function addToDatabase() {
1377 $fname = 'User::addToDatabase';
1378 $dbw =& wfGetDB( DB_MASTER
);
1379 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1380 $dbw->insert( 'user',
1382 'user_id' => $seqVal,
1383 'user_name' => $this->mName
,
1384 'user_password' => $this->mPassword
,
1385 'user_newpassword' => $this->mNewpassword
,
1386 'user_email' => $this->mEmail
,
1387 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1388 'user_real_name' => $this->mRealName
,
1389 'user_options' => $this->encodeOptions(),
1390 'user_token' => $this->mToken
1393 $this->mId
= $dbw->insertId();
1396 function spreadBlock() {
1398 # If the (non-anonymous) user is blocked, this function will block any IP address
1399 # that they successfully log on from.
1400 $fname = 'User::spreadBlock';
1402 wfDebug( "User:spreadBlock()\n" );
1403 if ( $this->mId
== 0 ) {
1407 $userblock = Block
::newFromDB( '', $this->mId
);
1408 if ( !$userblock->isValid() ) {
1412 # Check if this IP address is already blocked
1413 $ipblock = Block
::newFromDB( $wgIP );
1414 if ( $ipblock->isValid() ) {
1415 # Just update the timestamp
1416 $ipblock->updateTimestamp();
1420 # Make a new block object with the desired properties
1421 wfDebug( "Autoblocking {$this->mName}@{$wgIP}\n" );
1422 $ipblock->mAddress
= $wgIP;
1423 $ipblock->mUser
= 0;
1424 $ipblock->mBy
= $userblock->mBy
;
1425 $ipblock->mReason
= wfMsg( 'autoblocker', $this->getName(), $userblock->mReason
);
1426 $ipblock->mTimestamp
= wfTimestampNow();
1427 $ipblock->mAuto
= 1;
1428 # If the user is already blocked with an expiry date, we don't
1429 # want to pile on top of that!
1430 if($userblock->mExpiry
) {
1431 $ipblock->mExpiry
= min ( $userblock->mExpiry
, Block
::getAutoblockExpiry( $ipblock->mTimestamp
));
1433 $ipblock->mExpiry
= Block
::getAutoblockExpiry( $ipblock->mTimestamp
);
1441 function getPageRenderingHash() {
1444 return $this->mHash
;
1447 // stubthreshold is only included below for completeness,
1448 // it will always be 0 when this function is called by parsercache.
1450 $confstr = $this->getOption( 'math' );
1451 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1452 $confstr .= '!' . $this->getOption( 'date' );
1453 $confstr .= '!' . $this->getOption( 'numberheadings' );
1454 $confstr .= '!' . $this->getOption( 'language' );
1455 $confstr .= '!' . $this->getOption( 'thumbsize' );
1456 // add in language specific options, if any
1457 $extra = $wgContLang->getExtraHashOptions();
1460 $this->mHash
= $confstr;
1464 function isAllowedToCreateAccount() {
1465 return $this->isAllowed( 'createaccount' );
1469 * Set mDataLoaded, return previous value
1470 * Use this to prevent DB access in command-line scripts or similar situations
1472 function setLoaded( $loaded ) {
1473 return wfSetVar( $this->mDataLoaded
, $loaded );
1477 * Get this user's personal page title.
1482 function getUserPage() {
1483 return Title
::makeTitle( NS_USER
, $this->mName
);
1487 * Get this user's talk page title.
1492 function getTalkPage() {
1493 $title = $this->getUserPage();
1494 return $title->getTalkPage();
1500 function getMaxID() {
1501 $dbr =& wfGetDB( DB_SLAVE
);
1502 return $dbr->selectField( 'user', 'max(user_id)', false );
1506 * Determine whether the user is a newbie. Newbies are either
1507 * anonymous IPs, or the 1% most recently created accounts.
1508 * Bots and sysops are excluded.
1509 * @return bool True if it is a newbie.
1511 function isNewbie() {
1512 return $this->isAnon() ||
$this->mId
> User
::getMaxID() * 0.99 && !$this->isAllowed( 'delete' ) && !$this->isBot();
1516 * Check to see if the given clear-text password is one of the accepted passwords
1517 * @param string $password User password.
1518 * @return bool True if the given password is correct otherwise False.
1520 function checkPassword( $password ) {
1521 global $wgAuth, $wgMinimalPasswordLength;
1522 $this->loadFromDatabase();
1524 // Even though we stop people from creating passwords that
1525 // are shorter than this, doesn't mean people wont be able
1526 // to. Certain authentication plugins do NOT want to save
1527 // domain passwords in a mysql database, so we should
1528 // check this (incase $wgAuth->strict() is false).
1529 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1533 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1535 } elseif( $wgAuth->strict() ) {
1536 /* Auth plugin doesn't allow local authentication */
1539 $ep = $this->encryptPassword( $password );
1540 if ( 0 == strcmp( $ep, $this->mPassword
) ) {
1542 } elseif ( ($this->mNewpassword
!= '') && (0 == strcmp( $ep, $this->mNewpassword
)) ) {
1544 } elseif ( function_exists( 'iconv' ) ) {
1545 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1546 # Check for this with iconv
1547 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1548 if ( 0 == strcmp( $cp1252hash, $this->mPassword
) ) {
1556 * Initialize (if necessary) and return a session token value
1557 * which can be used in edit forms to show that the user's
1558 * login credentials aren't being hijacked with a foreign form
1561 * @param mixed $salt - Optional function-specific data for hash.
1562 * Use a string or an array of strings.
1566 function editToken( $salt = '' ) {
1567 if( !isset( $_SESSION['wsEditToken'] ) ) {
1568 $token = $this->generateToken();
1569 $_SESSION['wsEditToken'] = $token;
1571 $token = $_SESSION['wsEditToken'];
1573 if( is_array( $salt ) ) {
1574 $salt = implode( '|', $salt );
1576 return md5( $token . $salt );
1580 * Generate a hex-y looking random token for various uses.
1581 * Could be made more cryptographically sure if someone cares.
1584 function generateToken( $salt = '' ) {
1585 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1586 return md5( $token . $salt );
1590 * Check given value against the token value stored in the session.
1591 * A match should confirm that the form was submitted from the
1592 * user's own login session, not a form submission from a third-party
1595 * @param string $val - the input value to compare
1596 * @param string $salt - Optional function-specific data for hash
1600 function matchEditToken( $val, $salt = '' ) {
1601 return ( $val == $this->editToken( $salt ) );
1605 * Generate a new e-mail confirmation token and send a confirmation
1606 * mail to the user's given address.
1608 * @return mixed True on success, a WikiError object on failure.
1610 function sendConfirmationMail() {
1611 global $wgIP, $wgContLang;
1612 $url = $this->confirmationTokenUrl( $expiration );
1613 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1614 wfMsg( 'confirmemail_body',
1618 $wgContLang->timeanddate( $expiration, false ) ) );
1622 * Send an e-mail to this user's account. Does not check for
1623 * confirmed status or validity.
1625 * @param string $subject
1626 * @param string $body
1627 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1628 * @return mixed True on success, a WikiError object on failure.
1630 function sendMail( $subject, $body, $from = null ) {
1631 if( is_null( $from ) ) {
1632 global $wgPasswordSender;
1633 $from = $wgPasswordSender;
1636 require_once( 'UserMailer.php' );
1637 $error = userMailer( $this->getEmail(), $from, $subject, $body );
1639 if( $error == '' ) {
1642 return new WikiError( $error );
1647 * Generate, store, and return a new e-mail confirmation code.
1648 * A hash (unsalted since it's used as a key) is stored.
1649 * @param &$expiration mixed output: accepts the expiration time
1653 function confirmationToken( &$expiration ) {
1654 $fname = 'User::confirmationToken';
1657 $expires = $now +
7 * 24 * 60 * 60;
1658 $expiration = wfTimestamp( TS_MW
, $expires );
1660 $token = $this->generateToken( $this->mId
. $this->mEmail
. $expires );
1661 $hash = md5( $token );
1663 $dbw =& wfGetDB( DB_MASTER
);
1664 $dbw->update( 'user',
1665 array( 'user_email_token' => $hash,
1666 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1667 array( 'user_id' => $this->mId
),
1674 * Generate and store a new e-mail confirmation token, and return
1675 * the URL the user can use to confirm.
1676 * @param &$expiration mixed output: accepts the expiration time
1680 function confirmationTokenUrl( &$expiration ) {
1681 $token = $this->confirmationToken( $expiration );
1682 $title = Title
::makeTitle( NS_SPECIAL
, 'Confirmemail/' . $token );
1683 return $title->getFullUrl();
1687 * Mark the e-mail address confirmed and save.
1689 function confirmEmail() {
1690 $this->loadFromDatabase();
1691 $this->mEmailAuthenticated
= wfTimestampNow();
1692 $this->saveSettings();
1697 * Is this user allowed to send e-mails within limits of current
1698 * site configuration?
1701 function canSendEmail() {
1702 return $this->isEmailConfirmed();
1706 * Is this user allowed to receive e-mails within limits of current
1707 * site configuration?
1710 function canReceiveEmail() {
1711 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1715 * Is this user's e-mail address valid-looking and confirmed within
1716 * limits of the current site configuration?
1718 * If $wgEmailAuthentication is on, this may require the user to have
1719 * confirmed their address by returning a code or using a password
1720 * sent to the address from the wiki.
1724 function isEmailConfirmed() {
1725 global $wgEmailAuthentication;
1726 $this->loadFromDatabase();
1727 if( $this->isAnon() )
1729 if( !$this->isValidEmailAddr( $this->mEmail
) )
1731 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1737 * @param array $groups list of groups
1738 * @return array list of permission key names for given groups combined
1741 function getGroupPermissions( $groups ) {
1742 global $wgGroupPermissions;
1744 foreach( $groups as $group ) {
1745 if( isset( $wgGroupPermissions[$group] ) ) {
1746 $rights = array_merge( $rights,
1747 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1754 * @param string $group key name
1755 * @return string localized descriptive name, if provided
1758 function getGroupName( $group ) {
1759 $key = "group-$group-name";
1760 $name = wfMsg( $key );
1761 if( $name == '' ||
$name == "<$key>" ) {
1769 * Return the set of defined explicit groups.
1770 * The * and 'user' groups are not included.
1774 function getAllGroups() {
1775 global $wgGroupPermissions;
1777 array_keys( $wgGroupPermissions ),
1778 array( '*', 'user' ) );