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( !is_object( $user ) ||
$user->mVersion
< MW_USER_VERSION
) {
535 # Expire old serialized objects; they may be corrupt.
538 if($makenew = !$user) {
539 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
542 $user->loadFromDatabase();
544 wfDebug( "User::loadFromSession() got from cache!\n" );
547 if ( isset( $_SESSION['wsToken'] ) ) {
548 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken
;
549 } else if ( isset( $_COOKIE["{$wgDBname}Token"] ) ) {
550 $passwordCorrect = $user->mToken
== $_COOKIE["{$wgDBname}Token"];
552 return new User(); # Can't log in from session
555 if ( ( $sName == $user->mName
) && $passwordCorrect ) {
557 if($wgMemc->set( $key, $user ))
558 wfDebug( "User::loadFromSession() successfully saved user\n" );
560 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
564 return new User(); # Can't log in from session
568 * Load a user from the database
570 function loadFromDatabase() {
571 global $wgCommandLineMode;
572 $fname = "User::loadFromDatabase";
574 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
575 # loading in a command line script, don't assume all command line scripts need it like this
576 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
577 if ( $this->mDataLoaded
) {
582 $this->mId
= IntVal( $this->mId
);
584 /** Anonymous user */
587 $this->mRights
= $this->getGroupPermissions( array( '*' ) );
588 $this->mDataLoaded
= true;
590 } # the following stuff is for non-anonymous users only
592 $dbr =& wfGetDB( DB_SLAVE
);
593 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
594 'user_email_authenticated',
595 'user_real_name','user_options','user_touched', 'user_token' ),
596 array( 'user_id' => $this->mId
), $fname );
598 if ( $s !== false ) {
599 $this->mName
= $s->user_name
;
600 $this->mEmail
= $s->user_email
;
601 $this->mEmailAuthenticated
= wfTimestampOrNull( TS_MW
, $s->user_email_authenticated
);
602 $this->mRealName
= $s->user_real_name
;
603 $this->mPassword
= $s->user_password
;
604 $this->mNewpassword
= $s->user_newpassword
;
605 $this->decodeOptions( $s->user_options
);
606 $this->mTouched
= wfTimestamp(TS_MW
,$s->user_touched
);
607 $this->mToken
= $s->user_token
;
609 $res = $dbr->select( 'user_groups',
611 array( 'ug_user' => $this->mId
),
613 $this->mGroups
= array();
614 while( $row = $dbr->fetchObject( $res ) ) {
615 $this->mGroups
[] = $row->ug_group
;
617 $effectiveGroups = array_merge( array( '*', 'user' ), $this->mGroups
);
618 $this->mRights
= $this->getGroupPermissions( $effectiveGroups );
621 $this->mDataLoaded
= true;
624 function getID() { return $this->mId
; }
625 function setID( $v ) {
627 $this->mDataLoaded
= false;
631 $this->loadFromDatabase();
635 function setName( $str ) {
636 $this->loadFromDatabase();
642 * Return the title dbkey form of the name, for eg user pages.
646 function getTitleKey() {
647 return str_replace( ' ', '_', $this->getName() );
650 function getNewtalk() {
652 $fname = 'User::getNewtalk';
653 $this->loadFromDatabase();
655 # Load the newtalk status if it is unloaded (mNewtalk=-1)
656 if( $this->mNewtalk
== -1 ) {
657 $this->mNewtalk
= 0; # reset talk page status
659 # Check memcached separately for anons, who have no
660 # entire User object stored in there.
662 global $wgDBname, $wgMemc;
663 $key = "$wgDBname:newtalk:ip:{$this->mName}";
664 $newtalk = $wgMemc->get( $key );
665 if( is_integer( $newtalk ) ) {
666 $this->mNewtalk
= $newtalk ?
1 : 0;
667 return (bool)$this->mNewtalk
;
671 $dbr =& wfGetDB( DB_SLAVE
);
672 if ( $wgUseEnotif ) {
673 $res = $dbr->select( 'watchlist',
675 array( 'wl_title' => $this->getTitleKey(),
676 'wl_namespace' => NS_USER_TALK
,
677 'wl_user' => $this->mId
,
678 'wl_notificationtimestamp != 0' ),
679 'User::getNewtalk' );
680 if( $dbr->numRows($res) > 0 ) {
683 $dbr->freeResult( $res );
684 } elseif ( $this->mId
) {
685 $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId
), $fname );
687 if ( $dbr->numRows($res)>0 ) {
690 $dbr->freeResult( $res );
692 $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->mName
), $fname );
693 $this->mNewtalk
= $dbr->numRows( $res ) > 0 ?
1 : 0;
694 $dbr->freeResult( $res );
698 $wgMemc->set( $key, $this->mNewtalk
, time() ); // + 1800 );
702 return ( 0 != $this->mNewtalk
);
705 function setNewtalk( $val ) {
706 $this->loadFromDatabase();
707 $this->mNewtalk
= $val;
708 $this->invalidateCache();
711 function invalidateCache() {
712 global $wgClockSkewFudge;
713 $this->loadFromDatabase();
714 $this->mTouched
= wfTimestamp(TS_MW
, time() +
$wgClockSkewFudge );
715 # Don't forget to save the options after this or
716 # it won't take effect!
719 function validateCache( $timestamp ) {
720 $this->loadFromDatabase();
721 return ($timestamp >= $this->mTouched
);
726 * Will only be salted if $wgPasswordSalt is true
727 * @param string Password.
728 * @return string Salted password or clear password.
730 function addSalt( $p ) {
731 global $wgPasswordSalt;
733 return md5( "{$this->mId}-{$p}" );
739 * Encrypt a password.
740 * It can eventuall salt a password @see User::addSalt()
741 * @param string $p clear Password.
742 * @param string Encrypted password.
744 function encryptPassword( $p ) {
745 return $this->addSalt( md5( $p ) );
748 # Set the password and reset the random token
749 function setPassword( $str ) {
750 $this->loadFromDatabase();
752 $this->mPassword
= $this->encryptPassword( $str );
753 $this->mNewpassword
= '';
756 # Set the random token (used for persistent authentication)
757 function setToken( $token = false ) {
758 global $wgSecretKey, $wgProxyKey, $wgDBname;
760 if ( $wgSecretKey ) {
762 } elseif ( $wgProxyKey ) {
767 $this->mToken
= md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId
);
769 $this->mToken
= $token;
774 function setCookiePassword( $str ) {
775 $this->loadFromDatabase();
776 $this->mCookiePassword
= md5( $str );
779 function setNewpassword( $str ) {
780 $this->loadFromDatabase();
781 $this->mNewpassword
= $this->encryptPassword( $str );
784 function getEmail() {
785 $this->loadFromDatabase();
786 return $this->mEmail
;
789 function getEmailAuthenticationTimestamp() {
790 $this->loadFromDatabase();
791 return $this->mEmailAuthenticated
;
794 function setEmail( $str ) {
795 $this->loadFromDatabase();
796 $this->mEmail
= $str;
799 function getRealName() {
800 $this->loadFromDatabase();
801 return $this->mRealName
;
804 function setRealName( $str ) {
805 $this->loadFromDatabase();
806 $this->mRealName
= $str;
809 function getOption( $oname ) {
810 $this->loadFromDatabase();
811 if ( array_key_exists( $oname, $this->mOptions
) ) {
812 return trim( $this->mOptions
[$oname] );
818 function setOption( $oname, $val ) {
819 $this->loadFromDatabase();
820 if ( $oname == 'skin' ) {
821 # Clear cached skin, so the new one displays immediately in Special:Preferences
822 unset( $this->mSkin
);
824 $this->mOptions
[$oname] = $val;
825 $this->invalidateCache();
828 function getRights() {
829 $this->loadFromDatabase();
830 return $this->mRights
;
834 * Get the list of explicit group memberships this user has.
835 * The implicit * and user groups are not included.
836 * @return array of strings
838 function getGroups() {
839 $this->loadFromDatabase();
840 return $this->mGroups
;
844 * Get the list of implicit group memberships this user has.
845 * This includes all explicit groups, plus 'user' if logged in
846 * and '*' for all accounts.
847 * @return array of strings
849 function getEffectiveGroups() {
850 $base = array( '*' );
851 if( $this->isLoggedIn() ) {
854 return array_merge( $base, $this->getGroups() );
858 * Remove the user from the given group.
859 * This takes immediate effect.
862 function addGroup( $group ) {
863 $dbw =& wfGetDB( DB_MASTER
);
864 $dbw->insert( 'user_groups',
866 'ug_user' => $this->getID(),
867 'ug_group' => $group,
872 $this->mGroups
= array_merge( $this->mGroups
, array( $group ) );
873 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
875 $this->invalidateCache();
876 $this->saveSettings();
880 * Remove the user from the given group.
881 * This takes immediate effect.
884 function removeGroup( $group ) {
885 $dbw =& wfGetDB( DB_MASTER
);
886 $dbw->delete( 'user_groups',
888 'ug_user' => $this->getID(),
889 'ug_group' => $group,
891 'User::removeGroup' );
893 $this->mGroups
= array_diff( $this->mGroups
, array( $group ) );
894 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
896 $this->invalidateCache();
897 $this->saveSettings();
902 * A more legible check for non-anonymousness.
903 * Returns true if the user is not an anonymous visitor.
907 function isLoggedIn() {
908 return( $this->getID() != 0 );
912 * A more legible check for anonymousness.
913 * Returns true if the user is an anonymous visitor.
918 return !$this->isLoggedIn();
922 * Check if a user is sysop
923 * Die with backtrace. Use User:isAllowed() instead.
927 return $this->isAllowed( 'protect' );
931 function isDeveloper() {
932 return $this->isAllowed( 'siteadmin' );
936 function isBureaucrat() {
937 return $this->isAllowed( 'makesysop' );
941 * Whether the user is a bot
942 * @todo need to be migrated to the new user level management sytem
945 $this->loadFromDatabase();
946 return in_array( 'bot', $this->mRights
);
950 * Check if user is allowed to access a feature / make an action
951 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
952 * @return boolean True: action is allowed, False: action should not be allowed
954 function isAllowed($action='') {
955 $this->loadFromDatabase();
956 return in_array( $action , $this->mRights
);
960 * Load a skin if it doesn't exist or return it
961 * @todo FIXME : need to check the old failback system [AV]
963 function &getSkin() {
965 if ( ! isset( $this->mSkin
) ) {
966 $fname = 'User::getSkin';
967 wfProfileIn( $fname );
969 # get all skin names available
970 $skinNames = Skin
::getSkinNames();
973 $userSkin = $this->getOption( 'skin' );
974 if ( $userSkin == '' ) { $userSkin = 'standard'; }
976 if ( !isset( $skinNames[$userSkin] ) ) {
977 # in case the user skin could not be found find a replacement
982 # if phptal is enabled we should have monobook skin that
983 # superseed the good old SkinStandard.
984 if ( isset( $skinNames['monobook'] ) ) {
985 $fallback[0] = 'MonoBook';
988 if(is_numeric($userSkin) && isset( $fallback[$userSkin]) ){
989 $sn = $fallback[$userSkin];
994 # The user skin is available
995 $sn = $skinNames[$userSkin];
998 # Grab the skin class and initialise it. Each skin checks for PHPTal
999 # and will not load if it's not enabled.
1000 require_once( $IP.'/skins/'.$sn.'.php' );
1002 # Check if we got if not failback to default skin
1003 $className = 'Skin'.$sn;
1004 if( !class_exists( $className ) ) {
1005 # DO NOT die if the class isn't found. This breaks maintenance
1006 # scripts and can cause a user account to be unrecoverable
1007 # except by SQL manipulation if a previously valid skin name
1008 # is no longer valid.
1009 $className = 'SkinStandard';
1010 require_once( $IP.'/skins/Standard.php' );
1012 $this->mSkin
=& new $className;
1013 wfProfileOut( $fname );
1015 return $this->mSkin
;
1019 * @param string $title Article title to look at
1023 * Check watched status of an article
1024 * @return bool True if article is watched
1026 function isWatched( $title ) {
1027 $wl = WatchedItem
::fromUserTitle( $this, $title );
1028 return $wl->isWatched();
1034 function addWatch( $title ) {
1035 $wl = WatchedItem
::fromUserTitle( $this, $title );
1037 $this->invalidateCache();
1041 * Stop watching an article
1043 function removeWatch( $title ) {
1044 $wl = WatchedItem
::fromUserTitle( $this, $title );
1046 $this->invalidateCache();
1050 * Clear the user's notification timestamp for the given title.
1051 * If e-notif e-mails are on, they will receive notification mails on
1052 * the next change of the page if it's watched etc.
1054 function clearNotification( &$title ) {
1055 global $wgUser, $wgUseEnotif;
1057 if ( !$wgUseEnotif ) {
1061 $userid = $this->getID();
1066 // Only update the timestamp if the page is being watched.
1067 // The query to find out if it is watched is cached both in memcached and per-invocation,
1068 // and when it does have to be executed, it can be on a slave
1069 // If this is the user's newtalk page, we always update the timestamp
1070 if ($title->getNamespace() == NS_USER_TALK
&&
1071 $title->getText() == $wgUser->getName())
1074 } elseif ( $this->getID() == $wgUser->getID() ) {
1075 $watched = $title->userIsWatching();
1080 // If the page is watched by the user (or may be watched), update the timestamp on any
1081 // any matching rows
1083 $dbw =& wfGetDB( DB_MASTER
);
1084 $success = $dbw->update( 'watchlist',
1086 'wl_notificationtimestamp' => 0
1087 ), array( /* WHERE */
1088 'wl_title' => $title->getDBkey(),
1089 'wl_namespace' => $title->getNamespace(),
1090 'wl_user' => $this->getID()
1091 ), 'User::clearLastVisited'
1099 * Resets all of the given user's page-change notification timestamps.
1100 * If e-notif e-mails are on, they will receive notification mails on
1101 * the next change of any watched page.
1103 * @param int $currentUser user ID number
1106 function clearAllNotifications( $currentUser ) {
1107 global $wgUseEnotif;
1108 if ( !$wgUseEnotif ) {
1111 if( $currentUser != 0 ) {
1113 $dbw =& wfGetDB( DB_MASTER
);
1114 $success = $dbw->update( 'watchlist',
1116 'wl_notificationtimestamp' => 0
1117 ), array( /* WHERE */
1118 'wl_user' => $currentUser
1119 ), 'UserMailer::clearAll'
1122 # we also need to clear here the "you have new message" notification for the own user_talk page
1123 # This is cleared one page view later in Article::viewUpdates();
1129 * @return string Encoding options
1131 function encodeOptions() {
1133 foreach ( $this->mOptions
as $oname => $oval ) {
1134 array_push( $a, $oname.'='.$oval );
1136 $s = implode( "\n", $a );
1143 function decodeOptions( $str ) {
1144 $a = explode( "\n", $str );
1145 foreach ( $a as $s ) {
1146 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1147 $this->mOptions
[$m[1]] = $m[2];
1152 function setCookies() {
1153 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgDBname;
1154 if ( 0 == $this->mId
) return;
1155 $this->loadFromDatabase();
1156 $exp = time() +
$wgCookieExpiration;
1158 $_SESSION['wsUserID'] = $this->mId
;
1159 setcookie( $wgDBname.'UserID', $this->mId
, $exp, $wgCookiePath, $wgCookieDomain );
1161 $_SESSION['wsUserName'] = $this->mName
;
1162 setcookie( $wgDBname.'UserName', $this->mName
, $exp, $wgCookiePath, $wgCookieDomain );
1164 $_SESSION['wsToken'] = $this->mToken
;
1165 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1166 setcookie( $wgDBname.'Token', $this->mToken
, $exp, $wgCookiePath, $wgCookieDomain );
1168 setcookie( $wgDBname.'Token', '', time() - 3600 );
1174 * It will clean the session cookie
1177 global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP;
1178 $this->loadDefaults();
1179 $this->setLoaded( true );
1181 $_SESSION['wsUserID'] = 0;
1183 setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1184 setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1186 # Remember when user logged out, to prevent seeing cached pages
1187 setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() +
86400, $wgCookiePath, $wgCookieDomain );
1191 * Save object settings into database
1193 function saveSettings() {
1194 global $wgMemc, $wgDBname, $wgUseEnotif;
1195 $fname = 'User::saveSettings';
1197 if ( wfReadOnly() ) { return; }
1198 $this->saveNewtalk();
1199 if ( 0 == $this->mId
) { return; }
1201 $dbw =& wfGetDB( DB_MASTER
);
1202 $dbw->update( 'user',
1204 'user_name' => $this->mName
,
1205 'user_password' => $this->mPassword
,
1206 'user_newpassword' => $this->mNewpassword
,
1207 'user_real_name' => $this->mRealName
,
1208 'user_email' => $this->mEmail
,
1209 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1210 'user_options' => $this->encodeOptions(),
1211 'user_touched' => $dbw->timestamp($this->mTouched
),
1212 'user_token' => $this->mToken
1213 ), array( /* WHERE */
1214 'user_id' => $this->mId
1217 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1221 * Save value of new talk flag.
1223 function saveNewtalk() {
1224 global $wgDBname, $wgMemc, $wgUseEnotif;
1226 $fname = 'User::saveNewtalk';
1228 if ( wfReadOnly() ) { return ; }
1229 $dbr =& wfGetDB( DB_SLAVE
);
1230 $dbw =& wfGetDB( DB_MASTER
);
1231 if ( $wgUseEnotif ) {
1232 if ( ! $this->getNewtalk() ) {
1233 # Delete the watchlist entry for user_talk page X watched by user X
1234 $dbw->delete( 'watchlist',
1235 array( 'wl_user' => $this->mId
,
1236 'wl_title' => $this->getTitleKey(),
1237 'wl_namespace' => NS_USER_TALK
),
1239 if ( $dbw->affectedRows() ) {
1243 # Anon users have a separate memcache space for newtalk
1244 # since they don't store their own info. Trim...
1245 $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" );
1249 if ($this->getID() != 0) {
1251 $value = $this->getID();
1255 $value = $this->mName
;
1256 $key = "$wgDBname:newtalk:ip:$this->mName";
1259 $dbr =& wfGetDB( DB_SLAVE
);
1260 $dbw =& wfGetDB( DB_MASTER
);
1262 $res = $dbr->selectField('user_newtalk', $field,
1263 array($field => $value), $fname);
1266 if ($res !== false && $this->mNewtalk
== 0) {
1267 $dbw->delete('user_newtalk', array($field => $value), $fname);
1269 $wgMemc->set( $key, 0 );
1271 } else if ($res === false && $this->mNewtalk
== 1) {
1272 $dbw->insert('user_newtalk', array($field => $value), $fname);
1274 $wgMemc->set( $key, 1 );
1281 # Update user_touched, so that newtalk notifications in the client cache are invalidated
1282 if ( $changed && $this->getID() ) {
1283 $dbw->update('user',
1284 /*SET*/ array( 'user_touched' => $this->mTouched
),
1285 /*WHERE*/ array( 'user_id' => $this->getID() ),
1287 $wgMemc->set( "$wgDBname:user:id:{$this->mId}", $this, 86400 );
1292 * Checks if a user with the given name exists, returns the ID
1294 function idForName() {
1295 $fname = 'User::idForName';
1298 $s = trim( $this->mName
);
1299 if ( 0 == strcmp( '', $s ) ) return 0;
1301 $dbr =& wfGetDB( DB_SLAVE
);
1302 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1303 if ( $id === false ) {
1310 * Add user object to the database
1312 function addToDatabase() {
1313 $fname = 'User::addToDatabase';
1314 $dbw =& wfGetDB( DB_MASTER
);
1315 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1316 $dbw->insert( 'user',
1318 'user_id' => $seqVal,
1319 'user_name' => $this->mName
,
1320 'user_password' => $this->mPassword
,
1321 'user_newpassword' => $this->mNewpassword
,
1322 'user_email' => $this->mEmail
,
1323 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1324 'user_real_name' => $this->mRealName
,
1325 'user_options' => $this->encodeOptions(),
1326 'user_token' => $this->mToken
1329 $this->mId
= $dbw->insertId();
1332 function spreadBlock() {
1334 # If the (non-anonymous) user is blocked, this function will block any IP address
1335 # that they successfully log on from.
1336 $fname = 'User::spreadBlock';
1338 wfDebug( "User:spreadBlock()\n" );
1339 if ( $this->mId
== 0 ) {
1343 $userblock = Block
::newFromDB( '', $this->mId
);
1344 if ( !$userblock->isValid() ) {
1348 # Check if this IP address is already blocked
1349 $ipblock = Block
::newFromDB( $wgIP );
1350 if ( $ipblock->isValid() ) {
1351 # Just update the timestamp
1352 $ipblock->updateTimestamp();
1356 # Make a new block object with the desired properties
1357 wfDebug( "Autoblocking {$this->mName}@{$wgIP}\n" );
1358 $ipblock->mAddress
= $wgIP;
1359 $ipblock->mUser
= 0;
1360 $ipblock->mBy
= $userblock->mBy
;
1361 $ipblock->mReason
= wfMsg( 'autoblocker', $this->getName(), $userblock->mReason
);
1362 $ipblock->mTimestamp
= wfTimestampNow();
1363 $ipblock->mAuto
= 1;
1364 # If the user is already blocked with an expiry date, we don't
1365 # want to pile on top of that!
1366 if($userblock->mExpiry
) {
1367 $ipblock->mExpiry
= min ( $userblock->mExpiry
, Block
::getAutoblockExpiry( $ipblock->mTimestamp
));
1369 $ipblock->mExpiry
= Block
::getAutoblockExpiry( $ipblock->mTimestamp
);
1377 function getPageRenderingHash() {
1380 return $this->mHash
;
1383 // stubthreshold is only included below for completeness,
1384 // it will always be 0 when this function is called by parsercache.
1386 $confstr = $this->getOption( 'math' );
1387 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1388 $confstr .= '!' . $this->getOption( 'editsection' );
1389 $confstr .= '!' . $this->getOption( 'date' );
1390 $confstr .= '!' . $this->getOption( 'numberheadings' );
1391 $confstr .= '!' . $this->getOption( 'language' );
1392 $confstr .= '!' . $this->getOption( 'thumbsize' );
1393 // add in language specific options, if any
1394 $extra = $wgContLang->getExtraHashOptions();
1397 $this->mHash
= $confstr;
1401 function isAllowedToCreateAccount() {
1402 return $this->isAllowed( 'createaccount' );
1406 * Set mDataLoaded, return previous value
1407 * Use this to prevent DB access in command-line scripts or similar situations
1409 function setLoaded( $loaded ) {
1410 return wfSetVar( $this->mDataLoaded
, $loaded );
1414 * Get this user's personal page title.
1419 function getUserPage() {
1420 return Title
::makeTitle( NS_USER
, $this->mName
);
1424 * Get this user's talk page title.
1429 function getTalkPage() {
1430 $title = $this->getUserPage();
1431 return $title->getTalkPage();
1437 function getMaxID() {
1438 $dbr =& wfGetDB( DB_SLAVE
);
1439 return $dbr->selectField( 'user', 'max(user_id)', false );
1443 * Determine whether the user is a newbie. Newbies are either
1444 * anonymous IPs, or the 1% most recently created accounts.
1445 * Bots and sysops are excluded.
1446 * @return bool True if it is a newbie.
1448 function isNewbie() {
1449 return $this->isAnon() ||
$this->mId
> User
::getMaxID() * 0.99 && !$this->isAllowed( 'delete' ) && !$this->isBot();
1453 * Check to see if the given clear-text password is one of the accepted passwords
1454 * @param string $password User password.
1455 * @return bool True if the given password is correct otherwise False.
1457 function checkPassword( $password ) {
1458 global $wgAuth, $wgMinimalPasswordLength;
1459 $this->loadFromDatabase();
1461 // Even though we stop people from creating passwords that
1462 // are shorter than this, doesn't mean people wont be able
1463 // to. Certain authentication plugins do NOT want to save
1464 // domain passwords in a mysql database, so we should
1465 // check this (incase $wgAuth->strict() is false).
1466 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1470 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1472 } elseif( $wgAuth->strict() ) {
1473 /* Auth plugin doesn't allow local authentication */
1476 $ep = $this->encryptPassword( $password );
1477 if ( 0 == strcmp( $ep, $this->mPassword
) ) {
1479 } elseif ( ($this->mNewpassword
!= '') && (0 == strcmp( $ep, $this->mNewpassword
)) ) {
1481 } elseif ( function_exists( 'iconv' ) ) {
1482 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1483 # Check for this with iconv
1484 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1485 if ( 0 == strcmp( $cp1252hash, $this->mPassword
) ) {
1493 * Initialize (if necessary) and return a session token value
1494 * which can be used in edit forms to show that the user's
1495 * login credentials aren't being hijacked with a foreign form
1498 * @param mixed $salt - Optional function-specific data for hash.
1499 * Use a string or an array of strings.
1503 function editToken( $salt = '' ) {
1504 if( !isset( $_SESSION['wsEditToken'] ) ) {
1505 $token = $this->generateToken();
1506 $_SESSION['wsEditToken'] = $token;
1508 $token = $_SESSION['wsEditToken'];
1510 if( is_array( $salt ) ) {
1511 $salt = implode( '|', $salt );
1513 return md5( $token . $salt );
1517 * Generate a hex-y looking random token for various uses.
1518 * Could be made more cryptographically sure if someone cares.
1521 function generateToken( $salt = '' ) {
1522 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1523 return md5( $token . $salt );
1527 * Check given value against the token value stored in the session.
1528 * A match should confirm that the form was submitted from the
1529 * user's own login session, not a form submission from a third-party
1532 * @param string $val - the input value to compare
1533 * @param string $salt - Optional function-specific data for hash
1537 function matchEditToken( $val, $salt = '' ) {
1538 return ( $val == $this->editToken( $salt ) );
1542 * Generate a new e-mail confirmation token and send a confirmation
1543 * mail to the user's given address.
1545 * @return mixed True on success, a WikiError object on failure.
1547 function sendConfirmationMail() {
1548 global $wgIP, $wgContLang;
1549 $url = $this->confirmationTokenUrl( $expiration );
1550 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1551 wfMsg( 'confirmemail_body',
1555 $wgContLang->timeanddate( $expiration, false ) ) );
1559 * Send an e-mail to this user's account. Does not check for
1560 * confirmed status or validity.
1562 * @param string $subject
1563 * @param string $body
1564 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1565 * @return mixed True on success, a WikiError object on failure.
1567 function sendMail( $subject, $body, $from = null ) {
1568 if( is_null( $from ) ) {
1569 global $wgPasswordSender;
1570 $from = $wgPasswordSender;
1573 require_once( 'UserMailer.php' );
1574 $error = userMailer( $this->getEmail(), $from, $subject, $body );
1576 if( $error == '' ) {
1579 return new WikiError( $error );
1584 * Generate, store, and return a new e-mail confirmation code.
1585 * A hash (unsalted since it's used as a key) is stored.
1586 * @param &$expiration mixed output: accepts the expiration time
1590 function confirmationToken( &$expiration ) {
1591 $fname = 'User::confirmationToken';
1594 $expires = $now +
7 * 24 * 60 * 60;
1595 $expiration = wfTimestamp( TS_MW
, $expires );
1597 $token = $this->generateToken( $this->mId
. $this->mEmail
. $expires );
1598 $hash = md5( $token );
1600 $dbw =& wfGetDB( DB_MASTER
);
1601 $dbw->update( 'user',
1602 array( 'user_email_token' => $hash,
1603 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1604 array( 'user_id' => $this->mId
),
1611 * Generate and store a new e-mail confirmation token, and return
1612 * the URL the user can use to confirm.
1613 * @param &$expiration mixed output: accepts the expiration time
1617 function confirmationTokenUrl( &$expiration ) {
1618 $token = $this->confirmationToken( $expiration );
1619 $title = Title
::makeTitle( NS_SPECIAL
, 'Confirmemail/' . $token );
1620 return $title->getFullUrl();
1624 * Mark the e-mail address confirmed and save.
1626 function confirmEmail() {
1627 $this->loadFromDatabase();
1628 $this->mEmailAuthenticated
= wfTimestampNow();
1629 $this->saveSettings();
1634 * Is this user allowed to send e-mails within limits of current
1635 * site configuration?
1638 function canSendEmail() {
1639 return $this->isEmailConfirmed();
1643 * Is this user allowed to receive e-mails within limits of current
1644 * site configuration?
1647 function canReceiveEmail() {
1648 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1652 * Is this user's e-mail address valid-looking and confirmed within
1653 * limits of the current site configuration?
1655 * If $wgEmailAuthentication is on, this may require the user to have
1656 * confirmed their address by returning a code or using a password
1657 * sent to the address from the wiki.
1661 function isEmailConfirmed() {
1662 global $wgEmailAuthentication;
1663 $this->loadFromDatabase();
1664 if( $this->isAnon() )
1666 if( !$this->isValidEmailAddr( $this->mEmail
) )
1668 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1674 * @param array $groups list of groups
1675 * @return array list of permission key names for given groups combined
1678 function getGroupPermissions( $groups ) {
1679 global $wgGroupPermissions;
1681 foreach( $groups as $group ) {
1682 if( isset( $wgGroupPermissions[$group] ) ) {
1683 $rights = array_merge( $rights,
1684 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1691 * @param string $group key name
1692 * @return string localized descriptive name, if provided
1695 function getGroupName( $group ) {
1696 $key = "group-$group-name";
1697 $name = wfMsg( $key );
1698 if( $name == '' ||
$name == "<$key>" ) {
1706 * Return the set of defined explicit groups.
1707 * The * and 'user' groups are not included.
1711 function getAllGroups() {
1712 global $wgGroupPermissions;
1714 array_keys( $wgGroupPermissions ),
1715 array( '*', 'user' ) );