11 require_once( 'WatchedItem.php' );
13 # Number of characters in user_token field
14 define( 'USER_TOKEN_LENGTH', 32 );
16 # Serialized record version
17 define( 'MW_USER_VERSION', 2 );
27 var $mId, $mName, $mPassword, $mEmail, $mNewtalk;
28 var $mEmailAuthenticated;
29 var $mRights, $mOptions;
30 var $mDataLoaded, $mNewpassword;
32 var $mBlockedby, $mBlockreason;
38 var $mVersion; // serialized version
40 /** Construct using User:loadDefaults() */
42 $this->loadDefaults();
43 $this->mVersion
= MW_USER_VERSION
;
47 * Static factory method
48 * @param string $name Username, validated by Title:newFromText()
52 function newFromName( $name ) {
55 # Force usernames to capital
57 $name = $wgContLang->ucfirst( $name );
59 # Clean up name according to title rules
60 $t = Title
::newFromText( $name );
65 # Reject various classes of invalid names
66 $canonicalName = $t->getText();
67 if( !User
::isValidUserName( $canonicalName ) ) {
71 $u->setName( $canonicalName );
72 $u->setId( $u->idFromName( $t->getText() ) );
77 * Factory method to fetch whichever use has a given email confirmation code.
78 * This code is generated when an account is created or its e-mail address
81 * If the code is invalid or has expired, returns NULL.
87 function newFromConfirmationCode( $code ) {
88 $dbr =& wfGetDB( DB_SLAVE
);
89 $name = $dbr->selectField( 'user', 'user_name', array(
90 'user_email_token' => md5( $code ),
91 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
93 if( is_string( $name ) ) {
94 return User
::newFromName( $name );
101 * Serialze sleep function, for better cache efficiency and avoidance of
102 * silly "incomplete type" errors when skins are cached
105 return array( 'mId', 'mName', 'mPassword', 'mEmail', 'mNewtalk',
106 'mEmailAuthenticated', 'mRights', 'mOptions', 'mDataLoaded',
107 'mNewpassword', 'mBlockedby', 'mBlockreason', 'mTouched',
108 'mToken', 'mRealName', 'mHash', 'mGroups' );
112 * Get username given an id.
113 * @param integer $id Database user id
114 * @return string Nickname of a user
117 function whoIs( $id ) {
118 $dbr =& wfGetDB( DB_SLAVE
);
119 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ) );
123 * Get real username given an id.
124 * @param integer $id Database user id
125 * @return string Realname of a user
128 function whoIsReal( $id ) {
129 $dbr =& wfGetDB( DB_SLAVE
);
130 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ) );
134 * Get database id given a user name
135 * @param string $name Nickname of a user
136 * @return integer|null Database user id (null: if non existent
139 function idFromName( $name ) {
140 $fname = "User::idFromName";
142 $nt = Title
::newFromText( $name );
143 if( is_null( $nt ) ) {
147 $dbr =& wfGetDB( DB_SLAVE
);
148 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
150 if ( $s === false ) {
158 * does the string match an anonymous IPv4 address?
161 * @param string $name Nickname of a user
164 function isIP( $name ) {
165 return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/",$name);
166 /*return preg_match("/^
167 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
168 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
169 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
170 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
175 * Is the input a valid username?
177 * Checks if the input is a valid username, we don't want an empty string,
178 * an IP address, anything that containins slashes (would mess up subpages),
179 * is longer than the maximum allowed username size or doesn't begin with
182 * @param string $name
186 function isValidUserName( $name ) {
187 global $wgContLang, $wgMaxNameChars;
190 || User
::isIP( $name )
191 ||
strpos( $name, '/' ) !== false
192 ||
strlen( $name ) > $wgMaxNameChars
193 ||
$name != $wgContLang->ucfirst( $name ) )
200 * Is the input a valid password?
202 * @param string $password
206 function isValidPassword( $password ) {
207 global $wgMinimalPasswordLength;
208 return strlen( $password ) >= $wgMinimalPasswordLength;
212 * does the string match roughly an email address ?
214 * @todo Check for RFC 2822 compilance
217 * @param string $addr email address
221 function isValidEmailAddr ( $addr ) {
222 # There used to be a regular expression here, it got removed because it
223 # rejected valid addresses.
224 return ( trim( $addr ) != '' ) &&
225 (false !== strpos( $addr, '@' ) );
229 * Count the number of edits of a user
231 * @param int $uid The user ID to check
234 function edits( $uid ) {
235 $fname = 'User::editCount';
237 $dbr =& wfGetDB( DB_SLAVE
);
238 return $dbr->selectField(
239 'revision', 'count(*)',
240 array( 'rev_user' => $uid ),
246 * probably return a random password
247 * @return string probably a random password
249 * @todo Check what is doing really [AV]
251 function randomPassword() {
252 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
253 $l = strlen( $pwchars ) - 1;
255 $np = $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
256 $pwchars{mt_rand( 0, $l )} . chr( mt_rand(48, 57) ) .
257 $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
258 $pwchars{mt_rand( 0, $l )};
263 * Set properties to default
264 * Used at construction. It will load per language default settings only
265 * if we have an available language object.
267 function loadDefaults() {
270 $fname = 'User::loadDefaults' . $n;
271 wfProfileIn( $fname );
273 global $wgContLang, $wgIP, $wgDBname;
274 global $wgNamespacesToBeSearchedDefault;
277 $this->mNewtalk
= -1;
278 $this->mName
= $wgIP;
279 $this->mRealName
= $this->mEmail
= '';
280 $this->mEmailAuthenticated
= null;
281 $this->mPassword
= $this->mNewpassword
= '';
282 $this->mRights
= array();
283 $this->mGroups
= array();
284 $this->mOptions
= User
::getDefaultOptions();
286 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
287 $this->mOptions
['searchNs'.$nsnum] = $val;
289 unset( $this->mSkin
);
290 $this->mDataLoaded
= false;
291 $this->mBlockedby
= -1; # Unset
292 $this->setToken(); # Random
293 $this->mHash
= false;
295 if ( isset( $_COOKIE[$wgDBname.'LoggedOut'] ) ) {
296 $this->mTouched
= wfTimestamp( TS_MW
, $_COOKIE[$wgDBname.'LoggedOut'] );
299 $this->mTouched
= '0'; # Allow any pages to be cached
302 wfProfileOut( $fname );
306 * Combine the language default options with any site-specific options
307 * and add the default language variants.
313 function getDefaultOptions() {
315 * Site defaults will override the global/language defaults
317 global $wgContLang, $wgDefaultUserOptions;
318 $defOpt = $wgDefaultUserOptions +
$wgContLang->getDefaultUserOptions();
321 * default language setting
323 $variant = $wgContLang->getPreferredVariant();
324 $defOpt['variant'] = $variant;
325 $defOpt['language'] = $variant;
331 * Get a given default option value.
338 function getDefaultOption( $opt ) {
339 $defOpts = User
::getDefaultOptions();
340 if( isset( $defOpts[$opt] ) ) {
341 return $defOpts[$opt];
348 * Get blocking information
350 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
351 * non-critical checks are done against slaves. Check when actually saving should be done against
354 * Note that even if $bFromSlave is false, the check is done first against slave, then master.
355 * The logic is that if blocked on slave, we'll assume it's either blocked on master or
356 * just slightly outta sync and soon corrected - safer to block slightly more that less.
357 * And it's cheaper to check slave first, then master if needed, than master always.
359 function getBlockedStatus( $bFromSlave = true ) {
360 global $wgIP, $wgBlockCache, $wgProxyList, $wgEnableSorbs, $wgProxyWhitelist;
362 if ( -1 != $this->mBlockedby
) { return; }
364 $this->mBlockedby
= 0;
368 $block = new Block();
369 $block->forUpdate( $bFromSlave );
370 if ( $block->load( $wgIP , $this->mId
) ) {
371 $this->mBlockedby
= $block->mBy
;
372 $this->mBlockreason
= $block->mReason
;
373 $this->spreadBlock();
378 if ( !$this->mBlockedby
) {
379 # Check first against slave, and optionally from master.
380 $block = $wgBlockCache->get( $wgIP, true );
381 if ( !$block && !$bFromSlave )
383 # Not blocked: check against master, to make sure.
384 $wgBlockCache->clearLocal( );
385 $block = $wgBlockCache->get( $wgIP, false );
387 if ( $block !== false ) {
388 $this->mBlockedby
= $block->mBy
;
389 $this->mBlockreason
= $block->mReason
;
394 if ( !$this->isSysop() && !in_array( $wgIP, $wgProxyWhitelist ) ) {
397 if ( array_key_exists( $wgIP, $wgProxyList ) ) {
398 $this->mBlockedby
= wfMsg( 'proxyblocker' );
399 $this->mBlockreason
= wfMsg( 'proxyblockreason' );
403 if ( !$this->mBlockedby
&& $wgEnableSorbs && !$this->getID() ) {
404 if ( $this->inSorbsBlacklist( $wgIP ) ) {
405 $this->mBlockedby
= wfMsg( 'sorbs' );
406 $this->mBlockreason
= wfMsg( 'sorbsreason' );
412 function inSorbsBlacklist( $ip ) {
413 global $wgEnableSorbs;
414 return $wgEnableSorbs &&
415 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
418 function inOpmBlacklist( $ip ) {
420 return $wgEnableOpm &&
421 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
424 function inDnsBlacklist( $ip, $base ) {
425 $fname = 'User::inDnsBlacklist';
426 wfProfileIn( $fname );
431 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
433 for ( $i=4; $i>=1; $i-- ) {
434 $host .= $m[$i] . '.';
439 $ipList = gethostbynamel( $host );
442 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
445 wfDebug( "Requested $host, not found in $base.\n" );
449 wfProfileOut( $fname );
454 * Primitive rate limits: enforce maximum actions per time period
455 * to put a brake on flooding.
457 * Note: when using a shared cache like memcached, IP-address
458 * last-hit counters will be shared across wikis.
460 * @return bool true if a rate limiter was tripped
463 function pingLimiter( $action='edit' ) {
464 global $wgRateLimits;
465 if( !isset( $wgRateLimits[$action] ) ) {
468 if( $this->isAllowed( 'delete' ) ) {
473 global $wgMemc, $wgIP, $wgDBname, $wgRateLimitLog;
474 $fname = 'User::pingLimiter';
475 $limits = $wgRateLimits[$action];
477 $id = $this->getId();
479 if( isset( $limits['anon'] ) && $id == 0 ) {
480 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
483 if( isset( $limits['user'] ) && $id != 0 ) {
484 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
486 if( $this->isNewbie() ) {
487 if( isset( $limits['newbie'] ) && $id != 0 ) {
488 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
490 if( isset( $limits['ip'] ) ) {
491 $keys["mediawiki:limiter:$action:ip:$wgIP"] = $limits['ip'];
493 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $wgIP, $matches ) ) {
494 $subnet = $matches[1];
495 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
500 foreach( $keys as $key => $limit ) {
501 list( $max, $period ) = $limit;
502 $summary = "(limit $max in {$period}s)";
503 $count = $wgMemc->get( $key );
505 if( $count > $max ) {
506 wfDebug( "$fname: tripped! $key at $count $summary\n" );
507 if( $wgRateLimitLog ) {
508 @error_log
( wfTimestamp( TS_MW
) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
512 wfDebug( "$fname: ok. $key at $count $summary\n" );
515 wfDebug( "$fname: adding record for $key $summary\n" );
516 $wgMemc->add( $key, 1, IntVal( $period ) );
518 $wgMemc->incr( $key );
525 * Check if user is blocked
526 * @return bool True if blocked, false otherwise
528 function isBlocked( $bFromSlave = false ) {
529 $this->getBlockedStatus( $bFromSlave );
530 return $this->mBlockedby
!== 0;
534 * Check if user is blocked from editing a particular article
536 function isBlockedFrom( $title, $bFromSlave = false ) {
537 global $wgBlockAllowsUTEdit;
538 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
539 $title->getNamespace() == NS_USER_TALK
)
543 return $this->isBlocked( $bFromSlave );
548 * Get name of blocker
549 * @return string name of blocker
551 function blockedBy() {
552 $this->getBlockedStatus();
553 return $this->mBlockedby
;
557 * Get blocking reason
558 * @return string Blocking reason
560 function blockedFor() {
561 $this->getBlockedStatus();
562 return $this->mBlockreason
;
566 * Initialise php session
568 function SetupSession() {
569 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
570 if( $wgSessionsInMemcached ) {
571 require_once( 'MemcachedSessions.php' );
572 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
573 # If it's left on 'user' or another setting from another
574 # application, it will end up failing. Try to recover.
575 ini_set ( 'session.save_handler', 'files' );
577 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
578 session_cache_limiter( 'private, must-revalidate' );
583 * Read datas from session
586 function loadFromSession() {
587 global $wgMemc, $wgDBname;
589 if ( isset( $_SESSION['wsUserID'] ) ) {
590 if ( 0 != $_SESSION['wsUserID'] ) {
591 $sId = $_SESSION['wsUserID'];
595 } else if ( isset( $_COOKIE["{$wgDBname}UserID"] ) ) {
596 $sId = IntVal( $_COOKIE["{$wgDBname}UserID"] );
597 $_SESSION['wsUserID'] = $sId;
601 if ( isset( $_SESSION['wsUserName'] ) ) {
602 $sName = $_SESSION['wsUserName'];
603 } else if ( isset( $_COOKIE["{$wgDBname}UserName"] ) ) {
604 $sName = $_COOKIE["{$wgDBname}UserName"];
605 $_SESSION['wsUserName'] = $sName;
610 $passwordCorrect = FALSE;
611 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
612 if( !is_object( $user ) ||
$user->mVersion
< MW_USER_VERSION
) {
613 # Expire old serialized objects; they may be corrupt.
616 if($makenew = !$user) {
617 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
620 $user->loadFromDatabase();
622 wfDebug( "User::loadFromSession() got from cache!\n" );
625 if ( isset( $_SESSION['wsToken'] ) ) {
626 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken
;
627 } else if ( isset( $_COOKIE["{$wgDBname}Token"] ) ) {
628 $passwordCorrect = $user->mToken
== $_COOKIE["{$wgDBname}Token"];
630 return new User(); # Can't log in from session
633 if ( ( $sName == $user->mName
) && $passwordCorrect ) {
635 if($wgMemc->set( $key, $user ))
636 wfDebug( "User::loadFromSession() successfully saved user\n" );
638 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
642 return new User(); # Can't log in from session
646 * Load a user from the database
648 function loadFromDatabase() {
649 global $wgCommandLineMode;
650 $fname = "User::loadFromDatabase";
652 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
653 # loading in a command line script, don't assume all command line scripts need it like this
654 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
655 if ( $this->mDataLoaded
) {
660 $this->mId
= IntVal( $this->mId
);
662 /** Anonymous user */
665 $this->mRights
= $this->getGroupPermissions( array( '*' ) );
666 $this->mDataLoaded
= true;
668 } # the following stuff is for non-anonymous users only
670 $dbr =& wfGetDB( DB_SLAVE
);
671 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
672 'user_email_authenticated',
673 'user_real_name','user_options','user_touched', 'user_token' ),
674 array( 'user_id' => $this->mId
), $fname );
676 if ( $s !== false ) {
677 $this->mName
= $s->user_name
;
678 $this->mEmail
= $s->user_email
;
679 $this->mEmailAuthenticated
= wfTimestampOrNull( TS_MW
, $s->user_email_authenticated
);
680 $this->mRealName
= $s->user_real_name
;
681 $this->mPassword
= $s->user_password
;
682 $this->mNewpassword
= $s->user_newpassword
;
683 $this->decodeOptions( $s->user_options
);
684 $this->mTouched
= wfTimestamp(TS_MW
,$s->user_touched
);
685 $this->mToken
= $s->user_token
;
687 $res = $dbr->select( 'user_groups',
689 array( 'ug_user' => $this->mId
),
691 $this->mGroups
= array();
692 while( $row = $dbr->fetchObject( $res ) ) {
693 $this->mGroups
[] = $row->ug_group
;
695 $effectiveGroups = array_merge( array( '*', 'user' ), $this->mGroups
);
696 $this->mRights
= $this->getGroupPermissions( $effectiveGroups );
699 $this->mDataLoaded
= true;
702 function getID() { return $this->mId
; }
703 function setID( $v ) {
705 $this->mDataLoaded
= false;
709 $this->loadFromDatabase();
713 function setName( $str ) {
714 $this->loadFromDatabase();
720 * Return the title dbkey form of the name, for eg user pages.
724 function getTitleKey() {
725 return str_replace( ' ', '_', $this->getName() );
728 function getNewtalk() {
730 $fname = 'User::getNewtalk';
731 $this->loadFromDatabase();
733 # Load the newtalk status if it is unloaded (mNewtalk=-1)
734 if( $this->mNewtalk
== -1 ) {
735 $this->mNewtalk
= 0; # reset talk page status
737 # Check memcached separately for anons, who have no
738 # entire User object stored in there.
740 global $wgDBname, $wgMemc;
741 $key = "$wgDBname:newtalk:ip:{$this->mName}";
742 $newtalk = $wgMemc->get( $key );
743 if( is_integer( $newtalk ) ) {
744 $this->mNewtalk
= $newtalk ?
1 : 0;
745 return (bool)$this->mNewtalk
;
749 $dbr =& wfGetDB( DB_SLAVE
);
750 if ( $wgUseEnotif ) {
751 $res = $dbr->select( 'watchlist',
753 array( 'wl_title' => $this->getTitleKey(),
754 'wl_namespace' => NS_USER_TALK
,
755 'wl_user' => $this->mId
,
756 'wl_notificationtimestamp != 0' ),
757 'User::getNewtalk' );
758 if( $dbr->numRows($res) > 0 ) {
761 $dbr->freeResult( $res );
762 } elseif ( $this->mId
) {
763 $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId
), $fname );
765 if ( $dbr->numRows($res)>0 ) {
768 $dbr->freeResult( $res );
770 $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->mName
), $fname );
771 $this->mNewtalk
= $dbr->numRows( $res ) > 0 ?
1 : 0;
772 $dbr->freeResult( $res );
776 $wgMemc->set( $key, $this->mNewtalk
, time() ); // + 1800 );
780 return ( 0 != $this->mNewtalk
);
783 function setNewtalk( $val ) {
784 $this->loadFromDatabase();
785 $this->mNewtalk
= $val;
786 $this->invalidateCache();
789 function invalidateCache() {
790 global $wgClockSkewFudge;
791 $this->loadFromDatabase();
792 $this->mTouched
= wfTimestamp(TS_MW
, time() +
$wgClockSkewFudge );
793 # Don't forget to save the options after this or
794 # it won't take effect!
797 function validateCache( $timestamp ) {
798 $this->loadFromDatabase();
799 return ($timestamp >= $this->mTouched
);
804 * Will only be salted if $wgPasswordSalt is true
805 * @param string Password.
806 * @return string Salted password or clear password.
808 function addSalt( $p ) {
809 global $wgPasswordSalt;
811 return md5( "{$this->mId}-{$p}" );
817 * Encrypt a password.
818 * It can eventuall salt a password @see User::addSalt()
819 * @param string $p clear Password.
820 * @param string Encrypted password.
822 function encryptPassword( $p ) {
823 return $this->addSalt( md5( $p ) );
826 # Set the password and reset the random token
827 function setPassword( $str ) {
828 $this->loadFromDatabase();
830 $this->mPassword
= $this->encryptPassword( $str );
831 $this->mNewpassword
= '';
834 # Set the random token (used for persistent authentication)
835 function setToken( $token = false ) {
836 global $wgSecretKey, $wgProxyKey, $wgDBname;
838 if ( $wgSecretKey ) {
840 } elseif ( $wgProxyKey ) {
845 $this->mToken
= md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId
);
847 $this->mToken
= $token;
852 function setCookiePassword( $str ) {
853 $this->loadFromDatabase();
854 $this->mCookiePassword
= md5( $str );
857 function setNewpassword( $str ) {
858 $this->loadFromDatabase();
859 $this->mNewpassword
= $this->encryptPassword( $str );
862 function getEmail() {
863 $this->loadFromDatabase();
864 return $this->mEmail
;
867 function getEmailAuthenticationTimestamp() {
868 $this->loadFromDatabase();
869 return $this->mEmailAuthenticated
;
872 function setEmail( $str ) {
873 $this->loadFromDatabase();
874 $this->mEmail
= $str;
877 function getRealName() {
878 $this->loadFromDatabase();
879 return $this->mRealName
;
882 function setRealName( $str ) {
883 $this->loadFromDatabase();
884 $this->mRealName
= $str;
887 function getOption( $oname ) {
888 $this->loadFromDatabase();
889 if ( array_key_exists( $oname, $this->mOptions
) ) {
890 return trim( $this->mOptions
[$oname] );
896 function setOption( $oname, $val ) {
897 $this->loadFromDatabase();
898 if ( $oname == 'skin' ) {
899 # Clear cached skin, so the new one displays immediately in Special:Preferences
900 unset( $this->mSkin
);
902 $this->mOptions
[$oname] = $val;
903 $this->invalidateCache();
906 function getRights() {
907 $this->loadFromDatabase();
908 return $this->mRights
;
912 * Get the list of explicit group memberships this user has.
913 * The implicit * and user groups are not included.
914 * @return array of strings
916 function getGroups() {
917 $this->loadFromDatabase();
918 return $this->mGroups
;
922 * Get the list of implicit group memberships this user has.
923 * This includes all explicit groups, plus 'user' if logged in
924 * and '*' for all accounts.
925 * @return array of strings
927 function getEffectiveGroups() {
928 $base = array( '*' );
929 if( $this->isLoggedIn() ) {
932 return array_merge( $base, $this->getGroups() );
936 * Remove the user from the given group.
937 * This takes immediate effect.
940 function addGroup( $group ) {
941 $dbw =& wfGetDB( DB_MASTER
);
942 $dbw->insert( 'user_groups',
944 'ug_user' => $this->getID(),
945 'ug_group' => $group,
950 $this->mGroups
= array_merge( $this->mGroups
, array( $group ) );
951 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
953 $this->invalidateCache();
954 $this->saveSettings();
958 * Remove the user from the given group.
959 * This takes immediate effect.
962 function removeGroup( $group ) {
963 $dbw =& wfGetDB( DB_MASTER
);
964 $dbw->delete( 'user_groups',
966 'ug_user' => $this->getID(),
967 'ug_group' => $group,
969 'User::removeGroup' );
971 $this->mGroups
= array_diff( $this->mGroups
, array( $group ) );
972 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
974 $this->invalidateCache();
975 $this->saveSettings();
980 * A more legible check for non-anonymousness.
981 * Returns true if the user is not an anonymous visitor.
985 function isLoggedIn() {
986 return( $this->getID() != 0 );
990 * A more legible check for anonymousness.
991 * Returns true if the user is an anonymous visitor.
996 return !$this->isLoggedIn();
1000 * Check if a user is sysop
1001 * Die with backtrace. Use User:isAllowed() instead.
1004 function isSysop() {
1005 return $this->isAllowed( 'protect' );
1009 function isDeveloper() {
1010 return $this->isAllowed( 'siteadmin' );
1014 function isBureaucrat() {
1015 return $this->isAllowed( 'makesysop' );
1019 * Whether the user is a bot
1020 * @todo need to be migrated to the new user level management sytem
1023 $this->loadFromDatabase();
1024 return in_array( 'bot', $this->mRights
);
1028 * Check if user is allowed to access a feature / make an action
1029 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1030 * @return boolean True: action is allowed, False: action should not be allowed
1032 function isAllowed($action='') {
1033 $this->loadFromDatabase();
1034 return in_array( $action , $this->mRights
);
1038 * Load a skin if it doesn't exist or return it
1039 * @todo FIXME : need to check the old failback system [AV]
1041 function &getSkin() {
1042 global $IP, $wgRequest;
1043 if ( ! isset( $this->mSkin
) ) {
1044 $fname = 'User::getSkin';
1045 wfProfileIn( $fname );
1047 # get all skin names available
1048 $skinNames = Skin
::getSkinNames();
1051 $userSkin = $this->getOption( 'skin' );
1052 $userSkin = $wgRequest->getText('useskin', $userSkin);
1053 if ( $userSkin == '' ) { $userSkin = 'standard'; }
1055 if ( !isset( $skinNames[$userSkin] ) ) {
1056 # in case the user skin could not be found find a replacement
1060 2 => 'CologneBlue');
1061 # if phptal is enabled we should have monobook skin that
1062 # superseed the good old SkinStandard.
1063 if ( isset( $skinNames['monobook'] ) ) {
1064 $fallback[0] = 'MonoBook';
1067 if(is_numeric($userSkin) && isset( $fallback[$userSkin]) ){
1068 $sn = $fallback[$userSkin];
1073 # The user skin is available
1074 $sn = $skinNames[$userSkin];
1077 # Grab the skin class and initialise it. Each skin checks for PHPTal
1078 # and will not load if it's not enabled.
1079 require_once( $IP.'/skins/'.$sn.'.php' );
1081 # Check if we got if not failback to default skin
1082 $className = 'Skin'.$sn;
1083 if( !class_exists( $className ) ) {
1084 # DO NOT die if the class isn't found. This breaks maintenance
1085 # scripts and can cause a user account to be unrecoverable
1086 # except by SQL manipulation if a previously valid skin name
1087 # is no longer valid.
1088 $className = 'SkinStandard';
1089 require_once( $IP.'/skins/Standard.php' );
1091 $this->mSkin
=& new $className;
1092 wfProfileOut( $fname );
1094 return $this->mSkin
;
1098 * @param string $title Article title to look at
1102 * Check watched status of an article
1103 * @return bool True if article is watched
1105 function isWatched( $title ) {
1106 $wl = WatchedItem
::fromUserTitle( $this, $title );
1107 return $wl->isWatched();
1113 function addWatch( $title ) {
1114 $wl = WatchedItem
::fromUserTitle( $this, $title );
1116 $this->invalidateCache();
1120 * Stop watching an article
1122 function removeWatch( $title ) {
1123 $wl = WatchedItem
::fromUserTitle( $this, $title );
1125 $this->invalidateCache();
1129 * Clear the user's notification timestamp for the given title.
1130 * If e-notif e-mails are on, they will receive notification mails on
1131 * the next change of the page if it's watched etc.
1133 function clearNotification( &$title ) {
1134 global $wgUser, $wgUseEnotif;
1136 if ( !$wgUseEnotif ) {
1140 $userid = $this->getID();
1145 // Only update the timestamp if the page is being watched.
1146 // The query to find out if it is watched is cached both in memcached and per-invocation,
1147 // and when it does have to be executed, it can be on a slave
1148 // If this is the user's newtalk page, we always update the timestamp
1149 if ($title->getNamespace() == NS_USER_TALK
&&
1150 $title->getText() == $wgUser->getName())
1153 } elseif ( $this->getID() == $wgUser->getID() ) {
1154 $watched = $title->userIsWatching();
1159 // If the page is watched by the user (or may be watched), update the timestamp on any
1160 // any matching rows
1162 $dbw =& wfGetDB( DB_MASTER
);
1163 $success = $dbw->update( 'watchlist',
1165 'wl_notificationtimestamp' => 0
1166 ), array( /* WHERE */
1167 'wl_title' => $title->getDBkey(),
1168 'wl_namespace' => $title->getNamespace(),
1169 'wl_user' => $this->getID()
1170 ), 'User::clearLastVisited'
1178 * Resets all of the given user's page-change notification timestamps.
1179 * If e-notif e-mails are on, they will receive notification mails on
1180 * the next change of any watched page.
1182 * @param int $currentUser user ID number
1185 function clearAllNotifications( $currentUser ) {
1186 global $wgUseEnotif;
1187 if ( !$wgUseEnotif ) {
1190 if( $currentUser != 0 ) {
1192 $dbw =& wfGetDB( DB_MASTER
);
1193 $success = $dbw->update( 'watchlist',
1195 'wl_notificationtimestamp' => 0
1196 ), array( /* WHERE */
1197 'wl_user' => $currentUser
1198 ), 'UserMailer::clearAll'
1201 # we also need to clear here the "you have new message" notification for the own user_talk page
1202 # This is cleared one page view later in Article::viewUpdates();
1208 * @return string Encoding options
1210 function encodeOptions() {
1212 foreach ( $this->mOptions
as $oname => $oval ) {
1213 array_push( $a, $oname.'='.$oval );
1215 $s = implode( "\n", $a );
1222 function decodeOptions( $str ) {
1223 $a = explode( "\n", $str );
1224 foreach ( $a as $s ) {
1225 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1226 $this->mOptions
[$m[1]] = $m[2];
1231 function setCookies() {
1232 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgDBname;
1233 if ( 0 == $this->mId
) return;
1234 $this->loadFromDatabase();
1235 $exp = time() +
$wgCookieExpiration;
1237 $_SESSION['wsUserID'] = $this->mId
;
1238 setcookie( $wgDBname.'UserID', $this->mId
, $exp, $wgCookiePath, $wgCookieDomain );
1240 $_SESSION['wsUserName'] = $this->mName
;
1241 setcookie( $wgDBname.'UserName', $this->mName
, $exp, $wgCookiePath, $wgCookieDomain );
1243 $_SESSION['wsToken'] = $this->mToken
;
1244 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1245 setcookie( $wgDBname.'Token', $this->mToken
, $exp, $wgCookiePath, $wgCookieDomain );
1247 setcookie( $wgDBname.'Token', '', time() - 3600 );
1253 * It will clean the session cookie
1256 global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP;
1257 $this->loadDefaults();
1258 $this->setLoaded( true );
1260 $_SESSION['wsUserID'] = 0;
1262 setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1263 setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1265 # Remember when user logged out, to prevent seeing cached pages
1266 setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() +
86400, $wgCookiePath, $wgCookieDomain );
1270 * Save object settings into database
1272 function saveSettings() {
1273 global $wgMemc, $wgDBname, $wgUseEnotif;
1274 $fname = 'User::saveSettings';
1276 if ( wfReadOnly() ) { return; }
1277 $this->saveNewtalk();
1278 if ( 0 == $this->mId
) { return; }
1280 $dbw =& wfGetDB( DB_MASTER
);
1281 $dbw->update( 'user',
1283 'user_name' => $this->mName
,
1284 'user_password' => $this->mPassword
,
1285 'user_newpassword' => $this->mNewpassword
,
1286 'user_real_name' => $this->mRealName
,
1287 'user_email' => $this->mEmail
,
1288 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1289 'user_options' => $this->encodeOptions(),
1290 'user_touched' => $dbw->timestamp($this->mTouched
),
1291 'user_token' => $this->mToken
1292 ), array( /* WHERE */
1293 'user_id' => $this->mId
1296 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1300 * Save value of new talk flag.
1302 function saveNewtalk() {
1303 global $wgDBname, $wgMemc, $wgUseEnotif;
1305 $fname = 'User::saveNewtalk';
1309 if ( wfReadOnly() ) { return ; }
1310 $dbr =& wfGetDB( DB_SLAVE
);
1311 $dbw =& wfGetDB( DB_MASTER
);
1313 if ( $wgUseEnotif ) {
1314 if ( ! $this->getNewtalk() ) {
1315 # Delete the watchlist entry for user_talk page X watched by user X
1316 $dbw->delete( 'watchlist',
1317 array( 'wl_user' => $this->mId
,
1318 'wl_title' => $this->getTitleKey(),
1319 'wl_namespace' => NS_USER_TALK
),
1321 if ( $dbw->affectedRows() ) {
1325 # Anon users have a separate memcache space for newtalk
1326 # since they don't store their own info. Trim...
1327 $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" );
1331 if ($this->getID() != 0) {
1333 $value = $this->getID();
1337 $value = $this->mName
;
1338 $key = "$wgDBname:newtalk:ip:$this->mName";
1341 $dbr =& wfGetDB( DB_SLAVE
);
1342 $dbw =& wfGetDB( DB_MASTER
);
1344 $res = $dbr->selectField('user_newtalk', $field,
1345 array($field => $value), $fname);
1348 if ($res !== false && $this->mNewtalk
== 0) {
1349 $dbw->delete('user_newtalk', array($field => $value), $fname);
1351 $wgMemc->set( $key, 0 );
1353 } else if ($res === false && $this->mNewtalk
== 1) {
1354 $dbw->insert('user_newtalk', array($field => $value), $fname);
1356 $wgMemc->set( $key, 1 );
1363 # Update user_touched, so that newtalk notifications in the client cache are invalidated
1364 if ( $changed && $this->getID() ) {
1365 $dbw->update('user',
1366 /*SET*/ array( 'user_touched' => $this->mTouched
),
1367 /*WHERE*/ array( 'user_id' => $this->getID() ),
1369 $wgMemc->set( "$wgDBname:user:id:{$this->mId}", $this, 86400 );
1374 * Checks if a user with the given name exists, returns the ID
1376 function idForName() {
1377 $fname = 'User::idForName';
1380 $s = trim( $this->mName
);
1381 if ( 0 == strcmp( '', $s ) ) return 0;
1383 $dbr =& wfGetDB( DB_SLAVE
);
1384 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1385 if ( $id === false ) {
1392 * Add user object to the database
1394 function addToDatabase() {
1395 $fname = 'User::addToDatabase';
1396 $dbw =& wfGetDB( DB_MASTER
);
1397 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1398 $dbw->insert( 'user',
1400 'user_id' => $seqVal,
1401 'user_name' => $this->mName
,
1402 'user_password' => $this->mPassword
,
1403 'user_newpassword' => $this->mNewpassword
,
1404 'user_email' => $this->mEmail
,
1405 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1406 'user_real_name' => $this->mRealName
,
1407 'user_options' => $this->encodeOptions(),
1408 'user_token' => $this->mToken
1411 $this->mId
= $dbw->insertId();
1414 function spreadBlock() {
1416 # If the (non-anonymous) user is blocked, this function will block any IP address
1417 # that they successfully log on from.
1418 $fname = 'User::spreadBlock';
1420 wfDebug( "User:spreadBlock()\n" );
1421 if ( $this->mId
== 0 ) {
1425 $userblock = Block
::newFromDB( '', $this->mId
);
1426 if ( !$userblock->isValid() ) {
1430 # Check if this IP address is already blocked
1431 $ipblock = Block
::newFromDB( $wgIP );
1432 if ( $ipblock->isValid() ) {
1433 # Just update the timestamp
1434 $ipblock->updateTimestamp();
1438 # Make a new block object with the desired properties
1439 wfDebug( "Autoblocking {$this->mName}@{$wgIP}\n" );
1440 $ipblock->mAddress
= $wgIP;
1441 $ipblock->mUser
= 0;
1442 $ipblock->mBy
= $userblock->mBy
;
1443 $ipblock->mReason
= wfMsg( 'autoblocker', $this->getName(), $userblock->mReason
);
1444 $ipblock->mTimestamp
= wfTimestampNow();
1445 $ipblock->mAuto
= 1;
1446 # If the user is already blocked with an expiry date, we don't
1447 # want to pile on top of that!
1448 if($userblock->mExpiry
) {
1449 $ipblock->mExpiry
= min ( $userblock->mExpiry
, Block
::getAutoblockExpiry( $ipblock->mTimestamp
));
1451 $ipblock->mExpiry
= Block
::getAutoblockExpiry( $ipblock->mTimestamp
);
1459 function getPageRenderingHash() {
1462 return $this->mHash
;
1465 // stubthreshold is only included below for completeness,
1466 // it will always be 0 when this function is called by parsercache.
1468 $confstr = $this->getOption( 'math' );
1469 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1470 $confstr .= '!' . $this->getOption( 'date' );
1471 $confstr .= '!' . $this->getOption( 'numberheadings' );
1472 $confstr .= '!' . $this->getOption( 'language' );
1473 $confstr .= '!' . $this->getOption( 'thumbsize' );
1474 // add in language specific options, if any
1475 $extra = $wgContLang->getExtraHashOptions();
1478 $this->mHash
= $confstr;
1482 function isAllowedToCreateAccount() {
1483 return $this->isAllowed( 'createaccount' );
1487 * Set mDataLoaded, return previous value
1488 * Use this to prevent DB access in command-line scripts or similar situations
1490 function setLoaded( $loaded ) {
1491 return wfSetVar( $this->mDataLoaded
, $loaded );
1495 * Get this user's personal page title.
1500 function getUserPage() {
1501 return Title
::makeTitle( NS_USER
, $this->mName
);
1505 * Get this user's talk page title.
1510 function getTalkPage() {
1511 $title = $this->getUserPage();
1512 return $title->getTalkPage();
1518 function getMaxID() {
1519 $dbr =& wfGetDB( DB_SLAVE
);
1520 return $dbr->selectField( 'user', 'max(user_id)', false );
1524 * Determine whether the user is a newbie. Newbies are either
1525 * anonymous IPs, or the 1% most recently created accounts.
1526 * Bots and sysops are excluded.
1527 * @return bool True if it is a newbie.
1529 function isNewbie() {
1530 return $this->isAnon() ||
$this->mId
> User
::getMaxID() * 0.99 && !$this->isAllowed( 'delete' ) && !$this->isBot();
1534 * Check to see if the given clear-text password is one of the accepted passwords
1535 * @param string $password User password.
1536 * @return bool True if the given password is correct otherwise False.
1538 function checkPassword( $password ) {
1539 global $wgAuth, $wgMinimalPasswordLength;
1540 $this->loadFromDatabase();
1542 // Even though we stop people from creating passwords that
1543 // are shorter than this, doesn't mean people wont be able
1544 // to. Certain authentication plugins do NOT want to save
1545 // domain passwords in a mysql database, so we should
1546 // check this (incase $wgAuth->strict() is false).
1547 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1551 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1553 } elseif( $wgAuth->strict() ) {
1554 /* Auth plugin doesn't allow local authentication */
1557 $ep = $this->encryptPassword( $password );
1558 if ( 0 == strcmp( $ep, $this->mPassword
) ) {
1560 } elseif ( ($this->mNewpassword
!= '') && (0 == strcmp( $ep, $this->mNewpassword
)) ) {
1562 } elseif ( function_exists( 'iconv' ) ) {
1563 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1564 # Check for this with iconv
1565 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1566 if ( 0 == strcmp( $cp1252hash, $this->mPassword
) ) {
1574 * Initialize (if necessary) and return a session token value
1575 * which can be used in edit forms to show that the user's
1576 * login credentials aren't being hijacked with a foreign form
1579 * @param mixed $salt - Optional function-specific data for hash.
1580 * Use a string or an array of strings.
1584 function editToken( $salt = '' ) {
1585 if( !isset( $_SESSION['wsEditToken'] ) ) {
1586 $token = $this->generateToken();
1587 $_SESSION['wsEditToken'] = $token;
1589 $token = $_SESSION['wsEditToken'];
1591 if( is_array( $salt ) ) {
1592 $salt = implode( '|', $salt );
1594 return md5( $token . $salt );
1598 * Generate a hex-y looking random token for various uses.
1599 * Could be made more cryptographically sure if someone cares.
1602 function generateToken( $salt = '' ) {
1603 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1604 return md5( $token . $salt );
1608 * Check given value against the token value stored in the session.
1609 * A match should confirm that the form was submitted from the
1610 * user's own login session, not a form submission from a third-party
1613 * @param string $val - the input value to compare
1614 * @param string $salt - Optional function-specific data for hash
1618 function matchEditToken( $val, $salt = '' ) {
1619 return ( $val == $this->editToken( $salt ) );
1623 * Generate a new e-mail confirmation token and send a confirmation
1624 * mail to the user's given address.
1626 * @return mixed True on success, a WikiError object on failure.
1628 function sendConfirmationMail() {
1629 global $wgIP, $wgContLang;
1630 $url = $this->confirmationTokenUrl( $expiration );
1631 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1632 wfMsg( 'confirmemail_body',
1636 $wgContLang->timeanddate( $expiration, false ) ) );
1640 * Send an e-mail to this user's account. Does not check for
1641 * confirmed status or validity.
1643 * @param string $subject
1644 * @param string $body
1645 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1646 * @return mixed True on success, a WikiError object on failure.
1648 function sendMail( $subject, $body, $from = null ) {
1649 if( is_null( $from ) ) {
1650 global $wgPasswordSender;
1651 $from = $wgPasswordSender;
1654 require_once( 'UserMailer.php' );
1655 $error = userMailer( $this->getEmail(), $from, $subject, $body );
1657 if( $error == '' ) {
1660 return new WikiError( $error );
1665 * Generate, store, and return a new e-mail confirmation code.
1666 * A hash (unsalted since it's used as a key) is stored.
1667 * @param &$expiration mixed output: accepts the expiration time
1671 function confirmationToken( &$expiration ) {
1672 $fname = 'User::confirmationToken';
1675 $expires = $now +
7 * 24 * 60 * 60;
1676 $expiration = wfTimestamp( TS_MW
, $expires );
1678 $token = $this->generateToken( $this->mId
. $this->mEmail
. $expires );
1679 $hash = md5( $token );
1681 $dbw =& wfGetDB( DB_MASTER
);
1682 $dbw->update( 'user',
1683 array( 'user_email_token' => $hash,
1684 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1685 array( 'user_id' => $this->mId
),
1692 * Generate and store a new e-mail confirmation token, and return
1693 * the URL the user can use to confirm.
1694 * @param &$expiration mixed output: accepts the expiration time
1698 function confirmationTokenUrl( &$expiration ) {
1699 $token = $this->confirmationToken( $expiration );
1700 $title = Title
::makeTitle( NS_SPECIAL
, 'Confirmemail/' . $token );
1701 return $title->getFullUrl();
1705 * Mark the e-mail address confirmed and save.
1707 function confirmEmail() {
1708 $this->loadFromDatabase();
1709 $this->mEmailAuthenticated
= wfTimestampNow();
1710 $this->saveSettings();
1715 * Is this user allowed to send e-mails within limits of current
1716 * site configuration?
1719 function canSendEmail() {
1720 return $this->isEmailConfirmed();
1724 * Is this user allowed to receive e-mails within limits of current
1725 * site configuration?
1728 function canReceiveEmail() {
1729 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1733 * Is this user's e-mail address valid-looking and confirmed within
1734 * limits of the current site configuration?
1736 * If $wgEmailAuthentication is on, this may require the user to have
1737 * confirmed their address by returning a code or using a password
1738 * sent to the address from the wiki.
1742 function isEmailConfirmed() {
1743 global $wgEmailAuthentication;
1744 $this->loadFromDatabase();
1745 if( $this->isAnon() )
1747 if( !$this->isValidEmailAddr( $this->mEmail
) )
1749 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1755 * @param array $groups list of groups
1756 * @return array list of permission key names for given groups combined
1759 function getGroupPermissions( $groups ) {
1760 global $wgGroupPermissions;
1762 foreach( $groups as $group ) {
1763 if( isset( $wgGroupPermissions[$group] ) ) {
1764 $rights = array_merge( $rights,
1765 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1772 * @param string $group key name
1773 * @return string localized descriptive name, if provided
1776 function getGroupName( $group ) {
1777 $key = "group-$group-name";
1778 $name = wfMsg( $key );
1779 if( $name == '' ||
$name == "<$key>" ) {
1787 * Return the set of defined explicit groups.
1788 * The * and 'user' groups are not included.
1792 function getAllGroups() {
1793 global $wgGroupPermissions;
1795 array_keys( $wgGroupPermissions ),
1796 array( '*', 'user' ) );