11 require_once( 'WatchedItem.php' );
13 # Number of characters in user_token field
14 define( 'USER_TOKEN_LENGTH', 32 );
16 # Serialized record version
17 define( 'MW_USER_VERSION', 2 );
27 var $mId, $mName, $mPassword, $mEmail, $mNewtalk;
28 var $mEmailAuthenticated;
29 var $mRights, $mOptions;
30 var $mDataLoaded, $mNewpassword;
32 var $mBlockedby, $mBlockreason;
38 var $mVersion; // serialized version
40 /** Construct using User:loadDefaults() */
42 $this->loadDefaults();
43 $this->mVersion
= MW_USER_VERSION
;
47 * Static factory method
48 * @param string $name Username, validated by Title:newFromText()
52 function newFromName( $name ) {
55 # Force usernames to capital
57 $name = $wgContLang->ucfirst( $name );
59 # Clean up name according to title rules
60 $t = Title
::newFromText( $name );
65 # Reject various classes of invalid names
66 $canonicalName = $t->getText();
68 $canonicalName = $wgAuth->getCanonicalName( $t->getText() );
70 if( !User
::isValidUserName( $canonicalName ) ) {
74 $u->setName( $canonicalName );
75 $u->setId( $u->idFromName( $canonicalName ) );
80 * Factory method to fetch whichever use has a given email confirmation code.
81 * This code is generated when an account is created or its e-mail address
84 * If the code is invalid or has expired, returns NULL.
90 function newFromConfirmationCode( $code ) {
91 $dbr =& wfGetDB( DB_SLAVE
);
92 $name = $dbr->selectField( 'user', 'user_name', array(
93 'user_email_token' => md5( $code ),
94 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
96 if( is_string( $name ) ) {
97 return User
::newFromName( $name );
104 * Serialze sleep function, for better cache efficiency and avoidance of
105 * silly "incomplete type" errors when skins are cached
108 return array( 'mId', 'mName', 'mPassword', 'mEmail', 'mNewtalk',
109 'mEmailAuthenticated', 'mRights', 'mOptions', 'mDataLoaded',
110 'mNewpassword', 'mBlockedby', 'mBlockreason', 'mTouched',
111 'mToken', 'mRealName', 'mHash', 'mGroups' );
115 * Get username given an id.
116 * @param integer $id Database user id
117 * @return string Nickname of a user
120 function whoIs( $id ) {
121 $dbr =& wfGetDB( DB_SLAVE
);
122 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ) );
126 * Get real username given an id.
127 * @param integer $id Database user id
128 * @return string Realname of a user
131 function whoIsReal( $id ) {
132 $dbr =& wfGetDB( DB_SLAVE
);
133 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ) );
137 * Get database id given a user name
138 * @param string $name Nickname of a user
139 * @return integer|null Database user id (null: if non existent
142 function idFromName( $name ) {
143 $fname = "User::idFromName";
145 $nt = Title
::newFromText( $name );
146 if( is_null( $nt ) ) {
150 $dbr =& wfGetDB( DB_SLAVE
);
151 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
153 if ( $s === false ) {
161 * does the string match an anonymous IPv4 address?
164 * @param string $name Nickname of a user
167 function isIP( $name ) {
168 return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/",$name);
169 /*return preg_match("/^
170 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
171 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
172 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
173 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
178 * Is the input a valid username?
180 * Checks if the input is a valid username, we don't want an empty string,
181 * an IP address, anything that containins slashes (would mess up subpages),
182 * is longer than the maximum allowed username size or doesn't begin with
185 * @param string $name
189 function isValidUserName( $name ) {
190 global $wgContLang, $wgMaxNameChars;
193 || User
::isIP( $name )
194 ||
strpos( $name, '/' ) !== false
195 ||
strlen( $name ) > $wgMaxNameChars
196 ||
$name != $wgContLang->ucfirst( $name ) )
203 * Is the input a valid password?
205 * @param string $password
209 function isValidPassword( $password ) {
210 global $wgMinimalPasswordLength;
211 return strlen( $password ) >= $wgMinimalPasswordLength;
215 * does the string match roughly an email address ?
217 * @todo Check for RFC 2822 compilance
220 * @param string $addr email address
224 function isValidEmailAddr ( $addr ) {
225 # There used to be a regular expression here, it got removed because it
226 # rejected valid addresses.
227 return ( trim( $addr ) != '' ) &&
228 (false !== strpos( $addr, '@' ) );
232 * Count the number of edits of a user
234 * @param int $uid The user ID to check
237 function edits( $uid ) {
238 $fname = 'User::editCount';
240 $dbr =& wfGetDB( DB_SLAVE
);
241 return $dbr->selectField(
242 'revision', 'count(*)',
243 array( 'rev_user' => $uid ),
249 * probably return a random password
250 * @return string probably a random password
252 * @todo Check what is doing really [AV]
254 function randomPassword() {
255 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
256 $l = strlen( $pwchars ) - 1;
258 $np = $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
259 $pwchars{mt_rand( 0, $l )} . chr( mt_rand(48, 57) ) .
260 $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
261 $pwchars{mt_rand( 0, $l )};
266 * Set properties to default
267 * Used at construction. It will load per language default settings only
268 * if we have an available language object.
270 function loadDefaults() {
273 $fname = 'User::loadDefaults' . $n;
274 wfProfileIn( $fname );
276 global $wgContLang, $wgIP, $wgDBname;
277 global $wgNamespacesToBeSearchedDefault;
280 $this->mNewtalk
= -1;
281 $this->mName
= $wgIP;
282 $this->mRealName
= $this->mEmail
= '';
283 $this->mEmailAuthenticated
= null;
284 $this->mPassword
= $this->mNewpassword
= '';
285 $this->mRights
= array();
286 $this->mGroups
= array();
287 $this->mOptions
= User
::getDefaultOptions();
289 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
290 $this->mOptions
['searchNs'.$nsnum] = $val;
292 unset( $this->mSkin
);
293 $this->mDataLoaded
= false;
294 $this->mBlockedby
= -1; # Unset
295 $this->setToken(); # Random
296 $this->mHash
= false;
298 if ( isset( $_COOKIE[$wgDBname.'LoggedOut'] ) ) {
299 $this->mTouched
= wfTimestamp( TS_MW
, $_COOKIE[$wgDBname.'LoggedOut'] );
302 $this->mTouched
= '0'; # Allow any pages to be cached
305 wfProfileOut( $fname );
309 * Combine the language default options with any site-specific options
310 * and add the default language variants.
316 function getDefaultOptions() {
318 * Site defaults will override the global/language defaults
320 global $wgContLang, $wgDefaultUserOptions;
321 $defOpt = $wgDefaultUserOptions +
$wgContLang->getDefaultUserOptions();
324 * default language setting
326 $variant = $wgContLang->getPreferredVariant();
327 $defOpt['variant'] = $variant;
328 $defOpt['language'] = $variant;
334 * Get a given default option value.
341 function getDefaultOption( $opt ) {
342 $defOpts = User
::getDefaultOptions();
343 if( isset( $defOpts[$opt] ) ) {
344 return $defOpts[$opt];
351 * Get blocking information
353 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
354 * non-critical checks are done against slaves. Check when actually saving should be done against
357 * Note that even if $bFromSlave is false, the check is done first against slave, then master.
358 * The logic is that if blocked on slave, we'll assume it's either blocked on master or
359 * just slightly outta sync and soon corrected - safer to block slightly more that less.
360 * And it's cheaper to check slave first, then master if needed, than master always.
362 function getBlockedStatus( $bFromSlave = true ) {
363 global $wgIP, $wgBlockCache, $wgProxyList, $wgEnableSorbs, $wgProxyWhitelist;
365 if ( -1 != $this->mBlockedby
) { return; }
367 $this->mBlockedby
= 0;
371 $block = new Block();
372 $block->forUpdate( $bFromSlave );
373 if ( $block->load( $wgIP , $this->mId
) ) {
374 $this->mBlockedby
= $block->mBy
;
375 $this->mBlockreason
= $block->mReason
;
376 $this->spreadBlock();
381 if ( !$this->mBlockedby
) {
382 # Check first against slave, and optionally from master.
383 $block = $wgBlockCache->get( $wgIP, true );
384 if ( !$block && !$bFromSlave )
386 # Not blocked: check against master, to make sure.
387 $wgBlockCache->clearLocal( );
388 $block = $wgBlockCache->get( $wgIP, false );
390 if ( $block !== false ) {
391 $this->mBlockedby
= $block->mBy
;
392 $this->mBlockreason
= $block->mReason
;
397 if ( !$this->isSysop() && !in_array( $wgIP, $wgProxyWhitelist ) ) {
400 if ( array_key_exists( $wgIP, $wgProxyList ) ) {
401 $this->mBlockedby
= wfMsg( 'proxyblocker' );
402 $this->mBlockreason
= wfMsg( 'proxyblockreason' );
406 if ( !$this->mBlockedby
&& $wgEnableSorbs && !$this->getID() ) {
407 if ( $this->inSorbsBlacklist( $wgIP ) ) {
408 $this->mBlockedby
= wfMsg( 'sorbs' );
409 $this->mBlockreason
= wfMsg( 'sorbsreason' );
415 function inSorbsBlacklist( $ip ) {
416 global $wgEnableSorbs;
417 return $wgEnableSorbs &&
418 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
421 function inOpmBlacklist( $ip ) {
423 return $wgEnableOpm &&
424 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
427 function inDnsBlacklist( $ip, $base ) {
428 $fname = 'User::inDnsBlacklist';
429 wfProfileIn( $fname );
434 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
436 for ( $i=4; $i>=1; $i-- ) {
437 $host .= $m[$i] . '.';
442 $ipList = gethostbynamel( $host );
445 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
448 wfDebug( "Requested $host, not found in $base.\n" );
452 wfProfileOut( $fname );
457 * Primitive rate limits: enforce maximum actions per time period
458 * to put a brake on flooding.
460 * Note: when using a shared cache like memcached, IP-address
461 * last-hit counters will be shared across wikis.
463 * @return bool true if a rate limiter was tripped
466 function pingLimiter( $action='edit' ) {
467 global $wgRateLimits;
468 if( !isset( $wgRateLimits[$action] ) ) {
471 if( $this->isAllowed( 'delete' ) ) {
476 global $wgMemc, $wgIP, $wgDBname, $wgRateLimitLog;
477 $fname = 'User::pingLimiter';
478 $limits = $wgRateLimits[$action];
480 $id = $this->getId();
482 if( isset( $limits['anon'] ) && $id == 0 ) {
483 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
486 if( isset( $limits['user'] ) && $id != 0 ) {
487 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
489 if( $this->isNewbie() ) {
490 if( isset( $limits['newbie'] ) && $id != 0 ) {
491 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
493 if( isset( $limits['ip'] ) ) {
494 $keys["mediawiki:limiter:$action:ip:$wgIP"] = $limits['ip'];
496 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $wgIP, $matches ) ) {
497 $subnet = $matches[1];
498 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
503 foreach( $keys as $key => $limit ) {
504 list( $max, $period ) = $limit;
505 $summary = "(limit $max in {$period}s)";
506 $count = $wgMemc->get( $key );
508 if( $count > $max ) {
509 wfDebug( "$fname: tripped! $key at $count $summary\n" );
510 if( $wgRateLimitLog ) {
511 @error_log
( wfTimestamp( TS_MW
) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
515 wfDebug( "$fname: ok. $key at $count $summary\n" );
518 wfDebug( "$fname: adding record for $key $summary\n" );
519 $wgMemc->add( $key, 1, IntVal( $period ) );
521 $wgMemc->incr( $key );
528 * Check if user is blocked
529 * @return bool True if blocked, false otherwise
531 function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
532 $this->getBlockedStatus( $bFromSlave );
533 return $this->mBlockedby
!== 0;
537 * Check if user is blocked from editing a particular article
539 function isBlockedFrom( $title, $bFromSlave = false ) {
540 global $wgBlockAllowsUTEdit;
541 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
542 $title->getNamespace() == NS_USER_TALK
)
546 return $this->isBlocked( $bFromSlave );
551 * Get name of blocker
552 * @return string name of blocker
554 function blockedBy() {
555 $this->getBlockedStatus();
556 return $this->mBlockedby
;
560 * Get blocking reason
561 * @return string Blocking reason
563 function blockedFor() {
564 $this->getBlockedStatus();
565 return $this->mBlockreason
;
569 * Initialise php session
571 function SetupSession() {
572 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
573 if( $wgSessionsInMemcached ) {
574 require_once( 'MemcachedSessions.php' );
575 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
576 # If it's left on 'user' or another setting from another
577 # application, it will end up failing. Try to recover.
578 ini_set ( 'session.save_handler', 'files' );
580 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
581 session_cache_limiter( 'private, must-revalidate' );
586 * Read datas from session
589 function loadFromSession() {
590 global $wgMemc, $wgDBname;
592 if ( isset( $_SESSION['wsUserID'] ) ) {
593 if ( 0 != $_SESSION['wsUserID'] ) {
594 $sId = $_SESSION['wsUserID'];
598 } else if ( isset( $_COOKIE["{$wgDBname}UserID"] ) ) {
599 $sId = IntVal( $_COOKIE["{$wgDBname}UserID"] );
600 $_SESSION['wsUserID'] = $sId;
604 if ( isset( $_SESSION['wsUserName'] ) ) {
605 $sName = $_SESSION['wsUserName'];
606 } else if ( isset( $_COOKIE["{$wgDBname}UserName"] ) ) {
607 $sName = $_COOKIE["{$wgDBname}UserName"];
608 $_SESSION['wsUserName'] = $sName;
613 $passwordCorrect = FALSE;
614 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
615 if( !is_object( $user ) ||
$user->mVersion
< MW_USER_VERSION
) {
616 # Expire old serialized objects; they may be corrupt.
619 if($makenew = !$user) {
620 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
623 $user->loadFromDatabase();
625 wfDebug( "User::loadFromSession() got from cache!\n" );
628 if ( isset( $_SESSION['wsToken'] ) ) {
629 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken
;
630 } else if ( isset( $_COOKIE["{$wgDBname}Token"] ) ) {
631 $passwordCorrect = $user->mToken
== $_COOKIE["{$wgDBname}Token"];
633 return new User(); # Can't log in from session
636 if ( ( $sName == $user->mName
) && $passwordCorrect ) {
638 if($wgMemc->set( $key, $user ))
639 wfDebug( "User::loadFromSession() successfully saved user\n" );
641 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
645 return new User(); # Can't log in from session
649 * Load a user from the database
651 function loadFromDatabase() {
652 global $wgCommandLineMode;
653 $fname = "User::loadFromDatabase";
655 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
656 # loading in a command line script, don't assume all command line scripts need it like this
657 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
658 if ( $this->mDataLoaded
) {
663 $this->mId
= IntVal( $this->mId
);
665 /** Anonymous user */
668 $this->mRights
= $this->getGroupPermissions( array( '*' ) );
669 $this->mDataLoaded
= true;
671 } # the following stuff is for non-anonymous users only
673 $dbr =& wfGetDB( DB_SLAVE
);
674 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
675 'user_email_authenticated',
676 'user_real_name','user_options','user_touched', 'user_token' ),
677 array( 'user_id' => $this->mId
), $fname );
679 if ( $s !== false ) {
680 $this->mName
= $s->user_name
;
681 $this->mEmail
= $s->user_email
;
682 $this->mEmailAuthenticated
= wfTimestampOrNull( TS_MW
, $s->user_email_authenticated
);
683 $this->mRealName
= $s->user_real_name
;
684 $this->mPassword
= $s->user_password
;
685 $this->mNewpassword
= $s->user_newpassword
;
686 $this->decodeOptions( $s->user_options
);
687 $this->mTouched
= wfTimestamp(TS_MW
,$s->user_touched
);
688 $this->mToken
= $s->user_token
;
690 $res = $dbr->select( 'user_groups',
692 array( 'ug_user' => $this->mId
),
694 $this->mGroups
= array();
695 while( $row = $dbr->fetchObject( $res ) ) {
696 $this->mGroups
[] = $row->ug_group
;
698 $effectiveGroups = array_merge( array( '*', 'user' ), $this->mGroups
);
699 $this->mRights
= $this->getGroupPermissions( $effectiveGroups );
702 $this->mDataLoaded
= true;
705 function getID() { return $this->mId
; }
706 function setID( $v ) {
708 $this->mDataLoaded
= false;
712 $this->loadFromDatabase();
716 function setName( $str ) {
717 $this->loadFromDatabase();
723 * Return the title dbkey form of the name, for eg user pages.
727 function getTitleKey() {
728 return str_replace( ' ', '_', $this->getName() );
731 function getNewtalk() {
733 $fname = 'User::getNewtalk';
734 $this->loadFromDatabase();
736 # Load the newtalk status if it is unloaded (mNewtalk=-1)
737 if( $this->mNewtalk
== -1 ) {
738 $this->mNewtalk
= 0; # reset talk page status
740 # Check memcached separately for anons, who have no
741 # entire User object stored in there.
743 global $wgDBname, $wgMemc;
744 $key = "$wgDBname:newtalk:ip:{$this->mName}";
745 $newtalk = $wgMemc->get( $key );
746 if( is_integer( $newtalk ) ) {
747 $this->mNewtalk
= $newtalk ?
1 : 0;
748 return (bool)$this->mNewtalk
;
752 $dbr =& wfGetDB( DB_SLAVE
);
753 if ( $wgUseEnotif ) {
754 $res = $dbr->select( 'watchlist',
756 array( 'wl_title' => $this->getTitleKey(),
757 'wl_namespace' => NS_USER_TALK
,
758 'wl_user' => $this->mId
,
759 'wl_notificationtimestamp ' . $dbr->notNullTimestamp() ),
760 'User::getNewtalk' );
761 if( $dbr->numRows($res) > 0 ) {
764 $dbr->freeResult( $res );
765 } elseif ( $this->mId
) {
766 $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId
), $fname );
768 if ( $dbr->numRows($res)>0 ) {
771 $dbr->freeResult( $res );
773 $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->mName
), $fname );
774 $this->mNewtalk
= $dbr->numRows( $res ) > 0 ?
1 : 0;
775 $dbr->freeResult( $res );
779 $wgMemc->set( $key, $this->mNewtalk
, time() ); // + 1800 );
783 return ( 0 != $this->mNewtalk
);
786 function setNewtalk( $val ) {
787 $this->loadFromDatabase();
788 $this->mNewtalk
= $val;
789 $this->invalidateCache();
792 function invalidateCache() {
793 global $wgClockSkewFudge;
794 $this->loadFromDatabase();
795 $this->mTouched
= wfTimestamp(TS_MW
, time() +
$wgClockSkewFudge );
796 # Don't forget to save the options after this or
797 # it won't take effect!
800 function validateCache( $timestamp ) {
801 $this->loadFromDatabase();
802 return ($timestamp >= $this->mTouched
);
806 * Encrypt a password.
807 * It can eventuall salt a password @see User::addSalt()
808 * @param string $p clear Password.
809 * @return string Encrypted password.
811 function encryptPassword( $p ) {
812 return wfEncryptPassword( $this->mId
, $p );
815 # Set the password and reset the random token
816 function setPassword( $str ) {
817 $this->loadFromDatabase();
819 $this->mPassword
= $this->encryptPassword( $str );
820 $this->mNewpassword
= '';
823 # Set the random token (used for persistent authentication)
824 function setToken( $token = false ) {
825 global $wgSecretKey, $wgProxyKey, $wgDBname;
827 if ( $wgSecretKey ) {
829 } elseif ( $wgProxyKey ) {
834 $this->mToken
= md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId
);
836 $this->mToken
= $token;
841 function setCookiePassword( $str ) {
842 $this->loadFromDatabase();
843 $this->mCookiePassword
= md5( $str );
846 function setNewpassword( $str ) {
847 $this->loadFromDatabase();
848 $this->mNewpassword
= $this->encryptPassword( $str );
851 function getEmail() {
852 $this->loadFromDatabase();
853 return $this->mEmail
;
856 function getEmailAuthenticationTimestamp() {
857 $this->loadFromDatabase();
858 return $this->mEmailAuthenticated
;
861 function setEmail( $str ) {
862 $this->loadFromDatabase();
863 $this->mEmail
= $str;
866 function getRealName() {
867 $this->loadFromDatabase();
868 return $this->mRealName
;
871 function setRealName( $str ) {
872 $this->loadFromDatabase();
873 $this->mRealName
= $str;
876 function getOption( $oname ) {
877 $this->loadFromDatabase();
878 if ( array_key_exists( $oname, $this->mOptions
) ) {
879 return trim( $this->mOptions
[$oname] );
885 function setOption( $oname, $val ) {
886 $this->loadFromDatabase();
887 if ( $oname == 'skin' ) {
888 # Clear cached skin, so the new one displays immediately in Special:Preferences
889 unset( $this->mSkin
);
891 $this->mOptions
[$oname] = $val;
892 $this->invalidateCache();
895 function getRights() {
896 $this->loadFromDatabase();
897 return $this->mRights
;
901 * Get the list of explicit group memberships this user has.
902 * The implicit * and user groups are not included.
903 * @return array of strings
905 function getGroups() {
906 $this->loadFromDatabase();
907 return $this->mGroups
;
911 * Get the list of implicit group memberships this user has.
912 * This includes all explicit groups, plus 'user' if logged in
913 * and '*' for all accounts.
914 * @return array of strings
916 function getEffectiveGroups() {
917 $base = array( '*' );
918 if( $this->isLoggedIn() ) {
921 return array_merge( $base, $this->getGroups() );
925 * Remove the user from the given group.
926 * This takes immediate effect.
929 function addGroup( $group ) {
930 $dbw =& wfGetDB( DB_MASTER
);
931 $dbw->insert( 'user_groups',
933 'ug_user' => $this->getID(),
934 'ug_group' => $group,
939 $this->mGroups
= array_merge( $this->mGroups
, array( $group ) );
940 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
942 $this->invalidateCache();
943 $this->saveSettings();
947 * Remove the user from the given group.
948 * This takes immediate effect.
951 function removeGroup( $group ) {
952 $dbw =& wfGetDB( DB_MASTER
);
953 $dbw->delete( 'user_groups',
955 'ug_user' => $this->getID(),
956 'ug_group' => $group,
958 'User::removeGroup' );
960 $this->mGroups
= array_diff( $this->mGroups
, array( $group ) );
961 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
963 $this->invalidateCache();
964 $this->saveSettings();
969 * A more legible check for non-anonymousness.
970 * Returns true if the user is not an anonymous visitor.
974 function isLoggedIn() {
975 return( $this->getID() != 0 );
979 * A more legible check for anonymousness.
980 * Returns true if the user is an anonymous visitor.
985 return !$this->isLoggedIn();
989 * Check if a user is sysop
990 * Die with backtrace. Use User:isAllowed() instead.
994 return $this->isAllowed( 'protect' );
998 function isDeveloper() {
999 return $this->isAllowed( 'siteadmin' );
1003 function isBureaucrat() {
1004 return $this->isAllowed( 'makesysop' );
1008 * Whether the user is a bot
1009 * @todo need to be migrated to the new user level management sytem
1012 $this->loadFromDatabase();
1013 return in_array( 'bot', $this->mRights
);
1017 * Check if user is allowed to access a feature / make an action
1018 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1019 * @return boolean True: action is allowed, False: action should not be allowed
1021 function isAllowed($action='') {
1022 $this->loadFromDatabase();
1023 return in_array( $action , $this->mRights
);
1027 * Load a skin if it doesn't exist or return it
1028 * @todo FIXME : need to check the old failback system [AV]
1030 function &getSkin() {
1031 global $IP, $wgRequest;
1032 if ( ! isset( $this->mSkin
) ) {
1033 $fname = 'User::getSkin';
1034 wfProfileIn( $fname );
1036 # get all skin names available
1037 $skinNames = Skin
::getSkinNames();
1040 $userSkin = $this->getOption( 'skin' );
1041 $userSkin = $wgRequest->getText('useskin', $userSkin);
1042 if ( $userSkin == '' ) { $userSkin = 'standard'; }
1044 if ( !isset( $skinNames[$userSkin] ) ) {
1045 # in case the user skin could not be found find a replacement
1049 2 => 'CologneBlue');
1050 # if phptal is enabled we should have monobook skin that
1051 # superseed the good old SkinStandard.
1052 if ( isset( $skinNames['monobook'] ) ) {
1053 $fallback[0] = 'MonoBook';
1056 if(is_numeric($userSkin) && isset( $fallback[$userSkin]) ){
1057 $sn = $fallback[$userSkin];
1062 # The user skin is available
1063 $sn = $skinNames[$userSkin];
1066 # Grab the skin class and initialise it. Each skin checks for PHPTal
1067 # and will not load if it's not enabled.
1068 require_once( $IP.'/skins/'.$sn.'.php' );
1070 # Check if we got if not failback to default skin
1071 $className = 'Skin'.$sn;
1072 if( !class_exists( $className ) ) {
1073 # DO NOT die if the class isn't found. This breaks maintenance
1074 # scripts and can cause a user account to be unrecoverable
1075 # except by SQL manipulation if a previously valid skin name
1076 # is no longer valid.
1077 $className = 'SkinStandard';
1078 require_once( $IP.'/skins/Standard.php' );
1080 $this->mSkin
=& new $className;
1081 wfProfileOut( $fname );
1083 return $this->mSkin
;
1087 * @param string $title Article title to look at
1091 * Check watched status of an article
1092 * @return bool True if article is watched
1094 function isWatched( $title ) {
1095 $wl = WatchedItem
::fromUserTitle( $this, $title );
1096 return $wl->isWatched();
1102 function addWatch( $title ) {
1103 $wl = WatchedItem
::fromUserTitle( $this, $title );
1105 $this->invalidateCache();
1109 * Stop watching an article
1111 function removeWatch( $title ) {
1112 $wl = WatchedItem
::fromUserTitle( $this, $title );
1114 $this->invalidateCache();
1118 * Clear the user's notification timestamp for the given title.
1119 * If e-notif e-mails are on, they will receive notification mails on
1120 * the next change of the page if it's watched etc.
1122 function clearNotification( &$title ) {
1123 global $wgUser, $wgUseEnotif;
1125 if ( !$wgUseEnotif ) {
1129 $userid = $this->getID();
1134 // Only update the timestamp if the page is being watched.
1135 // The query to find out if it is watched is cached both in memcached and per-invocation,
1136 // and when it does have to be executed, it can be on a slave
1137 // If this is the user's newtalk page, we always update the timestamp
1138 if ($title->getNamespace() == NS_USER_TALK
&&
1139 $title->getText() == $wgUser->getName())
1142 } elseif ( $this->getID() == $wgUser->getID() ) {
1143 $watched = $title->userIsWatching();
1148 // If the page is watched by the user (or may be watched), update the timestamp on any
1149 // any matching rows
1151 $dbw =& wfGetDB( DB_MASTER
);
1152 $success = $dbw->update( 'watchlist',
1154 'wl_notificationtimestamp' => NULL
1155 ), array( /* WHERE */
1156 'wl_title' => $title->getDBkey(),
1157 'wl_namespace' => $title->getNamespace(),
1158 'wl_user' => $this->getID()
1159 ), 'User::clearLastVisited'
1167 * Resets all of the given user's page-change notification timestamps.
1168 * If e-notif e-mails are on, they will receive notification mails on
1169 * the next change of any watched page.
1171 * @param int $currentUser user ID number
1174 function clearAllNotifications( $currentUser ) {
1175 global $wgUseEnotif;
1176 if ( !$wgUseEnotif ) {
1179 if( $currentUser != 0 ) {
1181 $dbw =& wfGetDB( DB_MASTER
);
1182 $success = $dbw->update( 'watchlist',
1184 'wl_notificationtimestamp' => 0
1185 ), array( /* WHERE */
1186 'wl_user' => $currentUser
1187 ), 'UserMailer::clearAll'
1190 # we also need to clear here the "you have new message" notification for the own user_talk page
1191 # This is cleared one page view later in Article::viewUpdates();
1197 * @return string Encoding options
1199 function encodeOptions() {
1201 foreach ( $this->mOptions
as $oname => $oval ) {
1202 array_push( $a, $oname.'='.$oval );
1204 $s = implode( "\n", $a );
1211 function decodeOptions( $str ) {
1212 $a = explode( "\n", $str );
1213 foreach ( $a as $s ) {
1214 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1215 $this->mOptions
[$m[1]] = $m[2];
1220 function setCookies() {
1221 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgDBname;
1222 if ( 0 == $this->mId
) return;
1223 $this->loadFromDatabase();
1224 $exp = time() +
$wgCookieExpiration;
1226 $_SESSION['wsUserID'] = $this->mId
;
1227 setcookie( $wgDBname.'UserID', $this->mId
, $exp, $wgCookiePath, $wgCookieDomain );
1229 $_SESSION['wsUserName'] = $this->mName
;
1230 setcookie( $wgDBname.'UserName', $this->mName
, $exp, $wgCookiePath, $wgCookieDomain );
1232 $_SESSION['wsToken'] = $this->mToken
;
1233 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1234 setcookie( $wgDBname.'Token', $this->mToken
, $exp, $wgCookiePath, $wgCookieDomain );
1236 setcookie( $wgDBname.'Token', '', time() - 3600 );
1242 * It will clean the session cookie
1245 global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP;
1246 $this->loadDefaults();
1247 $this->setLoaded( true );
1249 $_SESSION['wsUserID'] = 0;
1251 setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1252 setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1254 # Remember when user logged out, to prevent seeing cached pages
1255 setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() +
86400, $wgCookiePath, $wgCookieDomain );
1259 * Save object settings into database
1261 function saveSettings() {
1262 global $wgMemc, $wgDBname, $wgUseEnotif;
1263 $fname = 'User::saveSettings';
1265 if ( wfReadOnly() ) { return; }
1266 $this->saveNewtalk();
1267 if ( 0 == $this->mId
) { return; }
1269 $dbw =& wfGetDB( DB_MASTER
);
1270 $dbw->update( 'user',
1272 'user_name' => $this->mName
,
1273 'user_password' => $this->mPassword
,
1274 'user_newpassword' => $this->mNewpassword
,
1275 'user_real_name' => $this->mRealName
,
1276 'user_email' => $this->mEmail
,
1277 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1278 'user_options' => $this->encodeOptions(),
1279 'user_touched' => $dbw->timestamp($this->mTouched
),
1280 'user_token' => $this->mToken
1281 ), array( /* WHERE */
1282 'user_id' => $this->mId
1285 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1289 * Save value of new talk flag.
1291 function saveNewtalk() {
1292 global $wgDBname, $wgMemc, $wgUseEnotif;
1294 $fname = 'User::saveNewtalk';
1298 if ( wfReadOnly() ) { return ; }
1299 $dbr =& wfGetDB( DB_SLAVE
);
1300 $dbw =& wfGetDB( DB_MASTER
);
1302 if ( $wgUseEnotif ) {
1303 if ( ! $this->getNewtalk() ) {
1304 # Delete the watchlist entry for user_talk page X watched by user X
1305 $dbw->delete( 'watchlist',
1306 array( 'wl_user' => $this->mId
,
1307 'wl_title' => $this->getTitleKey(),
1308 'wl_namespace' => NS_USER_TALK
),
1310 if ( $dbw->affectedRows() ) {
1314 # Anon users have a separate memcache space for newtalk
1315 # since they don't store their own info. Trim...
1316 $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" );
1320 if ($this->getID() != 0) {
1322 $value = $this->getID();
1326 $value = $this->mName
;
1327 $key = "$wgDBname:newtalk:ip:$this->mName";
1330 $dbr =& wfGetDB( DB_SLAVE
);
1331 $dbw =& wfGetDB( DB_MASTER
);
1333 $res = $dbr->selectField('user_newtalk', $field,
1334 array($field => $value), $fname);
1337 if ($res !== false && $this->mNewtalk
== 0) {
1338 $dbw->delete('user_newtalk', array($field => $value), $fname);
1340 $wgMemc->set( $key, 0 );
1342 } else if ($res === false && $this->mNewtalk
== 1) {
1343 $dbw->insert('user_newtalk', array($field => $value), $fname);
1345 $wgMemc->set( $key, 1 );
1352 # Update user_touched, so that newtalk notifications in the client cache are invalidated
1353 if ( $changed && $this->getID() ) {
1354 $dbw->update('user',
1355 /*SET*/ array( 'user_touched' => $this->mTouched
),
1356 /*WHERE*/ array( 'user_id' => $this->getID() ),
1358 $wgMemc->set( "$wgDBname:user:id:{$this->mId}", $this, 86400 );
1363 * Checks if a user with the given name exists, returns the ID
1365 function idForName() {
1366 $fname = 'User::idForName';
1369 $s = trim( $this->mName
);
1370 if ( 0 == strcmp( '', $s ) ) return 0;
1372 $dbr =& wfGetDB( DB_SLAVE
);
1373 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1374 if ( $id === false ) {
1381 * Add user object to the database
1383 function addToDatabase() {
1384 $fname = 'User::addToDatabase';
1385 $dbw =& wfGetDB( DB_MASTER
);
1386 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1387 $dbw->insert( 'user',
1389 'user_id' => $seqVal,
1390 'user_name' => $this->mName
,
1391 'user_password' => $this->mPassword
,
1392 'user_newpassword' => $this->mNewpassword
,
1393 'user_email' => $this->mEmail
,
1394 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1395 'user_real_name' => $this->mRealName
,
1396 'user_options' => $this->encodeOptions(),
1397 'user_token' => $this->mToken
1400 $this->mId
= $dbw->insertId();
1403 function spreadBlock() {
1405 # If the (non-anonymous) user is blocked, this function will block any IP address
1406 # that they successfully log on from.
1407 $fname = 'User::spreadBlock';
1409 wfDebug( "User:spreadBlock()\n" );
1410 if ( $this->mId
== 0 ) {
1414 $userblock = Block
::newFromDB( '', $this->mId
);
1415 if ( !$userblock->isValid() ) {
1419 # Check if this IP address is already blocked
1420 $ipblock = Block
::newFromDB( $wgIP );
1421 if ( $ipblock->isValid() ) {
1422 # Just update the timestamp
1423 $ipblock->updateTimestamp();
1427 # Make a new block object with the desired properties
1428 wfDebug( "Autoblocking {$this->mName}@{$wgIP}\n" );
1429 $ipblock->mAddress
= $wgIP;
1430 $ipblock->mUser
= 0;
1431 $ipblock->mBy
= $userblock->mBy
;
1432 $ipblock->mReason
= wfMsg( 'autoblocker', $this->getName(), $userblock->mReason
);
1433 $ipblock->mTimestamp
= wfTimestampNow();
1434 $ipblock->mAuto
= 1;
1435 # If the user is already blocked with an expiry date, we don't
1436 # want to pile on top of that!
1437 if($userblock->mExpiry
) {
1438 $ipblock->mExpiry
= min ( $userblock->mExpiry
, Block
::getAutoblockExpiry( $ipblock->mTimestamp
));
1440 $ipblock->mExpiry
= Block
::getAutoblockExpiry( $ipblock->mTimestamp
);
1448 function getPageRenderingHash() {
1451 return $this->mHash
;
1454 // stubthreshold is only included below for completeness,
1455 // it will always be 0 when this function is called by parsercache.
1457 $confstr = $this->getOption( 'math' );
1458 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1459 $confstr .= '!' . $this->getOption( 'date' );
1460 $confstr .= '!' . $this->getOption( 'numberheadings' );
1461 $confstr .= '!' . $this->getOption( 'language' );
1462 $confstr .= '!' . $this->getOption( 'thumbsize' );
1463 // add in language specific options, if any
1464 $extra = $wgContLang->getExtraHashOptions();
1467 $this->mHash
= $confstr;
1471 function isAllowedToCreateAccount() {
1472 return $this->isAllowed( 'createaccount' );
1476 * Set mDataLoaded, return previous value
1477 * Use this to prevent DB access in command-line scripts or similar situations
1479 function setLoaded( $loaded ) {
1480 return wfSetVar( $this->mDataLoaded
, $loaded );
1484 * Get this user's personal page title.
1489 function getUserPage() {
1490 return Title
::makeTitle( NS_USER
, $this->mName
);
1494 * Get this user's talk page title.
1499 function getTalkPage() {
1500 $title = $this->getUserPage();
1501 return $title->getTalkPage();
1507 function getMaxID() {
1508 $dbr =& wfGetDB( DB_SLAVE
);
1509 return $dbr->selectField( 'user', 'max(user_id)', false );
1513 * Determine whether the user is a newbie. Newbies are either
1514 * anonymous IPs, or the 1% most recently created accounts.
1515 * Bots and sysops are excluded.
1516 * @return bool True if it is a newbie.
1518 function isNewbie() {
1519 return $this->isAnon() ||
$this->mId
> User
::getMaxID() * 0.99 && !$this->isAllowed( 'delete' ) && !$this->isBot();
1523 * Check to see if the given clear-text password is one of the accepted passwords
1524 * @param string $password User password.
1525 * @return bool True if the given password is correct otherwise False.
1527 function checkPassword( $password ) {
1528 global $wgAuth, $wgMinimalPasswordLength;
1529 $this->loadFromDatabase();
1531 // Even though we stop people from creating passwords that
1532 // are shorter than this, doesn't mean people wont be able
1533 // to. Certain authentication plugins do NOT want to save
1534 // domain passwords in a mysql database, so we should
1535 // check this (incase $wgAuth->strict() is false).
1536 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1540 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1542 } elseif( $wgAuth->strict() ) {
1543 /* Auth plugin doesn't allow local authentication */
1546 $ep = $this->encryptPassword( $password );
1547 if ( 0 == strcmp( $ep, $this->mPassword
) ) {
1549 } elseif ( ($this->mNewpassword
!= '') && (0 == strcmp( $ep, $this->mNewpassword
)) ) {
1551 } elseif ( function_exists( 'iconv' ) ) {
1552 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1553 # Check for this with iconv
1554 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1555 if ( 0 == strcmp( $cp1252hash, $this->mPassword
) ) {
1563 * Initialize (if necessary) and return a session token value
1564 * which can be used in edit forms to show that the user's
1565 * login credentials aren't being hijacked with a foreign form
1568 * @param mixed $salt - Optional function-specific data for hash.
1569 * Use a string or an array of strings.
1573 function editToken( $salt = '' ) {
1574 if( !isset( $_SESSION['wsEditToken'] ) ) {
1575 $token = $this->generateToken();
1576 $_SESSION['wsEditToken'] = $token;
1578 $token = $_SESSION['wsEditToken'];
1580 if( is_array( $salt ) ) {
1581 $salt = implode( '|', $salt );
1583 return md5( $token . $salt );
1587 * Generate a hex-y looking random token for various uses.
1588 * Could be made more cryptographically sure if someone cares.
1591 function generateToken( $salt = '' ) {
1592 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1593 return md5( $token . $salt );
1597 * Check given value against the token value stored in the session.
1598 * A match should confirm that the form was submitted from the
1599 * user's own login session, not a form submission from a third-party
1602 * @param string $val - the input value to compare
1603 * @param string $salt - Optional function-specific data for hash
1607 function matchEditToken( $val, $salt = '' ) {
1611 if ( !isset( $_SESSION['wsEditToken'] ) ) {
1612 $logfile = '/home/wikipedia/logs/session_debug/session.log';
1613 $mckey = memsess_key( session_id() );
1614 $uname = @posix_uname();
1615 $msg = "wsEditToken not set!\n" .
1616 'apache server=' . $uname['nodename'] . "\n" .
1617 'session_id = ' . session_id() . "\n" .
1618 '$_SESSION=' . var_export( $_SESSION, true ) . "\n" .
1619 '$_COOKIE=' . var_export( $_COOKIE, true ) . "\n" .
1620 "mc get($mckey) = " . var_export( $wgMemc->get( $mckey ), true ) . "\n\n\n";
1622 @error_log( $msg, 3, $logfile );
1625 return ( $val == $this->editToken( $salt ) );
1629 * Generate a new e-mail confirmation token and send a confirmation
1630 * mail to the user's given address.
1632 * @return mixed True on success, a WikiError object on failure.
1634 function sendConfirmationMail() {
1635 global $wgIP, $wgContLang;
1636 $url = $this->confirmationTokenUrl( $expiration );
1637 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1638 wfMsg( 'confirmemail_body',
1642 $wgContLang->timeanddate( $expiration, false ) ) );
1646 * Send an e-mail to this user's account. Does not check for
1647 * confirmed status or validity.
1649 * @param string $subject
1650 * @param string $body
1651 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1652 * @return mixed True on success, a WikiError object on failure.
1654 function sendMail( $subject, $body, $from = null ) {
1655 if( is_null( $from ) ) {
1656 global $wgPasswordSender;
1657 $from = $wgPasswordSender;
1660 require_once( 'UserMailer.php' );
1661 $error = userMailer( $this->getEmail(), $from, $subject, $body );
1663 if( $error == '' ) {
1666 return new WikiError( $error );
1671 * Generate, store, and return a new e-mail confirmation code.
1672 * A hash (unsalted since it's used as a key) is stored.
1673 * @param &$expiration mixed output: accepts the expiration time
1677 function confirmationToken( &$expiration ) {
1678 $fname = 'User::confirmationToken';
1681 $expires = $now +
7 * 24 * 60 * 60;
1682 $expiration = wfTimestamp( TS_MW
, $expires );
1684 $token = $this->generateToken( $this->mId
. $this->mEmail
. $expires );
1685 $hash = md5( $token );
1687 $dbw =& wfGetDB( DB_MASTER
);
1688 $dbw->update( 'user',
1689 array( 'user_email_token' => $hash,
1690 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1691 array( 'user_id' => $this->mId
),
1698 * Generate and store a new e-mail confirmation token, and return
1699 * the URL the user can use to confirm.
1700 * @param &$expiration mixed output: accepts the expiration time
1704 function confirmationTokenUrl( &$expiration ) {
1705 $token = $this->confirmationToken( $expiration );
1706 $title = Title
::makeTitle( NS_SPECIAL
, 'Confirmemail/' . $token );
1707 return $title->getFullUrl();
1711 * Mark the e-mail address confirmed and save.
1713 function confirmEmail() {
1714 $this->loadFromDatabase();
1715 $this->mEmailAuthenticated
= wfTimestampNow();
1716 $this->saveSettings();
1721 * Is this user allowed to send e-mails within limits of current
1722 * site configuration?
1725 function canSendEmail() {
1726 return $this->isEmailConfirmed();
1730 * Is this user allowed to receive e-mails within limits of current
1731 * site configuration?
1734 function canReceiveEmail() {
1735 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1739 * Is this user's e-mail address valid-looking and confirmed within
1740 * limits of the current site configuration?
1742 * If $wgEmailAuthentication is on, this may require the user to have
1743 * confirmed their address by returning a code or using a password
1744 * sent to the address from the wiki.
1748 function isEmailConfirmed() {
1749 global $wgEmailAuthentication;
1750 $this->loadFromDatabase();
1751 if( $this->isAnon() )
1753 if( !$this->isValidEmailAddr( $this->mEmail
) )
1755 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1761 * @param array $groups list of groups
1762 * @return array list of permission key names for given groups combined
1765 function getGroupPermissions( $groups ) {
1766 global $wgGroupPermissions;
1768 foreach( $groups as $group ) {
1769 if( isset( $wgGroupPermissions[$group] ) ) {
1770 $rights = array_merge( $rights,
1771 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1778 * @param string $group key name
1779 * @return string localized descriptive name, if provided
1782 function getGroupName( $group ) {
1783 $key = "group-$group-name";
1784 $name = wfMsg( $key );
1785 if( $name == '' ||
$name == "<$key>" ) {
1793 * Return the set of defined explicit groups.
1794 * The * and 'user' groups are not included.
1798 function getAllGroups() {
1799 global $wgGroupPermissions;
1801 array_keys( $wgGroupPermissions ),
1802 array( '*', 'user' ) );