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 # Clean up name according to title rules
57 $t = Title
::newFromText( $name );
61 $u->setName( $t->getText() );
62 $u->setId( $u->idFromName( $t->getText() ) );
68 * Factory method to fetch whichever use has a given email confirmation code.
69 * This code is generated when an account is created or its e-mail address
72 * If the code is invalid or has expired, returns NULL.
78 function newFromConfirmationCode( $code ) {
79 $dbr =& wfGetDB( DB_SLAVE
);
80 $name = $dbr->selectField( 'user', 'user_name', array(
81 'user_email_token' => md5( $code ),
82 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
84 if( is_string( $name ) ) {
85 return User
::newFromName( $name );
92 * Serialze sleep function, for better cache efficiency and avoidance of
93 * silly "incomplete type" errors when skins are cached
96 return array( 'mId', 'mName', 'mPassword', 'mEmail', 'mNewtalk',
97 'mEmailAuthenticated', 'mRights', 'mOptions', 'mDataLoaded',
98 'mNewpassword', 'mBlockedby', 'mBlockreason', 'mTouched',
99 'mToken', 'mRealName', 'mHash', 'mGroups' );
103 * Get username given an id.
104 * @param integer $id Database user id
105 * @return string Nickname of a user
108 function whoIs( $id ) {
109 $dbr =& wfGetDB( DB_SLAVE
);
110 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ) );
114 * Get real username given an id.
115 * @param integer $id Database user id
116 * @return string Realname of a user
119 function whoIsReal( $id ) {
120 $dbr =& wfGetDB( DB_SLAVE
);
121 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ) );
125 * Get database id given a user name
126 * @param string $name Nickname of a user
127 * @return integer|null Database user id (null: if non existent
130 function idFromName( $name ) {
131 $fname = "User::idFromName";
133 $nt = Title
::newFromText( $name );
134 if( is_null( $nt ) ) {
138 $dbr =& wfGetDB( DB_SLAVE
);
139 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
141 if ( $s === false ) {
149 * does the string match an anonymous IPv4 address?
152 * @param string $name Nickname of a user
155 function isIP( $name ) {
156 return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/",$name);
157 /*return preg_match("/^
158 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
159 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
160 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
161 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
166 * does the string match roughly an email address ?
170 * @param string $addr email address
174 function isValidEmailAddr ( $addr ) {
175 # There used to be a regular expression here, it got removed because it
176 # rejected valid addresses.
177 return ( trim( $addr ) != '' ) &&
178 (false !== strpos( $addr, '@' ) );
182 * probably return a random password
183 * @return string probably a random password
185 * @todo Check what is doing really [AV]
187 function randomPassword() {
188 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
189 $l = strlen( $pwchars ) - 1;
191 $np = $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
192 $pwchars{mt_rand( 0, $l )} . chr( mt_rand(48, 57) ) .
193 $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
194 $pwchars{mt_rand( 0, $l )};
199 * Set properties to default
200 * Used at construction. It will load per language default settings only
201 * if we have an available language object.
203 function loadDefaults() {
206 $fname = 'User::loadDefaults' . $n;
207 wfProfileIn( $fname );
209 global $wgContLang, $wgIP, $wgDBname;
210 global $wgNamespacesToBeSearchedDefault;
213 $this->mNewtalk
= -1;
214 $this->mName
= $wgIP;
215 $this->mRealName
= $this->mEmail
= '';
216 $this->mEmailAuthenticated
= null;
217 $this->mPassword
= $this->mNewpassword
= '';
218 $this->mRights
= array();
219 $this->mGroups
= array();
220 $this->mOptions
= User
::getDefaultOptions();
222 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
223 $this->mOptions
['searchNs'.$nsnum] = $val;
225 unset( $this->mSkin
);
226 $this->mDataLoaded
= false;
227 $this->mBlockedby
= -1; # Unset
228 $this->setToken(); # Random
229 $this->mHash
= false;
231 if ( isset( $_COOKIE[$wgDBname.'LoggedOut'] ) ) {
232 $this->mTouched
= wfTimestamp( TS_MW
, $_COOKIE[$wgDBname.'LoggedOut'] );
235 $this->mTouched
= '0'; # Allow any pages to be cached
238 wfProfileOut( $fname );
242 * Combine the language default options with any site-specific options
243 * and add the default language variants.
249 function getDefaultOptions() {
251 * Site defaults will override the global/language defaults
253 global $wgContLang, $wgDefaultUserOptions;
254 $defOpt = $wgDefaultUserOptions +
$wgContLang->getDefaultUserOptions();
257 * default language setting
259 $variant = $wgContLang->getPreferredVariant();
260 $defOpt['variant'] = $variant;
261 $defOpt['language'] = $variant;
267 * Get a given default option value.
274 function getDefaultOption( $opt ) {
275 $defOpts = User
::getDefaultOptions();
276 if( isset( $defOpts[$opt] ) ) {
277 return $defOpts[$opt];
284 * Get blocking information
286 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
287 * non-critical checks are done against slaves. Check when actually saving should be done against
290 * Note that even if $bFromSlave is false, the check is done first against slave, then master.
291 * The logic is that if blocked on slave, we'll assume it's either blocked on master or
292 * just slightly outta sync and soon corrected - safer to block slightly more that less.
293 * And it's cheaper to check slave first, then master if needed, than master always.
295 function getBlockedStatus( $bFromSlave = true ) {
296 global $wgIP, $wgBlockCache, $wgProxyList, $wgEnableSorbs, $wgProxyWhitelist;
298 if ( -1 != $this->mBlockedby
) { return; }
300 $this->mBlockedby
= 0;
304 $block = new Block();
305 $block->forUpdate( $bFromSlave );
306 if ( $block->load( $wgIP , $this->mId
) ) {
307 $this->mBlockedby
= $block->mBy
;
308 $this->mBlockreason
= $block->mReason
;
309 $this->spreadBlock();
314 if ( !$this->mBlockedby
) {
315 # Check first against slave, and optionally from master.
316 $block = $wgBlockCache->get( $wgIP, true );
317 if ( !$block && !$bFromSlave )
319 # Not blocked: check against master, to make sure.
320 $wgBlockCache->clearLocal( );
321 $block = $wgBlockCache->get( $wgIP, false );
323 if ( $block !== false ) {
324 $this->mBlockedby
= $block->mBy
;
325 $this->mBlockreason
= $block->mReason
;
330 if ( !$this->isSysop() && !in_array( $wgIP, $wgProxyWhitelist ) ) {
333 if ( array_key_exists( $wgIP, $wgProxyList ) ) {
334 $this->mBlockedby
= wfMsg( 'proxyblocker' );
335 $this->mBlockreason
= wfMsg( 'proxyblockreason' );
339 if ( !$this->mBlockedby
&& $wgEnableSorbs && !$this->getID() ) {
340 if ( $this->inSorbsBlacklist( $wgIP ) ) {
341 $this->mBlockedby
= wfMsg( 'sorbs' );
342 $this->mBlockreason
= wfMsg( 'sorbsreason' );
348 function inSorbsBlacklist( $ip ) {
349 global $wgEnableSorbs;
350 return $wgEnableSorbs &&
351 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
354 function inOpmBlacklist( $ip ) {
356 return $wgEnableOpm &&
357 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
360 function inDnsBlacklist( $ip, $base ) {
361 $fname = 'User::inDnsBlacklist';
362 wfProfileIn( $fname );
367 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
369 for ( $i=4; $i>=1; $i-- ) {
370 $host .= $m[$i] . '.';
375 $ipList = gethostbynamel( $host );
378 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
381 wfDebug( "Requested $host, not found in $base.\n" );
385 wfProfileOut( $fname );
390 * Primitive rate limits: enforce maximum actions per time period
391 * to put a brake on flooding.
393 * Note: when using a shared cache like memcached, IP-address
394 * last-hit counters will be shared across wikis.
396 * @return bool true if a rate limiter was tripped
399 function pingLimiter( $action='edit' ) {
400 global $wgRateLimits;
401 if( !isset( $wgRateLimits[$action] ) ) {
404 if( $this->isAllowed( 'delete' ) ) {
409 global $wgMemc, $wgIP, $wgDBname, $wgRateLimitLog;
410 $fname = 'User::pingLimiter';
411 $limits = $wgRateLimits[$action];
413 $id = $this->getId();
415 if( isset( $limits['anon'] ) && $id == 0 ) {
416 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
419 if( isset( $limits['user'] ) && $id != 0 ) {
420 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
422 if( $this->isNewbie() ) {
423 if( isset( $limits['newbie'] ) && $id != 0 ) {
424 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
426 if( isset( $limits['ip'] ) ) {
427 $keys["mediawiki:limiter:$action:ip:$wgIP"] = $limits['ip'];
429 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $wgIP, $matches ) ) {
430 $subnet = $matches[1];
431 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
436 foreach( $keys as $key => $limit ) {
437 list( $max, $period ) = $limit;
438 $summary = "(limit $max in {$period}s)";
439 $count = $wgMemc->get( $key );
441 if( $count > $max ) {
442 wfDebug( "$fname: tripped! $key at $count $summary\n" );
443 if( $wgRateLimitLog ) {
444 @error_log
( wfTimestamp( TS_MW
) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
448 wfDebug( "$fname: ok. $key at $count $summary\n" );
451 wfDebug( "$fname: adding record for $key $summary\n" );
452 $wgMemc->add( $key, 1, IntVal( $period ) );
454 $wgMemc->incr( $key );
461 * Check if user is blocked
462 * @return bool True if blocked, false otherwise
464 function isBlocked( $bFromSlave = false ) {
465 $this->getBlockedStatus( $bFromSlave );
466 return $this->mBlockedby
!== 0;
470 * Get name of blocker
471 * @return string name of blocker
473 function blockedBy() {
474 $this->getBlockedStatus();
475 return $this->mBlockedby
;
479 * Get blocking reason
480 * @return string Blocking reason
482 function blockedFor() {
483 $this->getBlockedStatus();
484 return $this->mBlockreason
;
488 * Initialise php session
490 function SetupSession() {
491 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
492 if( $wgSessionsInMemcached ) {
493 require_once( 'MemcachedSessions.php' );
494 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
495 # If it's left on 'user' or another setting from another
496 # application, it will end up failing. Try to recover.
497 ini_set ( 'session.save_handler', 'files' );
499 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
500 session_cache_limiter( 'private, must-revalidate' );
505 * Read datas from session
508 function loadFromSession() {
509 global $wgMemc, $wgDBname;
511 if ( isset( $_SESSION['wsUserID'] ) ) {
512 if ( 0 != $_SESSION['wsUserID'] ) {
513 $sId = $_SESSION['wsUserID'];
517 } else if ( isset( $_COOKIE["{$wgDBname}UserID"] ) ) {
518 $sId = IntVal( $_COOKIE["{$wgDBname}UserID"] );
519 $_SESSION['wsUserID'] = $sId;
523 if ( isset( $_SESSION['wsUserName'] ) ) {
524 $sName = $_SESSION['wsUserName'];
525 } else if ( isset( $_COOKIE["{$wgDBname}UserName"] ) ) {
526 $sName = $_COOKIE["{$wgDBname}UserName"];
527 $_SESSION['wsUserName'] = $sName;
532 $passwordCorrect = FALSE;
533 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
534 if( $user->mVersion
< MW_USER_VERSION
) $user = false;
535 if($makenew = !$user) {
536 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
539 $user->loadFromDatabase();
541 wfDebug( "User::loadFromSession() got from cache!\n" );
544 if ( isset( $_SESSION['wsToken'] ) ) {
545 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken
;
546 } else if ( isset( $_COOKIE["{$wgDBname}Token"] ) ) {
547 $passwordCorrect = $user->mToken
== $_COOKIE["{$wgDBname}Token"];
549 return new User(); # Can't log in from session
552 if ( ( $sName == $user->mName
) && $passwordCorrect ) {
554 if($wgMemc->set( $key, $user ))
555 wfDebug( "User::loadFromSession() successfully saved user\n" );
557 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
561 return new User(); # Can't log in from session
565 * Load a user from the database
567 function loadFromDatabase() {
568 global $wgCommandLineMode;
569 $fname = "User::loadFromDatabase";
571 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
572 # loading in a command line script, don't assume all command line scripts need it like this
573 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
574 if ( $this->mDataLoaded
) {
579 $this->mId
= IntVal( $this->mId
);
581 /** Anonymous user */
584 $this->mRights
= $this->getGroupPermissions( array( '*' ) );
585 $this->mDataLoaded
= true;
587 } # the following stuff is for non-anonymous users only
589 $dbr =& wfGetDB( DB_SLAVE
);
590 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
591 'user_email_authenticated',
592 'user_real_name','user_options','user_touched', 'user_token' ),
593 array( 'user_id' => $this->mId
), $fname );
595 if ( $s !== false ) {
596 $this->mName
= $s->user_name
;
597 $this->mEmail
= $s->user_email
;
598 $this->mEmailAuthenticated
= wfTimestampOrNull( TS_MW
, $s->user_email_authenticated
);
599 $this->mRealName
= $s->user_real_name
;
600 $this->mPassword
= $s->user_password
;
601 $this->mNewpassword
= $s->user_newpassword
;
602 $this->decodeOptions( $s->user_options
);
603 $this->mTouched
= wfTimestamp(TS_MW
,$s->user_touched
);
604 $this->mToken
= $s->user_token
;
606 $res = $dbr->select( 'user_groups',
608 array( 'ug_user' => $this->mId
),
610 $this->mGroups
= array();
611 while( $row = $dbr->fetchObject( $res ) ) {
612 $this->mGroups
[] = $row->ug_group
;
614 $effectiveGroups = array_merge( array( '*', 'user' ), $this->mGroups
);
615 $this->mRights
= $this->getGroupPermissions( $effectiveGroups );
618 $this->mDataLoaded
= true;
621 function getID() { return $this->mId
; }
622 function setID( $v ) {
624 $this->mDataLoaded
= false;
628 $this->loadFromDatabase();
632 function setName( $str ) {
633 $this->loadFromDatabase();
639 * Return the title dbkey form of the name, for eg user pages.
643 function getTitleKey() {
644 return str_replace( ' ', '_', $this->getName() );
647 function getNewtalk() {
649 $fname = 'User::getNewtalk';
650 $this->loadFromDatabase();
652 # Load the newtalk status if it is unloaded (mNewtalk=-1)
653 if( $this->mNewtalk
== -1 ) {
654 $this->mNewtalk
= 0; # reset talk page status
656 # Check memcached separately for anons, who have no
657 # entire User object stored in there.
659 global $wgDBname, $wgMemc;
660 $key = "$wgDBname:newtalk:ip:{$this->mName}";
661 $newtalk = $wgMemc->get( $key );
662 if( is_integer( $newtalk ) ) {
663 $this->mNewtalk
= $newtalk ?
1 : 0;
664 return (bool)$this->mNewtalk
;
668 $dbr =& wfGetDB( DB_SLAVE
);
669 if ( $wgUseEnotif ) {
670 $res = $dbr->select( 'watchlist',
672 array( 'wl_title' => $this->getTitleKey(),
673 'wl_namespace' => NS_USER_TALK
,
674 'wl_user' => $this->mId
,
675 'wl_notificationtimestamp != 0' ),
676 'User::getNewtalk' );
677 if( $dbr->numRows($res) > 0 ) {
680 $dbr->freeResult( $res );
681 } elseif ( $this->mId
) {
682 $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId
), $fname );
684 if ( $dbr->numRows($res)>0 ) {
687 $dbr->freeResult( $res );
689 $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->mName
), $fname );
690 $this->mNewtalk
= $dbr->numRows( $res ) > 0 ?
1 : 0;
691 $dbr->freeResult( $res );
695 $wgMemc->set( $key, $this->mNewtalk
, time() ); // + 1800 );
699 return ( 0 != $this->mNewtalk
);
702 function setNewtalk( $val ) {
703 $this->loadFromDatabase();
704 $this->mNewtalk
= $val;
705 $this->invalidateCache();
708 function invalidateCache() {
709 global $wgClockSkewFudge;
710 $this->loadFromDatabase();
711 $this->mTouched
= wfTimestamp(TS_MW
, time() +
$wgClockSkewFudge );
712 # Don't forget to save the options after this or
713 # it won't take effect!
716 function validateCache( $timestamp ) {
717 $this->loadFromDatabase();
718 return ($timestamp >= $this->mTouched
);
723 * Will only be salted if $wgPasswordSalt is true
724 * @param string Password.
725 * @return string Salted password or clear password.
727 function addSalt( $p ) {
728 global $wgPasswordSalt;
730 return md5( "{$this->mId}-{$p}" );
736 * Encrypt a password.
737 * It can eventuall salt a password @see User::addSalt()
738 * @param string $p clear Password.
739 * @param string Encrypted password.
741 function encryptPassword( $p ) {
742 return $this->addSalt( md5( $p ) );
745 # Set the password and reset the random token
746 function setPassword( $str ) {
747 $this->loadFromDatabase();
749 $this->mPassword
= $this->encryptPassword( $str );
750 $this->mNewpassword
= '';
753 # Set the random token (used for persistent authentication)
754 function setToken( $token = false ) {
755 global $wgSecretKey, $wgProxyKey, $wgDBname;
757 if ( $wgSecretKey ) {
759 } elseif ( $wgProxyKey ) {
764 $this->mToken
= md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId
);
766 $this->mToken
= $token;
771 function setCookiePassword( $str ) {
772 $this->loadFromDatabase();
773 $this->mCookiePassword
= md5( $str );
776 function setNewpassword( $str ) {
777 $this->loadFromDatabase();
778 $this->mNewpassword
= $this->encryptPassword( $str );
781 function getEmail() {
782 $this->loadFromDatabase();
783 return $this->mEmail
;
786 function getEmailAuthenticationTimestamp() {
787 $this->loadFromDatabase();
788 return $this->mEmailAuthenticated
;
791 function setEmail( $str ) {
792 $this->loadFromDatabase();
793 $this->mEmail
= $str;
796 function getRealName() {
797 $this->loadFromDatabase();
798 return $this->mRealName
;
801 function setRealName( $str ) {
802 $this->loadFromDatabase();
803 $this->mRealName
= $str;
806 function getOption( $oname ) {
807 $this->loadFromDatabase();
808 if ( array_key_exists( $oname, $this->mOptions
) ) {
809 return trim( $this->mOptions
[$oname] );
815 function setOption( $oname, $val ) {
816 $this->loadFromDatabase();
817 if ( $oname == 'skin' ) {
818 # Clear cached skin, so the new one displays immediately in Special:Preferences
819 unset( $this->mSkin
);
821 $this->mOptions
[$oname] = $val;
822 $this->invalidateCache();
825 function getRights() {
826 $this->loadFromDatabase();
827 return $this->mRights
;
831 * Get the list of explicit group memberships this user has.
832 * The implicit * and user groups are not included.
833 * @return array of strings
835 function getGroups() {
836 $this->loadFromDatabase();
837 return $this->mGroups
;
841 * Get the list of implicit group memberships this user has.
842 * This includes all explicit groups, plus 'user' if logged in
843 * and '*' for all accounts.
844 * @return array of strings
846 function getEffectiveGroups() {
847 $base = array( '*' );
848 if( $this->isLoggedIn() ) {
851 return array_merge( $base, $this->getGroups() );
855 * Remove the user from the given group.
856 * This takes immediate effect.
859 function addGroup( $group ) {
860 $dbw =& wfGetDB( DB_MASTER
);
861 $dbw->insert( 'user_groups',
863 'ug_user' => $this->getID(),
864 'ug_group' => $group,
869 $this->mGroups
= array_merge( $this->mGroups
, array( $group ) );
870 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
872 $this->invalidateCache();
873 $this->saveSettings();
877 * Remove the user from the given group.
878 * This takes immediate effect.
881 function removeGroup( $group ) {
882 $dbw =& wfGetDB( DB_MASTER
);
883 $dbw->delete( 'user_groups',
885 'ug_user' => $this->getID(),
886 'ug_group' => $group,
888 'User::removeGroup' );
890 $this->mGroups
= array_diff( $this->mGroups
, array( $group ) );
891 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
893 $this->invalidateCache();
894 $this->saveSettings();
899 * A more legible check for non-anonymousness.
900 * Returns true if the user is not an anonymous visitor.
904 function isLoggedIn() {
905 return( $this->getID() != 0 );
909 * A more legible check for anonymousness.
910 * Returns true if the user is an anonymous visitor.
915 return !$this->isLoggedIn();
919 * Check if a user is sysop
920 * Die with backtrace. Use User:isAllowed() instead.
924 return $this->isAllowed( 'protect' );
928 function isDeveloper() {
929 return $this->isAllowed( 'siteadmin' );
933 function isBureaucrat() {
934 return $this->isAllowed( 'makesysop' );
938 * Whether the user is a bot
939 * @todo need to be migrated to the new user level management sytem
942 $this->loadFromDatabase();
943 return in_array( 'bot', $this->mRights
);
947 * Check if user is allowed to access a feature / make an action
948 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
949 * @return boolean True: action is allowed, False: action should not be allowed
951 function isAllowed($action='') {
952 $this->loadFromDatabase();
953 return in_array( $action , $this->mRights
);
957 * Load a skin if it doesn't exist or return it
958 * @todo FIXME : need to check the old failback system [AV]
960 function &getSkin() {
962 if ( ! isset( $this->mSkin
) ) {
963 $fname = 'User::getSkin';
964 wfProfileIn( $fname );
966 # get all skin names available
967 $skinNames = Skin
::getSkinNames();
970 $userSkin = $this->getOption( 'skin' );
971 if ( $userSkin == '' ) { $userSkin = 'standard'; }
973 if ( !isset( $skinNames[$userSkin] ) ) {
974 # in case the user skin could not be found find a replacement
979 # if phptal is enabled we should have monobook skin that
980 # superseed the good old SkinStandard.
981 if ( isset( $skinNames['monobook'] ) ) {
982 $fallback[0] = 'MonoBook';
985 if(is_numeric($userSkin) && isset( $fallback[$userSkin]) ){
986 $sn = $fallback[$userSkin];
991 # The user skin is available
992 $sn = $skinNames[$userSkin];
995 # Grab the skin class and initialise it. Each skin checks for PHPTal
996 # and will not load if it's not enabled.
997 require_once( $IP.'/skins/'.$sn.'.php' );
999 # Check if we got if not failback to default skin
1000 $className = 'Skin'.$sn;
1001 if( !class_exists( $className ) ) {
1002 # DO NOT die if the class isn't found. This breaks maintenance
1003 # scripts and can cause a user account to be unrecoverable
1004 # except by SQL manipulation if a previously valid skin name
1005 # is no longer valid.
1006 $className = 'SkinStandard';
1007 require_once( $IP.'/skins/Standard.php' );
1009 $this->mSkin
=& new $className;
1010 wfProfileOut( $fname );
1012 return $this->mSkin
;
1016 * @param string $title Article title to look at
1020 * Check watched status of an article
1021 * @return bool True if article is watched
1023 function isWatched( $title ) {
1024 $wl = WatchedItem
::fromUserTitle( $this, $title );
1025 return $wl->isWatched();
1031 function addWatch( $title ) {
1032 $wl = WatchedItem
::fromUserTitle( $this, $title );
1034 $this->invalidateCache();
1038 * Stop watching an article
1040 function removeWatch( $title ) {
1041 $wl = WatchedItem
::fromUserTitle( $this, $title );
1043 $this->invalidateCache();
1047 * Clear the user's notification timestamp for the given title.
1048 * If e-notif e-mails are on, they will receive notification mails on
1049 * the next change of the page if it's watched etc.
1051 function clearNotification( &$title ) {
1052 global $wgUser, $wgUseEnotif;
1054 if ( !$wgUseEnotif ) {
1058 $userid = $this->getID();
1063 // Only update the timestamp if the page is being watched.
1064 // The query to find out if it is watched is cached both in memcached and per-invocation,
1065 // and when it does have to be executed, it can be on a slave
1066 // If this is the user's newtalk page, we always update the timestamp
1067 if ($title->getNamespace() == NS_USER_TALK
&&
1068 $title->getText() == $wgUser->getName())
1071 } elseif ( $this->getID() == $wgUser->getID() ) {
1072 $watched = $title->userIsWatching();
1077 // If the page is watched by the user (or may be watched), update the timestamp on any
1078 // any matching rows
1080 $dbw =& wfGetDB( DB_MASTER
);
1081 $success = $dbw->update( 'watchlist',
1083 'wl_notificationtimestamp' => 0
1084 ), array( /* WHERE */
1085 'wl_title' => $title->getDBkey(),
1086 'wl_namespace' => $title->getNamespace(),
1087 'wl_user' => $this->getID()
1088 ), 'User::clearLastVisited'
1096 * Resets all of the given user's page-change notification timestamps.
1097 * If e-notif e-mails are on, they will receive notification mails on
1098 * the next change of any watched page.
1100 * @param int $currentUser user ID number
1103 function clearAllNotifications( $currentUser ) {
1104 global $wgUseEnotif;
1105 if ( !$wgUseEnotif ) {
1108 if( $currentUser != 0 ) {
1110 $dbw =& wfGetDB( DB_MASTER
);
1111 $success = $dbw->update( 'watchlist',
1113 'wl_notificationtimestamp' => 0
1114 ), array( /* WHERE */
1115 'wl_user' => $currentUser
1116 ), 'UserMailer::clearAll'
1119 # we also need to clear here the "you have new message" notification for the own user_talk page
1120 # This is cleared one page view later in Article::viewUpdates();
1126 * @return string Encoding options
1128 function encodeOptions() {
1130 foreach ( $this->mOptions
as $oname => $oval ) {
1131 array_push( $a, $oname.'='.$oval );
1133 $s = implode( "\n", $a );
1140 function decodeOptions( $str ) {
1141 $a = explode( "\n", $str );
1142 foreach ( $a as $s ) {
1143 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1144 $this->mOptions
[$m[1]] = $m[2];
1149 function setCookies() {
1150 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgDBname;
1151 if ( 0 == $this->mId
) return;
1152 $this->loadFromDatabase();
1153 $exp = time() +
$wgCookieExpiration;
1155 $_SESSION['wsUserID'] = $this->mId
;
1156 setcookie( $wgDBname.'UserID', $this->mId
, $exp, $wgCookiePath, $wgCookieDomain );
1158 $_SESSION['wsUserName'] = $this->mName
;
1159 setcookie( $wgDBname.'UserName', $this->mName
, $exp, $wgCookiePath, $wgCookieDomain );
1161 $_SESSION['wsToken'] = $this->mToken
;
1162 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1163 setcookie( $wgDBname.'Token', $this->mToken
, $exp, $wgCookiePath, $wgCookieDomain );
1165 setcookie( $wgDBname.'Token', '', time() - 3600 );
1171 * It will clean the session cookie
1174 global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP;
1175 $this->loadDefaults();
1176 $this->setLoaded( true );
1178 $_SESSION['wsUserID'] = 0;
1180 setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1181 setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1183 # Remember when user logged out, to prevent seeing cached pages
1184 setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() +
86400, $wgCookiePath, $wgCookieDomain );
1188 * Save object settings into database
1190 function saveSettings() {
1191 global $wgMemc, $wgDBname, $wgUseEnotif;
1192 $fname = 'User::saveSettings';
1194 if ( wfReadOnly() ) { return; }
1195 $this->saveNewtalk();
1196 if ( 0 == $this->mId
) { return; }
1198 $dbw =& wfGetDB( DB_MASTER
);
1199 $dbw->update( 'user',
1201 'user_name' => $this->mName
,
1202 'user_password' => $this->mPassword
,
1203 'user_newpassword' => $this->mNewpassword
,
1204 'user_real_name' => $this->mRealName
,
1205 'user_email' => $this->mEmail
,
1206 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1207 'user_options' => $this->encodeOptions(),
1208 'user_touched' => $dbw->timestamp($this->mTouched
),
1209 'user_token' => $this->mToken
1210 ), array( /* WHERE */
1211 'user_id' => $this->mId
1214 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1218 * Save value of new talk flag.
1220 function saveNewtalk() {
1221 global $wgDBname, $wgMemc, $wgUseEnotif;
1223 $fname = 'User::saveNewtalk';
1225 if ( wfReadOnly() ) { return ; }
1226 $dbr =& wfGetDB( DB_SLAVE
);
1227 $dbw =& wfGetDB( DB_MASTER
);
1228 if ( $wgUseEnotif ) {
1229 if ( ! $this->getNewtalk() ) {
1230 # Delete the watchlist entry for user_talk page X watched by user X
1231 $dbw->delete( 'watchlist',
1232 array( 'wl_user' => $this->mId
,
1233 'wl_title' => $this->getTitleKey(),
1234 'wl_namespace' => NS_USER_TALK
),
1236 if ( $dbw->affectedRows() ) {
1240 # Anon users have a separate memcache space for newtalk
1241 # since they don't store their own info. Trim...
1242 $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" );
1246 if ($this->getID() != 0) {
1248 $value = $this->getID();
1252 $value = $this->mName
;
1253 $key = "$wgDBname:newtalk:ip:$this->mName";
1256 $dbr =& wfGetDB( DB_SLAVE
);
1257 $dbw =& wfGetDB( DB_MASTER
);
1259 $res = $dbr->selectField('user_newtalk', $field,
1260 array($field => $value), $fname);
1263 if ($res !== false && $this->mNewtalk
== 0) {
1264 $dbw->delete('user_newtalk', array($field => $value), $fname);
1266 $wgMemc->set( $key, 0 );
1268 } else if ($res === false && $this->mNewtalk
== 1) {
1269 $dbw->insert('user_newtalk', array($field => $value), $fname);
1271 $wgMemc->set( $key, 1 );
1278 # Update user_touched, so that newtalk notifications in the client cache are invalidated
1279 if ( $changed && $this->getID() ) {
1280 $dbw->update('user',
1281 /*SET*/ array( 'user_touched' => $this->mTouched
),
1282 /*WHERE*/ array( 'user_id' => $this->getID() ),
1284 $wgMemc->set( "$wgDBname:user:id:{$this->mId}", $this, 86400 );
1289 * Checks if a user with the given name exists, returns the ID
1291 function idForName() {
1292 $fname = 'User::idForName';
1295 $s = trim( $this->mName
);
1296 if ( 0 == strcmp( '', $s ) ) return 0;
1298 $dbr =& wfGetDB( DB_SLAVE
);
1299 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1300 if ( $id === false ) {
1307 * Add user object to the database
1309 function addToDatabase() {
1310 $fname = 'User::addToDatabase';
1311 $dbw =& wfGetDB( DB_MASTER
);
1312 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1313 $dbw->insert( 'user',
1315 'user_id' => $seqVal,
1316 'user_name' => $this->mName
,
1317 'user_password' => $this->mPassword
,
1318 'user_newpassword' => $this->mNewpassword
,
1319 'user_email' => $this->mEmail
,
1320 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1321 'user_real_name' => $this->mRealName
,
1322 'user_options' => $this->encodeOptions(),
1323 'user_token' => $this->mToken
1326 $this->mId
= $dbw->insertId();
1329 function spreadBlock() {
1331 # If the (non-anonymous) user is blocked, this function will block any IP address
1332 # that they successfully log on from.
1333 $fname = 'User::spreadBlock';
1335 wfDebug( "User:spreadBlock()\n" );
1336 if ( $this->mId
== 0 ) {
1340 $userblock = Block
::newFromDB( '', $this->mId
);
1341 if ( !$userblock->isValid() ) {
1345 # Check if this IP address is already blocked
1346 $ipblock = Block
::newFromDB( $wgIP );
1347 if ( $ipblock->isValid() ) {
1348 # Just update the timestamp
1349 $ipblock->updateTimestamp();
1353 # Make a new block object with the desired properties
1354 wfDebug( "Autoblocking {$this->mName}@{$wgIP}\n" );
1355 $ipblock->mAddress
= $wgIP;
1356 $ipblock->mUser
= 0;
1357 $ipblock->mBy
= $userblock->mBy
;
1358 $ipblock->mReason
= wfMsg( 'autoblocker', $this->getName(), $userblock->mReason
);
1359 $ipblock->mTimestamp
= wfTimestampNow();
1360 $ipblock->mAuto
= 1;
1361 # If the user is already blocked with an expiry date, we don't
1362 # want to pile on top of that!
1363 if($userblock->mExpiry
) {
1364 $ipblock->mExpiry
= min ( $userblock->mExpiry
, Block
::getAutoblockExpiry( $ipblock->mTimestamp
));
1366 $ipblock->mExpiry
= Block
::getAutoblockExpiry( $ipblock->mTimestamp
);
1374 function getPageRenderingHash() {
1377 return $this->mHash
;
1380 // stubthreshold is only included below for completeness,
1381 // it will always be 0 when this function is called by parsercache.
1383 $confstr = $this->getOption( 'math' );
1384 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1385 $confstr .= '!' . $this->getOption( 'editsection' );
1386 $confstr .= '!' . $this->getOption( 'date' );
1387 $confstr .= '!' . $this->getOption( 'numberheadings' );
1388 $confstr .= '!' . $this->getOption( 'language' );
1389 $confstr .= '!' . $this->getOption( 'thumbsize' );
1390 // add in language specific options, if any
1391 $extra = $wgContLang->getExtraHashOptions();
1394 $this->mHash
= $confstr;
1398 function isAllowedToCreateAccount() {
1399 return $this->isAllowed( 'createaccount' );
1403 * Set mDataLoaded, return previous value
1404 * Use this to prevent DB access in command-line scripts or similar situations
1406 function setLoaded( $loaded ) {
1407 return wfSetVar( $this->mDataLoaded
, $loaded );
1411 * Get this user's personal page title.
1416 function getUserPage() {
1417 return Title
::makeTitle( NS_USER
, $this->mName
);
1421 * Get this user's talk page title.
1426 function getTalkPage() {
1427 $title = $this->getUserPage();
1428 return $title->getTalkPage();
1434 function getMaxID() {
1435 $dbr =& wfGetDB( DB_SLAVE
);
1436 return $dbr->selectField( 'user', 'max(user_id)', false );
1440 * Determine whether the user is a newbie. Newbies are either
1441 * anonymous IPs, or the 1% most recently created accounts.
1442 * Bots and sysops are excluded.
1443 * @return bool True if it is a newbie.
1445 function isNewbie() {
1446 return $this->isAnon() ||
$this->mId
> User
::getMaxID() * 0.99 && !$this->isAllowed( 'delete' ) && !$this->isBot();
1450 * Check to see if the given clear-text password is one of the accepted passwords
1451 * @param string $password User password.
1452 * @return bool True if the given password is correct otherwise False.
1454 function checkPassword( $password ) {
1455 global $wgAuth, $wgMinimalPasswordLength;
1456 $this->loadFromDatabase();
1458 // Even though we stop people from creating passwords that
1459 // are shorter than this, doesn't mean people wont be able
1460 // to. Certain authentication plugins do NOT want to save
1461 // domain passwords in a mysql database, so we should
1462 // check this (incase $wgAuth->strict() is false).
1463 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1467 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1469 } elseif( $wgAuth->strict() ) {
1470 /* Auth plugin doesn't allow local authentication */
1473 $ep = $this->encryptPassword( $password );
1474 if ( 0 == strcmp( $ep, $this->mPassword
) ) {
1476 } elseif ( ($this->mNewpassword
!= '') && (0 == strcmp( $ep, $this->mNewpassword
)) ) {
1478 } elseif ( function_exists( 'iconv' ) ) {
1479 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1480 # Check for this with iconv
1481 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1482 if ( 0 == strcmp( $cp1252hash, $this->mPassword
) ) {
1490 * Initialize (if necessary) and return a session token value
1491 * which can be used in edit forms to show that the user's
1492 * login credentials aren't being hijacked with a foreign form
1495 * @param mixed $salt - Optional function-specific data for hash.
1496 * Use a string or an array of strings.
1500 function editToken( $salt = '' ) {
1501 if( !isset( $_SESSION['wsEditToken'] ) ) {
1502 $token = $this->generateToken();
1503 $_SESSION['wsEditToken'] = $token;
1505 $token = $_SESSION['wsEditToken'];
1507 if( is_array( $salt ) ) {
1508 $salt = implode( '|', $salt );
1510 return md5( $token . $salt );
1514 * Generate a hex-y looking random token for various uses.
1515 * Could be made more cryptographically sure if someone cares.
1518 function generateToken( $salt = '' ) {
1519 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1520 return md5( $token . $salt );
1524 * Check given value against the token value stored in the session.
1525 * A match should confirm that the form was submitted from the
1526 * user's own login session, not a form submission from a third-party
1529 * @param string $val - the input value to compare
1530 * @param string $salt - Optional function-specific data for hash
1534 function matchEditToken( $val, $salt = '' ) {
1535 return ( $val == $this->editToken( $salt ) );
1539 * Generate a new e-mail confirmation token and send a confirmation
1540 * mail to the user's given address.
1542 * @return mixed True on success, a WikiError object on failure.
1544 function sendConfirmationMail() {
1545 global $wgIP, $wgContLang;
1546 $url = $this->confirmationTokenUrl( $expiration );
1547 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1548 wfMsg( 'confirmemail_body',
1552 $wgContLang->timeanddate( $expiration, false ) ) );
1556 * Send an e-mail to this user's account. Does not check for
1557 * confirmed status or validity.
1559 * @param string $subject
1560 * @param string $body
1561 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1562 * @return mixed True on success, a WikiError object on failure.
1564 function sendMail( $subject, $body, $from = null ) {
1565 if( is_null( $from ) ) {
1566 global $wgPasswordSender;
1567 $from = $wgPasswordSender;
1570 require_once( 'UserMailer.php' );
1571 $error = userMailer( $this->getEmail(), $from, $subject, $body );
1573 if( $error == '' ) {
1576 return new WikiError( $error );
1581 * Generate, store, and return a new e-mail confirmation code.
1582 * A hash (unsalted since it's used as a key) is stored.
1583 * @param &$expiration mixed output: accepts the expiration time
1587 function confirmationToken( &$expiration ) {
1588 $fname = 'User::confirmationToken';
1591 $expires = $now +
7 * 24 * 60 * 60;
1592 $expiration = wfTimestamp( TS_MW
, $expires );
1594 $token = $this->generateToken( $this->mId
. $this->mEmail
. $expires );
1595 $hash = md5( $token );
1597 $dbw =& wfGetDB( DB_MASTER
);
1598 $dbw->update( 'user',
1599 array( 'user_email_token' => $hash,
1600 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1601 array( 'user_id' => $this->mId
),
1608 * Generate and store a new e-mail confirmation token, and return
1609 * the URL the user can use to confirm.
1610 * @param &$expiration mixed output: accepts the expiration time
1614 function confirmationTokenUrl( &$expiration ) {
1615 $token = $this->confirmationToken( $expiration );
1616 $title = Title
::makeTitle( NS_SPECIAL
, 'Confirmemail/' . $token );
1617 return $title->getFullUrl();
1621 * Mark the e-mail address confirmed and save.
1623 function confirmEmail() {
1624 $this->loadFromDatabase();
1625 $this->mEmailAuthenticated
= wfTimestampNow();
1626 $this->saveSettings();
1631 * Is this user allowed to send e-mails within limits of current
1632 * site configuration?
1635 function canSendEmail() {
1636 return $this->isEmailConfirmed();
1640 * Is this user allowed to receive e-mails within limits of current
1641 * site configuration?
1644 function canReceiveEmail() {
1645 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1649 * Is this user's e-mail address valid-looking and confirmed within
1650 * limits of the current site configuration?
1652 * If $wgEmailAuthentication is on, this may require the user to have
1653 * confirmed their address by returning a code or using a password
1654 * sent to the address from the wiki.
1658 function isEmailConfirmed() {
1659 global $wgEmailAuthentication;
1660 $this->loadFromDatabase();
1661 if( $this->isAnon() )
1663 if( !$this->isValidEmailAddr( $this->mEmail
) )
1665 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1671 * @param array $groups list of groups
1672 * @return array list of permission key names for given groups combined
1675 function getGroupPermissions( $groups ) {
1676 global $wgGroupPermissions;
1678 foreach( $groups as $group ) {
1679 if( isset( $wgGroupPermissions[$group] ) ) {
1680 $rights = array_merge( $rights,
1681 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1688 * @param string $group key name
1689 * @return string localized descriptive name, if provided
1692 function getGroupName( $group ) {
1693 $key = "group-$group-name";
1694 $name = wfMsg( $key );
1695 if( $name == '' ||
$name == "<$key>" ) {
1703 * Return the set of defined explicit groups.
1704 * The * and 'user' groups are not included.
1708 function getAllGroups() {
1709 global $wgGroupPermissions;
1711 array_keys( $wgGroupPermissions ),
1712 array( '*', 'user' ) );