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 * Is the input a valid username?
168 * Checks if the input is a valid username, we don't want an empty string,
169 * an IP address, anything that containins slashes (would mess up subpages),
170 * is longer than the maximum allowed username size or doesn't begin with
173 * @param string $name
176 function isValidUserName( $name ) {
177 global $wgContLang, $wgMaxNameChars;
180 ||
$this->isIP( $name )
181 ||
strpos( $name, '/' ) !== false
182 ||
strlen( $name ) > $wgMaxNameChars
183 ||
$name != $wgContLang->ucfirst( $name ) )
190 * Is the input a valid password?
192 * @param string $password
195 function isValidPassword( $password ) {
196 global $wgMinimalPasswordLength;
197 return strlen( $password ) >= $wgMinimalPasswordLength;
201 * does the string match roughly an email address ?
203 * @todo Check for RFC 2822 compilance
206 * @param string $addr email address
210 function isValidEmailAddr ( $addr ) {
211 # There used to be a regular expression here, it got removed because it
212 # rejected valid addresses.
213 return ( trim( $addr ) != '' ) &&
214 (false !== strpos( $addr, '@' ) );
218 * probably return a random password
219 * @return string probably a random password
221 * @todo Check what is doing really [AV]
223 function randomPassword() {
224 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
225 $l = strlen( $pwchars ) - 1;
227 $np = $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
228 $pwchars{mt_rand( 0, $l )} . chr( mt_rand(48, 57) ) .
229 $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
230 $pwchars{mt_rand( 0, $l )};
235 * Set properties to default
236 * Used at construction. It will load per language default settings only
237 * if we have an available language object.
239 function loadDefaults() {
242 $fname = 'User::loadDefaults' . $n;
243 wfProfileIn( $fname );
245 global $wgContLang, $wgIP, $wgDBname;
246 global $wgNamespacesToBeSearchedDefault;
249 $this->mNewtalk
= -1;
250 $this->mName
= $wgIP;
251 $this->mRealName
= $this->mEmail
= '';
252 $this->mEmailAuthenticated
= null;
253 $this->mPassword
= $this->mNewpassword
= '';
254 $this->mRights
= array();
255 $this->mGroups
= array();
256 $this->mOptions
= User
::getDefaultOptions();
258 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
259 $this->mOptions
['searchNs'.$nsnum] = $val;
261 unset( $this->mSkin
);
262 $this->mDataLoaded
= false;
263 $this->mBlockedby
= -1; # Unset
264 $this->setToken(); # Random
265 $this->mHash
= false;
267 if ( isset( $_COOKIE[$wgDBname.'LoggedOut'] ) ) {
268 $this->mTouched
= wfTimestamp( TS_MW
, $_COOKIE[$wgDBname.'LoggedOut'] );
271 $this->mTouched
= '0'; # Allow any pages to be cached
274 wfProfileOut( $fname );
278 * Combine the language default options with any site-specific options
279 * and add the default language variants.
285 function getDefaultOptions() {
287 * Site defaults will override the global/language defaults
289 global $wgContLang, $wgDefaultUserOptions;
290 $defOpt = $wgDefaultUserOptions +
$wgContLang->getDefaultUserOptions();
293 * default language setting
295 $variant = $wgContLang->getPreferredVariant();
296 $defOpt['variant'] = $variant;
297 $defOpt['language'] = $variant;
303 * Get a given default option value.
310 function getDefaultOption( $opt ) {
311 $defOpts = User
::getDefaultOptions();
312 if( isset( $defOpts[$opt] ) ) {
313 return $defOpts[$opt];
320 * Get blocking information
322 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
323 * non-critical checks are done against slaves. Check when actually saving should be done against
326 * Note that even if $bFromSlave is false, the check is done first against slave, then master.
327 * The logic is that if blocked on slave, we'll assume it's either blocked on master or
328 * just slightly outta sync and soon corrected - safer to block slightly more that less.
329 * And it's cheaper to check slave first, then master if needed, than master always.
331 function getBlockedStatus( $bFromSlave = true ) {
332 global $wgIP, $wgBlockCache, $wgProxyList, $wgEnableSorbs, $wgProxyWhitelist;
334 if ( -1 != $this->mBlockedby
) { return; }
336 $this->mBlockedby
= 0;
340 $block = new Block();
341 $block->forUpdate( $bFromSlave );
342 if ( $block->load( $wgIP , $this->mId
) ) {
343 $this->mBlockedby
= $block->mBy
;
344 $this->mBlockreason
= $block->mReason
;
345 $this->spreadBlock();
350 if ( !$this->mBlockedby
) {
351 # Check first against slave, and optionally from master.
352 $block = $wgBlockCache->get( $wgIP, true );
353 if ( !$block && !$bFromSlave )
355 # Not blocked: check against master, to make sure.
356 $wgBlockCache->clearLocal( );
357 $block = $wgBlockCache->get( $wgIP, false );
359 if ( $block !== false ) {
360 $this->mBlockedby
= $block->mBy
;
361 $this->mBlockreason
= $block->mReason
;
366 if ( !$this->isSysop() && !in_array( $wgIP, $wgProxyWhitelist ) ) {
369 if ( array_key_exists( $wgIP, $wgProxyList ) ) {
370 $this->mBlockedby
= wfMsg( 'proxyblocker' );
371 $this->mBlockreason
= wfMsg( 'proxyblockreason' );
375 if ( !$this->mBlockedby
&& $wgEnableSorbs && !$this->getID() ) {
376 if ( $this->inSorbsBlacklist( $wgIP ) ) {
377 $this->mBlockedby
= wfMsg( 'sorbs' );
378 $this->mBlockreason
= wfMsg( 'sorbsreason' );
384 function inSorbsBlacklist( $ip ) {
385 global $wgEnableSorbs;
386 return $wgEnableSorbs &&
387 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
390 function inOpmBlacklist( $ip ) {
392 return $wgEnableOpm &&
393 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
396 function inDnsBlacklist( $ip, $base ) {
397 $fname = 'User::inDnsBlacklist';
398 wfProfileIn( $fname );
403 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
405 for ( $i=4; $i>=1; $i-- ) {
406 $host .= $m[$i] . '.';
411 $ipList = gethostbynamel( $host );
414 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
417 wfDebug( "Requested $host, not found in $base.\n" );
421 wfProfileOut( $fname );
426 * Primitive rate limits: enforce maximum actions per time period
427 * to put a brake on flooding.
429 * Note: when using a shared cache like memcached, IP-address
430 * last-hit counters will be shared across wikis.
432 * @return bool true if a rate limiter was tripped
435 function pingLimiter( $action='edit' ) {
436 global $wgRateLimits;
437 if( !isset( $wgRateLimits[$action] ) ) {
440 if( $this->isAllowed( 'delete' ) ) {
445 global $wgMemc, $wgIP, $wgDBname, $wgRateLimitLog;
446 $fname = 'User::pingLimiter';
447 $limits = $wgRateLimits[$action];
449 $id = $this->getId();
451 if( isset( $limits['anon'] ) && $id == 0 ) {
452 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
455 if( isset( $limits['user'] ) && $id != 0 ) {
456 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
458 if( $this->isNewbie() ) {
459 if( isset( $limits['newbie'] ) && $id != 0 ) {
460 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
462 if( isset( $limits['ip'] ) ) {
463 $keys["mediawiki:limiter:$action:ip:$wgIP"] = $limits['ip'];
465 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $wgIP, $matches ) ) {
466 $subnet = $matches[1];
467 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
472 foreach( $keys as $key => $limit ) {
473 list( $max, $period ) = $limit;
474 $summary = "(limit $max in {$period}s)";
475 $count = $wgMemc->get( $key );
477 if( $count > $max ) {
478 wfDebug( "$fname: tripped! $key at $count $summary\n" );
479 if( $wgRateLimitLog ) {
480 @error_log
( wfTimestamp( TS_MW
) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
484 wfDebug( "$fname: ok. $key at $count $summary\n" );
487 wfDebug( "$fname: adding record for $key $summary\n" );
488 $wgMemc->add( $key, 1, IntVal( $period ) );
490 $wgMemc->incr( $key );
497 * Check if user is blocked
498 * @return bool True if blocked, false otherwise
500 function isBlocked( $bFromSlave = false ) {
501 $this->getBlockedStatus( $bFromSlave );
502 return $this->mBlockedby
!== 0;
506 * Get name of blocker
507 * @return string name of blocker
509 function blockedBy() {
510 $this->getBlockedStatus();
511 return $this->mBlockedby
;
515 * Get blocking reason
516 * @return string Blocking reason
518 function blockedFor() {
519 $this->getBlockedStatus();
520 return $this->mBlockreason
;
524 * Initialise php session
526 function SetupSession() {
527 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
528 if( $wgSessionsInMemcached ) {
529 require_once( 'MemcachedSessions.php' );
530 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
531 # If it's left on 'user' or another setting from another
532 # application, it will end up failing. Try to recover.
533 ini_set ( 'session.save_handler', 'files' );
535 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
536 session_cache_limiter( 'private, must-revalidate' );
541 * Read datas from session
544 function loadFromSession() {
545 global $wgMemc, $wgDBname;
547 if ( isset( $_SESSION['wsUserID'] ) ) {
548 if ( 0 != $_SESSION['wsUserID'] ) {
549 $sId = $_SESSION['wsUserID'];
553 } else if ( isset( $_COOKIE["{$wgDBname}UserID"] ) ) {
554 $sId = IntVal( $_COOKIE["{$wgDBname}UserID"] );
555 $_SESSION['wsUserID'] = $sId;
559 if ( isset( $_SESSION['wsUserName'] ) ) {
560 $sName = $_SESSION['wsUserName'];
561 } else if ( isset( $_COOKIE["{$wgDBname}UserName"] ) ) {
562 $sName = $_COOKIE["{$wgDBname}UserName"];
563 $_SESSION['wsUserName'] = $sName;
568 $passwordCorrect = FALSE;
569 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
570 if( !is_object( $user ) ||
$user->mVersion
< MW_USER_VERSION
) {
571 # Expire old serialized objects; they may be corrupt.
574 if($makenew = !$user) {
575 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
578 $user->loadFromDatabase();
580 wfDebug( "User::loadFromSession() got from cache!\n" );
583 if ( isset( $_SESSION['wsToken'] ) ) {
584 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken
;
585 } else if ( isset( $_COOKIE["{$wgDBname}Token"] ) ) {
586 $passwordCorrect = $user->mToken
== $_COOKIE["{$wgDBname}Token"];
588 return new User(); # Can't log in from session
591 if ( ( $sName == $user->mName
) && $passwordCorrect ) {
593 if($wgMemc->set( $key, $user ))
594 wfDebug( "User::loadFromSession() successfully saved user\n" );
596 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
600 return new User(); # Can't log in from session
604 * Load a user from the database
606 function loadFromDatabase() {
607 global $wgCommandLineMode;
608 $fname = "User::loadFromDatabase";
610 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
611 # loading in a command line script, don't assume all command line scripts need it like this
612 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
613 if ( $this->mDataLoaded
) {
618 $this->mId
= IntVal( $this->mId
);
620 /** Anonymous user */
623 $this->mRights
= $this->getGroupPermissions( array( '*' ) );
624 $this->mDataLoaded
= true;
626 } # the following stuff is for non-anonymous users only
628 $dbr =& wfGetDB( DB_SLAVE
);
629 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
630 'user_email_authenticated',
631 'user_real_name','user_options','user_touched', 'user_token' ),
632 array( 'user_id' => $this->mId
), $fname );
634 if ( $s !== false ) {
635 $this->mName
= $s->user_name
;
636 $this->mEmail
= $s->user_email
;
637 $this->mEmailAuthenticated
= wfTimestampOrNull( TS_MW
, $s->user_email_authenticated
);
638 $this->mRealName
= $s->user_real_name
;
639 $this->mPassword
= $s->user_password
;
640 $this->mNewpassword
= $s->user_newpassword
;
641 $this->decodeOptions( $s->user_options
);
642 $this->mTouched
= wfTimestamp(TS_MW
,$s->user_touched
);
643 $this->mToken
= $s->user_token
;
645 $res = $dbr->select( 'user_groups',
647 array( 'ug_user' => $this->mId
),
649 $this->mGroups
= array();
650 while( $row = $dbr->fetchObject( $res ) ) {
651 $this->mGroups
[] = $row->ug_group
;
653 $effectiveGroups = array_merge( array( '*', 'user' ), $this->mGroups
);
654 $this->mRights
= $this->getGroupPermissions( $effectiveGroups );
657 $this->mDataLoaded
= true;
660 function getID() { return $this->mId
; }
661 function setID( $v ) {
663 $this->mDataLoaded
= false;
667 $this->loadFromDatabase();
671 function setName( $str ) {
672 $this->loadFromDatabase();
678 * Return the title dbkey form of the name, for eg user pages.
682 function getTitleKey() {
683 return str_replace( ' ', '_', $this->getName() );
686 function getNewtalk() {
688 $fname = 'User::getNewtalk';
689 $this->loadFromDatabase();
691 # Load the newtalk status if it is unloaded (mNewtalk=-1)
692 if( $this->mNewtalk
== -1 ) {
693 $this->mNewtalk
= 0; # reset talk page status
695 # Check memcached separately for anons, who have no
696 # entire User object stored in there.
698 global $wgDBname, $wgMemc;
699 $key = "$wgDBname:newtalk:ip:{$this->mName}";
700 $newtalk = $wgMemc->get( $key );
701 if( is_integer( $newtalk ) ) {
702 $this->mNewtalk
= $newtalk ?
1 : 0;
703 return (bool)$this->mNewtalk
;
707 $dbr =& wfGetDB( DB_SLAVE
);
708 if ( $wgUseEnotif ) {
709 $res = $dbr->select( 'watchlist',
711 array( 'wl_title' => $this->getTitleKey(),
712 'wl_namespace' => NS_USER_TALK
,
713 'wl_user' => $this->mId
,
714 'wl_notificationtimestamp != 0' ),
715 'User::getNewtalk' );
716 if( $dbr->numRows($res) > 0 ) {
719 $dbr->freeResult( $res );
720 } elseif ( $this->mId
) {
721 $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId
), $fname );
723 if ( $dbr->numRows($res)>0 ) {
726 $dbr->freeResult( $res );
728 $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->mName
), $fname );
729 $this->mNewtalk
= $dbr->numRows( $res ) > 0 ?
1 : 0;
730 $dbr->freeResult( $res );
734 $wgMemc->set( $key, $this->mNewtalk
, time() ); // + 1800 );
738 return ( 0 != $this->mNewtalk
);
741 function setNewtalk( $val ) {
742 $this->loadFromDatabase();
743 $this->mNewtalk
= $val;
744 $this->invalidateCache();
747 function invalidateCache() {
748 global $wgClockSkewFudge;
749 $this->loadFromDatabase();
750 $this->mTouched
= wfTimestamp(TS_MW
, time() +
$wgClockSkewFudge );
751 # Don't forget to save the options after this or
752 # it won't take effect!
755 function validateCache( $timestamp ) {
756 $this->loadFromDatabase();
757 return ($timestamp >= $this->mTouched
);
762 * Will only be salted if $wgPasswordSalt is true
763 * @param string Password.
764 * @return string Salted password or clear password.
766 function addSalt( $p ) {
767 global $wgPasswordSalt;
769 return md5( "{$this->mId}-{$p}" );
775 * Encrypt a password.
776 * It can eventuall salt a password @see User::addSalt()
777 * @param string $p clear Password.
778 * @param string Encrypted password.
780 function encryptPassword( $p ) {
781 return $this->addSalt( md5( $p ) );
784 # Set the password and reset the random token
785 function setPassword( $str ) {
786 $this->loadFromDatabase();
788 $this->mPassword
= $this->encryptPassword( $str );
789 $this->mNewpassword
= '';
792 # Set the random token (used for persistent authentication)
793 function setToken( $token = false ) {
794 global $wgSecretKey, $wgProxyKey, $wgDBname;
796 if ( $wgSecretKey ) {
798 } elseif ( $wgProxyKey ) {
803 $this->mToken
= md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId
);
805 $this->mToken
= $token;
810 function setCookiePassword( $str ) {
811 $this->loadFromDatabase();
812 $this->mCookiePassword
= md5( $str );
815 function setNewpassword( $str ) {
816 $this->loadFromDatabase();
817 $this->mNewpassword
= $this->encryptPassword( $str );
820 function getEmail() {
821 $this->loadFromDatabase();
822 return $this->mEmail
;
825 function getEmailAuthenticationTimestamp() {
826 $this->loadFromDatabase();
827 return $this->mEmailAuthenticated
;
830 function setEmail( $str ) {
831 $this->loadFromDatabase();
832 $this->mEmail
= $str;
835 function getRealName() {
836 $this->loadFromDatabase();
837 return $this->mRealName
;
840 function setRealName( $str ) {
841 $this->loadFromDatabase();
842 $this->mRealName
= $str;
845 function getOption( $oname ) {
846 $this->loadFromDatabase();
847 if ( array_key_exists( $oname, $this->mOptions
) ) {
848 return trim( $this->mOptions
[$oname] );
854 function setOption( $oname, $val ) {
855 $this->loadFromDatabase();
856 if ( $oname == 'skin' ) {
857 # Clear cached skin, so the new one displays immediately in Special:Preferences
858 unset( $this->mSkin
);
860 $this->mOptions
[$oname] = $val;
861 $this->invalidateCache();
864 function getRights() {
865 $this->loadFromDatabase();
866 return $this->mRights
;
870 * Get the list of explicit group memberships this user has.
871 * The implicit * and user groups are not included.
872 * @return array of strings
874 function getGroups() {
875 $this->loadFromDatabase();
876 return $this->mGroups
;
880 * Get the list of implicit group memberships this user has.
881 * This includes all explicit groups, plus 'user' if logged in
882 * and '*' for all accounts.
883 * @return array of strings
885 function getEffectiveGroups() {
886 $base = array( '*' );
887 if( $this->isLoggedIn() ) {
890 return array_merge( $base, $this->getGroups() );
894 * Remove the user from the given group.
895 * This takes immediate effect.
898 function addGroup( $group ) {
899 $dbw =& wfGetDB( DB_MASTER
);
900 $dbw->insert( 'user_groups',
902 'ug_user' => $this->getID(),
903 'ug_group' => $group,
908 $this->mGroups
= array_merge( $this->mGroups
, array( $group ) );
909 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
911 $this->invalidateCache();
912 $this->saveSettings();
916 * Remove the user from the given group.
917 * This takes immediate effect.
920 function removeGroup( $group ) {
921 $dbw =& wfGetDB( DB_MASTER
);
922 $dbw->delete( 'user_groups',
924 'ug_user' => $this->getID(),
925 'ug_group' => $group,
927 'User::removeGroup' );
929 $this->mGroups
= array_diff( $this->mGroups
, array( $group ) );
930 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
932 $this->invalidateCache();
933 $this->saveSettings();
938 * A more legible check for non-anonymousness.
939 * Returns true if the user is not an anonymous visitor.
943 function isLoggedIn() {
944 return( $this->getID() != 0 );
948 * A more legible check for anonymousness.
949 * Returns true if the user is an anonymous visitor.
954 return !$this->isLoggedIn();
958 * Check if a user is sysop
959 * Die with backtrace. Use User:isAllowed() instead.
963 return $this->isAllowed( 'protect' );
967 function isDeveloper() {
968 return $this->isAllowed( 'siteadmin' );
972 function isBureaucrat() {
973 return $this->isAllowed( 'makesysop' );
977 * Whether the user is a bot
978 * @todo need to be migrated to the new user level management sytem
981 $this->loadFromDatabase();
982 return in_array( 'bot', $this->mRights
);
986 * Check if user is allowed to access a feature / make an action
987 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
988 * @return boolean True: action is allowed, False: action should not be allowed
990 function isAllowed($action='') {
991 $this->loadFromDatabase();
992 return in_array( $action , $this->mRights
);
996 * Load a skin if it doesn't exist or return it
997 * @todo FIXME : need to check the old failback system [AV]
999 function &getSkin() {
1001 if ( ! isset( $this->mSkin
) ) {
1002 $fname = 'User::getSkin';
1003 wfProfileIn( $fname );
1005 # get all skin names available
1006 $skinNames = Skin
::getSkinNames();
1009 $userSkin = $this->getOption( 'skin' );
1010 if ( $userSkin == '' ) { $userSkin = 'standard'; }
1012 if ( !isset( $skinNames[$userSkin] ) ) {
1013 # in case the user skin could not be found find a replacement
1017 2 => 'CologneBlue');
1018 # if phptal is enabled we should have monobook skin that
1019 # superseed the good old SkinStandard.
1020 if ( isset( $skinNames['monobook'] ) ) {
1021 $fallback[0] = 'MonoBook';
1024 if(is_numeric($userSkin) && isset( $fallback[$userSkin]) ){
1025 $sn = $fallback[$userSkin];
1030 # The user skin is available
1031 $sn = $skinNames[$userSkin];
1034 # Grab the skin class and initialise it. Each skin checks for PHPTal
1035 # and will not load if it's not enabled.
1036 require_once( $IP.'/skins/'.$sn.'.php' );
1038 # Check if we got if not failback to default skin
1039 $className = 'Skin'.$sn;
1040 if( !class_exists( $className ) ) {
1041 # DO NOT die if the class isn't found. This breaks maintenance
1042 # scripts and can cause a user account to be unrecoverable
1043 # except by SQL manipulation if a previously valid skin name
1044 # is no longer valid.
1045 $className = 'SkinStandard';
1046 require_once( $IP.'/skins/Standard.php' );
1048 $this->mSkin
=& new $className;
1049 wfProfileOut( $fname );
1051 return $this->mSkin
;
1055 * @param string $title Article title to look at
1059 * Check watched status of an article
1060 * @return bool True if article is watched
1062 function isWatched( $title ) {
1063 $wl = WatchedItem
::fromUserTitle( $this, $title );
1064 return $wl->isWatched();
1070 function addWatch( $title ) {
1071 $wl = WatchedItem
::fromUserTitle( $this, $title );
1073 $this->invalidateCache();
1077 * Stop watching an article
1079 function removeWatch( $title ) {
1080 $wl = WatchedItem
::fromUserTitle( $this, $title );
1082 $this->invalidateCache();
1086 * Clear the user's notification timestamp for the given title.
1087 * If e-notif e-mails are on, they will receive notification mails on
1088 * the next change of the page if it's watched etc.
1090 function clearNotification( &$title ) {
1091 global $wgUser, $wgUseEnotif;
1093 if ( !$wgUseEnotif ) {
1097 $userid = $this->getID();
1102 // Only update the timestamp if the page is being watched.
1103 // The query to find out if it is watched is cached both in memcached and per-invocation,
1104 // and when it does have to be executed, it can be on a slave
1105 // If this is the user's newtalk page, we always update the timestamp
1106 if ($title->getNamespace() == NS_USER_TALK
&&
1107 $title->getText() == $wgUser->getName())
1110 } elseif ( $this->getID() == $wgUser->getID() ) {
1111 $watched = $title->userIsWatching();
1116 // If the page is watched by the user (or may be watched), update the timestamp on any
1117 // any matching rows
1119 $dbw =& wfGetDB( DB_MASTER
);
1120 $success = $dbw->update( 'watchlist',
1122 'wl_notificationtimestamp' => 0
1123 ), array( /* WHERE */
1124 'wl_title' => $title->getDBkey(),
1125 'wl_namespace' => $title->getNamespace(),
1126 'wl_user' => $this->getID()
1127 ), 'User::clearLastVisited'
1135 * Resets all of the given user's page-change notification timestamps.
1136 * If e-notif e-mails are on, they will receive notification mails on
1137 * the next change of any watched page.
1139 * @param int $currentUser user ID number
1142 function clearAllNotifications( $currentUser ) {
1143 global $wgUseEnotif;
1144 if ( !$wgUseEnotif ) {
1147 if( $currentUser != 0 ) {
1149 $dbw =& wfGetDB( DB_MASTER
);
1150 $success = $dbw->update( 'watchlist',
1152 'wl_notificationtimestamp' => 0
1153 ), array( /* WHERE */
1154 'wl_user' => $currentUser
1155 ), 'UserMailer::clearAll'
1158 # we also need to clear here the "you have new message" notification for the own user_talk page
1159 # This is cleared one page view later in Article::viewUpdates();
1165 * @return string Encoding options
1167 function encodeOptions() {
1169 foreach ( $this->mOptions
as $oname => $oval ) {
1170 array_push( $a, $oname.'='.$oval );
1172 $s = implode( "\n", $a );
1179 function decodeOptions( $str ) {
1180 $a = explode( "\n", $str );
1181 foreach ( $a as $s ) {
1182 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1183 $this->mOptions
[$m[1]] = $m[2];
1188 function setCookies() {
1189 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgDBname;
1190 if ( 0 == $this->mId
) return;
1191 $this->loadFromDatabase();
1192 $exp = time() +
$wgCookieExpiration;
1194 $_SESSION['wsUserID'] = $this->mId
;
1195 setcookie( $wgDBname.'UserID', $this->mId
, $exp, $wgCookiePath, $wgCookieDomain );
1197 $_SESSION['wsUserName'] = $this->mName
;
1198 setcookie( $wgDBname.'UserName', $this->mName
, $exp, $wgCookiePath, $wgCookieDomain );
1200 $_SESSION['wsToken'] = $this->mToken
;
1201 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1202 setcookie( $wgDBname.'Token', $this->mToken
, $exp, $wgCookiePath, $wgCookieDomain );
1204 setcookie( $wgDBname.'Token', '', time() - 3600 );
1210 * It will clean the session cookie
1213 global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP;
1214 $this->loadDefaults();
1215 $this->setLoaded( true );
1217 $_SESSION['wsUserID'] = 0;
1219 setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1220 setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1222 # Remember when user logged out, to prevent seeing cached pages
1223 setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() +
86400, $wgCookiePath, $wgCookieDomain );
1227 * Save object settings into database
1229 function saveSettings() {
1230 global $wgMemc, $wgDBname, $wgUseEnotif;
1231 $fname = 'User::saveSettings';
1233 if ( wfReadOnly() ) { return; }
1234 $this->saveNewtalk();
1235 if ( 0 == $this->mId
) { return; }
1237 $dbw =& wfGetDB( DB_MASTER
);
1238 $dbw->update( 'user',
1240 'user_name' => $this->mName
,
1241 'user_password' => $this->mPassword
,
1242 'user_newpassword' => $this->mNewpassword
,
1243 'user_real_name' => $this->mRealName
,
1244 'user_email' => $this->mEmail
,
1245 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1246 'user_options' => $this->encodeOptions(),
1247 'user_touched' => $dbw->timestamp($this->mTouched
),
1248 'user_token' => $this->mToken
1249 ), array( /* WHERE */
1250 'user_id' => $this->mId
1253 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1257 * Save value of new talk flag.
1259 function saveNewtalk() {
1260 global $wgDBname, $wgMemc, $wgUseEnotif;
1262 $fname = 'User::saveNewtalk';
1266 if ( wfReadOnly() ) { return ; }
1267 $dbr =& wfGetDB( DB_SLAVE
);
1268 $dbw =& wfGetDB( DB_MASTER
);
1270 if ( $wgUseEnotif ) {
1271 if ( ! $this->getNewtalk() ) {
1272 # Delete the watchlist entry for user_talk page X watched by user X
1273 $dbw->delete( 'watchlist',
1274 array( 'wl_user' => $this->mId
,
1275 'wl_title' => $this->getTitleKey(),
1276 'wl_namespace' => NS_USER_TALK
),
1278 if ( $dbw->affectedRows() ) {
1282 # Anon users have a separate memcache space for newtalk
1283 # since they don't store their own info. Trim...
1284 $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" );
1288 if ($this->getID() != 0) {
1290 $value = $this->getID();
1294 $value = $this->mName
;
1295 $key = "$wgDBname:newtalk:ip:$this->mName";
1298 $dbr =& wfGetDB( DB_SLAVE
);
1299 $dbw =& wfGetDB( DB_MASTER
);
1301 $res = $dbr->selectField('user_newtalk', $field,
1302 array($field => $value), $fname);
1305 if ($res !== false && $this->mNewtalk
== 0) {
1306 $dbw->delete('user_newtalk', array($field => $value), $fname);
1308 $wgMemc->set( $key, 0 );
1310 } else if ($res === false && $this->mNewtalk
== 1) {
1311 $dbw->insert('user_newtalk', array($field => $value), $fname);
1313 $wgMemc->set( $key, 1 );
1320 # Update user_touched, so that newtalk notifications in the client cache are invalidated
1321 if ( $changed && $this->getID() ) {
1322 $dbw->update('user',
1323 /*SET*/ array( 'user_touched' => $this->mTouched
),
1324 /*WHERE*/ array( 'user_id' => $this->getID() ),
1326 $wgMemc->set( "$wgDBname:user:id:{$this->mId}", $this, 86400 );
1331 * Checks if a user with the given name exists, returns the ID
1333 function idForName() {
1334 $fname = 'User::idForName';
1337 $s = trim( $this->mName
);
1338 if ( 0 == strcmp( '', $s ) ) return 0;
1340 $dbr =& wfGetDB( DB_SLAVE
);
1341 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1342 if ( $id === false ) {
1349 * Add user object to the database
1351 function addToDatabase() {
1352 $fname = 'User::addToDatabase';
1353 $dbw =& wfGetDB( DB_MASTER
);
1354 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1355 $dbw->insert( 'user',
1357 'user_id' => $seqVal,
1358 'user_name' => $this->mName
,
1359 'user_password' => $this->mPassword
,
1360 'user_newpassword' => $this->mNewpassword
,
1361 'user_email' => $this->mEmail
,
1362 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1363 'user_real_name' => $this->mRealName
,
1364 'user_options' => $this->encodeOptions(),
1365 'user_token' => $this->mToken
1368 $this->mId
= $dbw->insertId();
1371 function spreadBlock() {
1373 # If the (non-anonymous) user is blocked, this function will block any IP address
1374 # that they successfully log on from.
1375 $fname = 'User::spreadBlock';
1377 wfDebug( "User:spreadBlock()\n" );
1378 if ( $this->mId
== 0 ) {
1382 $userblock = Block
::newFromDB( '', $this->mId
);
1383 if ( !$userblock->isValid() ) {
1387 # Check if this IP address is already blocked
1388 $ipblock = Block
::newFromDB( $wgIP );
1389 if ( $ipblock->isValid() ) {
1390 # Just update the timestamp
1391 $ipblock->updateTimestamp();
1395 # Make a new block object with the desired properties
1396 wfDebug( "Autoblocking {$this->mName}@{$wgIP}\n" );
1397 $ipblock->mAddress
= $wgIP;
1398 $ipblock->mUser
= 0;
1399 $ipblock->mBy
= $userblock->mBy
;
1400 $ipblock->mReason
= wfMsg( 'autoblocker', $this->getName(), $userblock->mReason
);
1401 $ipblock->mTimestamp
= wfTimestampNow();
1402 $ipblock->mAuto
= 1;
1403 # If the user is already blocked with an expiry date, we don't
1404 # want to pile on top of that!
1405 if($userblock->mExpiry
) {
1406 $ipblock->mExpiry
= min ( $userblock->mExpiry
, Block
::getAutoblockExpiry( $ipblock->mTimestamp
));
1408 $ipblock->mExpiry
= Block
::getAutoblockExpiry( $ipblock->mTimestamp
);
1416 function getPageRenderingHash() {
1419 return $this->mHash
;
1422 // stubthreshold is only included below for completeness,
1423 // it will always be 0 when this function is called by parsercache.
1425 $confstr = $this->getOption( 'math' );
1426 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1427 $confstr .= '!' . $this->getOption( 'editsection' );
1428 $confstr .= '!' . $this->getOption( 'date' );
1429 $confstr .= '!' . $this->getOption( 'numberheadings' );
1430 $confstr .= '!' . $this->getOption( 'language' );
1431 $confstr .= '!' . $this->getOption( 'thumbsize' );
1432 // add in language specific options, if any
1433 $extra = $wgContLang->getExtraHashOptions();
1436 $this->mHash
= $confstr;
1440 function isAllowedToCreateAccount() {
1441 return $this->isAllowed( 'createaccount' );
1445 * Set mDataLoaded, return previous value
1446 * Use this to prevent DB access in command-line scripts or similar situations
1448 function setLoaded( $loaded ) {
1449 return wfSetVar( $this->mDataLoaded
, $loaded );
1453 * Get this user's personal page title.
1458 function getUserPage() {
1459 return Title
::makeTitle( NS_USER
, $this->mName
);
1463 * Get this user's talk page title.
1468 function getTalkPage() {
1469 $title = $this->getUserPage();
1470 return $title->getTalkPage();
1476 function getMaxID() {
1477 $dbr =& wfGetDB( DB_SLAVE
);
1478 return $dbr->selectField( 'user', 'max(user_id)', false );
1482 * Determine whether the user is a newbie. Newbies are either
1483 * anonymous IPs, or the 1% most recently created accounts.
1484 * Bots and sysops are excluded.
1485 * @return bool True if it is a newbie.
1487 function isNewbie() {
1488 return $this->isAnon() ||
$this->mId
> User
::getMaxID() * 0.99 && !$this->isAllowed( 'delete' ) && !$this->isBot();
1492 * Check to see if the given clear-text password is one of the accepted passwords
1493 * @param string $password User password.
1494 * @return bool True if the given password is correct otherwise False.
1496 function checkPassword( $password ) {
1497 global $wgAuth, $wgMinimalPasswordLength;
1498 $this->loadFromDatabase();
1500 // Even though we stop people from creating passwords that
1501 // are shorter than this, doesn't mean people wont be able
1502 // to. Certain authentication plugins do NOT want to save
1503 // domain passwords in a mysql database, so we should
1504 // check this (incase $wgAuth->strict() is false).
1505 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1509 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1511 } elseif( $wgAuth->strict() ) {
1512 /* Auth plugin doesn't allow local authentication */
1515 $ep = $this->encryptPassword( $password );
1516 if ( 0 == strcmp( $ep, $this->mPassword
) ) {
1518 } elseif ( ($this->mNewpassword
!= '') && (0 == strcmp( $ep, $this->mNewpassword
)) ) {
1520 } elseif ( function_exists( 'iconv' ) ) {
1521 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1522 # Check for this with iconv
1523 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1524 if ( 0 == strcmp( $cp1252hash, $this->mPassword
) ) {
1532 * Initialize (if necessary) and return a session token value
1533 * which can be used in edit forms to show that the user's
1534 * login credentials aren't being hijacked with a foreign form
1537 * @param mixed $salt - Optional function-specific data for hash.
1538 * Use a string or an array of strings.
1542 function editToken( $salt = '' ) {
1543 if( !isset( $_SESSION['wsEditToken'] ) ) {
1544 $token = $this->generateToken();
1545 $_SESSION['wsEditToken'] = $token;
1547 $token = $_SESSION['wsEditToken'];
1549 if( is_array( $salt ) ) {
1550 $salt = implode( '|', $salt );
1552 return md5( $token . $salt );
1556 * Generate a hex-y looking random token for various uses.
1557 * Could be made more cryptographically sure if someone cares.
1560 function generateToken( $salt = '' ) {
1561 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1562 return md5( $token . $salt );
1566 * Check given value against the token value stored in the session.
1567 * A match should confirm that the form was submitted from the
1568 * user's own login session, not a form submission from a third-party
1571 * @param string $val - the input value to compare
1572 * @param string $salt - Optional function-specific data for hash
1576 function matchEditToken( $val, $salt = '' ) {
1577 return ( $val == $this->editToken( $salt ) );
1581 * Generate a new e-mail confirmation token and send a confirmation
1582 * mail to the user's given address.
1584 * @return mixed True on success, a WikiError object on failure.
1586 function sendConfirmationMail() {
1587 global $wgIP, $wgContLang;
1588 $url = $this->confirmationTokenUrl( $expiration );
1589 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1590 wfMsg( 'confirmemail_body',
1594 $wgContLang->timeanddate( $expiration, false ) ) );
1598 * Send an e-mail to this user's account. Does not check for
1599 * confirmed status or validity.
1601 * @param string $subject
1602 * @param string $body
1603 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1604 * @return mixed True on success, a WikiError object on failure.
1606 function sendMail( $subject, $body, $from = null ) {
1607 if( is_null( $from ) ) {
1608 global $wgPasswordSender;
1609 $from = $wgPasswordSender;
1612 require_once( 'UserMailer.php' );
1613 $error = userMailer( $this->getEmail(), $from, $subject, $body );
1615 if( $error == '' ) {
1618 return new WikiError( $error );
1623 * Generate, store, and return a new e-mail confirmation code.
1624 * A hash (unsalted since it's used as a key) is stored.
1625 * @param &$expiration mixed output: accepts the expiration time
1629 function confirmationToken( &$expiration ) {
1630 $fname = 'User::confirmationToken';
1633 $expires = $now +
7 * 24 * 60 * 60;
1634 $expiration = wfTimestamp( TS_MW
, $expires );
1636 $token = $this->generateToken( $this->mId
. $this->mEmail
. $expires );
1637 $hash = md5( $token );
1639 $dbw =& wfGetDB( DB_MASTER
);
1640 $dbw->update( 'user',
1641 array( 'user_email_token' => $hash,
1642 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1643 array( 'user_id' => $this->mId
),
1650 * Generate and store a new e-mail confirmation token, and return
1651 * the URL the user can use to confirm.
1652 * @param &$expiration mixed output: accepts the expiration time
1656 function confirmationTokenUrl( &$expiration ) {
1657 $token = $this->confirmationToken( $expiration );
1658 $title = Title
::makeTitle( NS_SPECIAL
, 'Confirmemail/' . $token );
1659 return $title->getFullUrl();
1663 * Mark the e-mail address confirmed and save.
1665 function confirmEmail() {
1666 $this->loadFromDatabase();
1667 $this->mEmailAuthenticated
= wfTimestampNow();
1668 $this->saveSettings();
1673 * Is this user allowed to send e-mails within limits of current
1674 * site configuration?
1677 function canSendEmail() {
1678 return $this->isEmailConfirmed();
1682 * Is this user allowed to receive e-mails within limits of current
1683 * site configuration?
1686 function canReceiveEmail() {
1687 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1691 * Is this user's e-mail address valid-looking and confirmed within
1692 * limits of the current site configuration?
1694 * If $wgEmailAuthentication is on, this may require the user to have
1695 * confirmed their address by returning a code or using a password
1696 * sent to the address from the wiki.
1700 function isEmailConfirmed() {
1701 global $wgEmailAuthentication;
1702 $this->loadFromDatabase();
1703 if( $this->isAnon() )
1705 if( !$this->isValidEmailAddr( $this->mEmail
) )
1707 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1713 * @param array $groups list of groups
1714 * @return array list of permission key names for given groups combined
1717 function getGroupPermissions( $groups ) {
1718 global $wgGroupPermissions;
1720 foreach( $groups as $group ) {
1721 if( isset( $wgGroupPermissions[$group] ) ) {
1722 $rights = array_merge( $rights,
1723 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1730 * @param string $group key name
1731 * @return string localized descriptive name, if provided
1734 function getGroupName( $group ) {
1735 $key = "group-$group-name";
1736 $name = wfMsg( $key );
1737 if( $name == '' ||
$name == "<$key>" ) {
1745 * Return the set of defined explicit groups.
1746 * The * and 'user' groups are not included.
1750 function getAllGroups() {
1751 global $wgGroupPermissions;
1753 array_keys( $wgGroupPermissions ),
1754 array( '*', 'user' ) );