MediaWiki  master
User.php
Go to the documentation of this file.
00001 <?php
00027 define( 'USER_TOKEN_LENGTH', 32 );
00028 
00033 define( 'MW_USER_VERSION', 8 );
00034 
00039 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
00040 
00045 class PasswordError extends MWException {
00046         // NOP
00047 }
00048 
00059 class User {
00064         const USER_TOKEN_LENGTH = USER_TOKEN_LENGTH;
00065         const MW_USER_VERSION = MW_USER_VERSION;
00066         const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
00067 
00071         const MAX_WATCHED_ITEMS_CACHE = 100;
00072 
00079         static $mCacheVars = array(
00080                 // user table
00081                 'mId',
00082                 'mName',
00083                 'mRealName',
00084                 'mPassword',
00085                 'mNewpassword',
00086                 'mNewpassTime',
00087                 'mEmail',
00088                 'mTouched',
00089                 'mToken',
00090                 'mEmailAuthenticated',
00091                 'mEmailToken',
00092                 'mEmailTokenExpires',
00093                 'mRegistration',
00094                 'mEditCount',
00095                 // user_groups table
00096                 'mGroups',
00097                 // user_properties table
00098                 'mOptionOverrides',
00099         );
00100 
00107         static $mCoreRights = array(
00108                 'apihighlimits',
00109                 'autoconfirmed',
00110                 'autopatrol',
00111                 'bigdelete',
00112                 'block',
00113                 'blockemail',
00114                 'bot',
00115                 'browsearchive',
00116                 'createaccount',
00117                 'createpage',
00118                 'createtalk',
00119                 'delete',
00120                 'deletedhistory',
00121                 'deletedtext',
00122                 'deletelogentry',
00123                 'deleterevision',
00124                 'edit',
00125                 'editinterface',
00126                 'editprotected',
00127                 'editusercssjs', #deprecated
00128                 'editusercss',
00129                 'edituserjs',
00130                 'hideuser',
00131                 'import',
00132                 'importupload',
00133                 'ipblock-exempt',
00134                 'markbotedits',
00135                 'mergehistory',
00136                 'minoredit',
00137                 'move',
00138                 'movefile',
00139                 'move-rootuserpages',
00140                 'move-subpages',
00141                 'nominornewtalk',
00142                 'noratelimit',
00143                 'override-export-depth',
00144                 'passwordreset',
00145                 'patrol',
00146                 'patrolmarks',
00147                 'protect',
00148                 'proxyunbannable',
00149                 'purge',
00150                 'read',
00151                 'reupload',
00152                 'reupload-own',
00153                 'reupload-shared',
00154                 'rollback',
00155                 'sendemail',
00156                 'siteadmin',
00157                 'suppressionlog',
00158                 'suppressredirect',
00159                 'suppressrevision',
00160                 'unblockself',
00161                 'undelete',
00162                 'unwatchedpages',
00163                 'upload',
00164                 'upload_by_url',
00165                 'userrights',
00166                 'userrights-interwiki',
00167                 'writeapi',
00168         );
00172         static $mAllRights = false;
00173 
00176         var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
00177                 $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
00178                 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount,
00179                 $mGroups, $mOptionOverrides;
00181 
00186         var $mOptionsLoaded;
00187 
00191         private $mLoadedItems = array();
00193 
00203         var $mFrom;
00204 
00208         var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mRights,
00209                 $mBlockreason, $mEffectiveGroups, $mImplicitGroups, $mFormerGroups, $mBlockedGlobally,
00210                 $mLocked, $mHideName, $mOptions;
00211 
00215         private $mRequest;
00216 
00220         var $mBlock;
00221 
00225         var $mAllowUsertalk;
00226 
00230         private $mBlockedFromCreateAccount = false;
00231 
00235         private $mWatchedItems = array();
00236 
00237         static $idCacheByName = array();
00238 
00249         function __construct() {
00250                 $this->clearInstanceCache( 'defaults' );
00251         }
00252 
00256         function __toString(){
00257                 return $this->getName();
00258         }
00259 
00263         public function load() {
00264                 if ( $this->mLoadedItems === true ) {
00265                         return;
00266                 }
00267                 wfProfileIn( __METHOD__ );
00268 
00269                 # Set it now to avoid infinite recursion in accessors
00270                 $this->mLoadedItems = true;
00271 
00272                 switch ( $this->mFrom ) {
00273                         case 'defaults':
00274                                 $this->loadDefaults();
00275                                 break;
00276                         case 'name':
00277                                 $this->mId = self::idFromName( $this->mName );
00278                                 if ( !$this->mId ) {
00279                                         # Nonexistent user placeholder object
00280                                         $this->loadDefaults( $this->mName );
00281                                 } else {
00282                                         $this->loadFromId();
00283                                 }
00284                                 break;
00285                         case 'id':
00286                                 $this->loadFromId();
00287                                 break;
00288                         case 'session':
00289                                 if( !$this->loadFromSession() ) {
00290                                         // Loading from session failed. Load defaults.
00291                                         $this->loadDefaults();
00292                                 }
00293                                 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
00294                                 break;
00295                         default:
00296                                 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
00297                 }
00298                 wfProfileOut( __METHOD__ );
00299         }
00300 
00305         public function loadFromId() {
00306                 global $wgMemc;
00307                 if ( $this->mId == 0 ) {
00308                         $this->loadDefaults();
00309                         return false;
00310                 }
00311 
00312                 # Try cache
00313                 $key = wfMemcKey( 'user', 'id', $this->mId );
00314                 $data = $wgMemc->get( $key );
00315                 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
00316                         # Object is expired, load from DB
00317                         $data = false;
00318                 }
00319 
00320                 if ( !$data ) {
00321                         wfDebug( "User: cache miss for user {$this->mId}\n" );
00322                         # Load from DB
00323                         if ( !$this->loadFromDatabase() ) {
00324                                 # Can't load from ID, user is anonymous
00325                                 return false;
00326                         }
00327                         $this->saveToCache();
00328                 } else {
00329                         wfDebug( "User: got user {$this->mId} from cache\n" );
00330                         # Restore from cache
00331                         foreach ( self::$mCacheVars as $name ) {
00332                                 $this->$name = $data[$name];
00333                         }
00334                 }
00335                 return true;
00336         }
00337 
00341         public function saveToCache() {
00342                 $this->load();
00343                 $this->loadGroups();
00344                 $this->loadOptions();
00345                 if ( $this->isAnon() ) {
00346                         // Anonymous users are uncached
00347                         return;
00348                 }
00349                 $data = array();
00350                 foreach ( self::$mCacheVars as $name ) {
00351                         $data[$name] = $this->$name;
00352                 }
00353                 $data['mVersion'] = MW_USER_VERSION;
00354                 $key = wfMemcKey( 'user', 'id', $this->mId );
00355                 global $wgMemc;
00356                 $wgMemc->set( $key, $data );
00357         }
00358 
00361 
00378         public static function newFromName( $name, $validate = 'valid' ) {
00379                 if ( $validate === true ) {
00380                         $validate = 'valid';
00381                 }
00382                 $name = self::getCanonicalName( $name, $validate );
00383                 if ( $name === false ) {
00384                         return false;
00385                 } else {
00386                         # Create unloaded user object
00387                         $u = new User;
00388                         $u->mName = $name;
00389                         $u->mFrom = 'name';
00390                         $u->setItemLoaded( 'name' );
00391                         return $u;
00392                 }
00393         }
00394 
00401         public static function newFromId( $id ) {
00402                 $u = new User;
00403                 $u->mId = $id;
00404                 $u->mFrom = 'id';
00405                 $u->setItemLoaded( 'id' );
00406                 return $u;
00407         }
00408 
00419         public static function newFromConfirmationCode( $code ) {
00420                 $dbr = wfGetDB( DB_SLAVE );
00421                 $id = $dbr->selectField( 'user', 'user_id', array(
00422                         'user_email_token' => md5( $code ),
00423                         'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
00424                         ) );
00425                 if( $id !== false ) {
00426                         return User::newFromId( $id );
00427                 } else {
00428                         return null;
00429                 }
00430         }
00431 
00440         public static function newFromSession( WebRequest $request = null ) {
00441                 $user = new User;
00442                 $user->mFrom = 'session';
00443                 $user->mRequest = $request;
00444                 return $user;
00445         }
00446 
00461         public static function newFromRow( $row, $data = null ) {
00462                 $user = new User;
00463                 $user->loadFromRow( $row, $data );
00464                 return $user;
00465         }
00466 
00468 
00474         public static function whoIs( $id ) {
00475                 return UserCache::singleton()->getProp( $id, 'name' );
00476         }
00477 
00484         public static function whoIsReal( $id ) {
00485                 return UserCache::singleton()->getProp( $id, 'real_name' );
00486         }
00487 
00493         public static function idFromName( $name ) {
00494                 $nt = Title::makeTitleSafe( NS_USER, $name );
00495                 if( is_null( $nt ) ) {
00496                         # Illegal name
00497                         return null;
00498                 }
00499 
00500                 if ( isset( self::$idCacheByName[$name] ) ) {
00501                         return self::$idCacheByName[$name];
00502                 }
00503 
00504                 $dbr = wfGetDB( DB_SLAVE );
00505                 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
00506 
00507                 if ( $s === false ) {
00508                         $result = null;
00509                 } else {
00510                         $result = $s->user_id;
00511                 }
00512 
00513                 self::$idCacheByName[$name] = $result;
00514 
00515                 if ( count( self::$idCacheByName ) > 1000 ) {
00516                         self::$idCacheByName = array();
00517                 }
00518 
00519                 return $result;
00520         }
00521 
00525         public static function resetIdByNameCache() {
00526                 self::$idCacheByName = array();
00527         }
00528 
00545         public static function isIP( $name ) {
00546                 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
00547         }
00548 
00560         public static function isValidUserName( $name ) {
00561                 global $wgContLang, $wgMaxNameChars;
00562 
00563                 if ( $name == ''
00564                 || User::isIP( $name )
00565                 || strpos( $name, '/' ) !== false
00566                 || strlen( $name ) > $wgMaxNameChars
00567                 || $name != $wgContLang->ucfirst( $name ) ) {
00568                         wfDebugLog( 'username', __METHOD__ .
00569                                 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
00570                         return false;
00571                 }
00572 
00573 
00574                 // Ensure that the name can't be misresolved as a different title,
00575                 // such as with extra namespace keys at the start.
00576                 $parsed = Title::newFromText( $name );
00577                 if( is_null( $parsed )
00578                         || $parsed->getNamespace()
00579                         || strcmp( $name, $parsed->getPrefixedText() ) ) {
00580                         wfDebugLog( 'username', __METHOD__ .
00581                                 ": '$name' invalid due to ambiguous prefixes" );
00582                         return false;
00583                 }
00584 
00585                 // Check an additional blacklist of troublemaker characters.
00586                 // Should these be merged into the title char list?
00587                 $unicodeBlacklist = '/[' .
00588                         '\x{0080}-\x{009f}' . # iso-8859-1 control chars
00589                         '\x{00a0}' .          # non-breaking space
00590                         '\x{2000}-\x{200f}' . # various whitespace
00591                         '\x{2028}-\x{202f}' . # breaks and control chars
00592                         '\x{3000}' .          # ideographic space
00593                         '\x{e000}-\x{f8ff}' . # private use
00594                         ']/u';
00595                 if( preg_match( $unicodeBlacklist, $name ) ) {
00596                         wfDebugLog( 'username', __METHOD__ .
00597                                 ": '$name' invalid due to blacklisted characters" );
00598                         return false;
00599                 }
00600 
00601                 return true;
00602         }
00603 
00615         public static function isUsableName( $name ) {
00616                 global $wgReservedUsernames;
00617                 // Must be a valid username, obviously ;)
00618                 if ( !self::isValidUserName( $name ) ) {
00619                         return false;
00620                 }
00621 
00622                 static $reservedUsernames = false;
00623                 if ( !$reservedUsernames ) {
00624                         $reservedUsernames = $wgReservedUsernames;
00625                         wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
00626                 }
00627 
00628                 // Certain names may be reserved for batch processes.
00629                 foreach ( $reservedUsernames as $reserved ) {
00630                         if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
00631                                 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
00632                         }
00633                         if ( $reserved == $name ) {
00634                                 return false;
00635                         }
00636                 }
00637                 return true;
00638         }
00639 
00652         public static function isCreatableName( $name ) {
00653                 global $wgInvalidUsernameCharacters;
00654 
00655                 // Ensure that the username isn't longer than 235 bytes, so that
00656                 // (at least for the builtin skins) user javascript and css files
00657                 // will work. (bug 23080)
00658                 if( strlen( $name ) > 235 ) {
00659                         wfDebugLog( 'username', __METHOD__ .
00660                                 ": '$name' invalid due to length" );
00661                         return false;
00662                 }
00663 
00664                 // Preg yells if you try to give it an empty string
00665                 if( $wgInvalidUsernameCharacters !== '' ) {
00666                         if( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
00667                                 wfDebugLog( 'username', __METHOD__ .
00668                                         ": '$name' invalid due to wgInvalidUsernameCharacters" );
00669                                 return false;
00670                         }
00671                 }
00672 
00673                 return self::isUsableName( $name );
00674         }
00675 
00682         public function isValidPassword( $password ) {
00683                 //simple boolean wrapper for getPasswordValidity
00684                 return $this->getPasswordValidity( $password ) === true;
00685         }
00686 
00693         public function getPasswordValidity( $password ) {
00694                 global $wgMinimalPasswordLength, $wgContLang;
00695 
00696                 static $blockedLogins = array(
00697                         'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589
00698                         'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605
00699                 );
00700 
00701                 $result = false; //init $result to false for the internal checks
00702 
00703                 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
00704                         return $result;
00705 
00706                 if ( $result === false ) {
00707                         if( strlen( $password ) < $wgMinimalPasswordLength ) {
00708                                 return 'passwordtooshort';
00709                         } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
00710                                 return 'password-name-match';
00711                         } elseif ( isset( $blockedLogins[ $this->getName() ] ) && $password == $blockedLogins[ $this->getName() ] ) {
00712                                 return 'password-login-forbidden';
00713                         } else {
00714                                 //it seems weird returning true here, but this is because of the
00715                                 //initialization of $result to false above. If the hook is never run or it
00716                                 //doesn't modify $result, then we will likely get down into this if with
00717                                 //a valid password.
00718                                 return true;
00719                         }
00720                 } elseif( $result === true ) {
00721                         return true;
00722                 } else {
00723                         return $result; //the isValidPassword hook set a string $result and returned true
00724                 }
00725         }
00726 
00754         public static function isValidEmailAddr( $addr ) {
00755                 wfDeprecated( __METHOD__, '1.18' );
00756                 return Sanitizer::validateEmail( $addr );
00757         }
00758 
00772         public static function getCanonicalName( $name, $validate = 'valid' ) {
00773                 # Force usernames to capital
00774                 global $wgContLang;
00775                 $name = $wgContLang->ucfirst( $name );
00776 
00777                 # Reject names containing '#'; these will be cleaned up
00778                 # with title normalisation, but then it's too late to
00779                 # check elsewhere
00780                 if( strpos( $name, '#' ) !== false )
00781                         return false;
00782 
00783                 # Clean up name according to title rules
00784                 $t = ( $validate === 'valid' ) ?
00785                         Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
00786                 # Check for invalid titles
00787                 if( is_null( $t ) ) {
00788                         return false;
00789                 }
00790 
00791                 # Reject various classes of invalid names
00792                 global $wgAuth;
00793                 $name = $wgAuth->getCanonicalName( $t->getText() );
00794 
00795                 switch ( $validate ) {
00796                         case false:
00797                                 break;
00798                         case 'valid':
00799                                 if ( !User::isValidUserName( $name ) ) {
00800                                         $name = false;
00801                                 }
00802                                 break;
00803                         case 'usable':
00804                                 if ( !User::isUsableName( $name ) ) {
00805                                         $name = false;
00806                                 }
00807                                 break;
00808                         case 'creatable':
00809                                 if ( !User::isCreatableName( $name ) ) {
00810                                         $name = false;
00811                                 }
00812                                 break;
00813                         default:
00814                                 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
00815                 }
00816                 return $name;
00817         }
00818 
00827         public static function edits( $uid ) {
00828                 wfDeprecated( __METHOD__, '1.21' );
00829                 $user = self::newFromId( $uid );
00830                 return $user->getEditCount();
00831         }
00832 
00838         public static function randomPassword() {
00839                 global $wgMinimalPasswordLength;
00840                 // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
00841                 $length = max( 10, $wgMinimalPasswordLength );
00842                 // Multiply by 1.25 to get the number of hex characters we need
00843                 $length = $length * 1.25;
00844                 // Generate random hex chars
00845                 $hex = MWCryptRand::generateHex( $length );
00846                 // Convert from base 16 to base 32 to get a proper password like string
00847                 return wfBaseConvert( $hex, 16, 32 );
00848         }
00849 
00858         public function loadDefaults( $name = false ) {
00859                 wfProfileIn( __METHOD__ );
00860 
00861                 $this->mId = 0;
00862                 $this->mName = $name;
00863                 $this->mRealName = '';
00864                 $this->mPassword = $this->mNewpassword = '';
00865                 $this->mNewpassTime = null;
00866                 $this->mEmail = '';
00867                 $this->mOptionOverrides = null;
00868                 $this->mOptionsLoaded = false;
00869 
00870                 $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
00871                 if( $loggedOut !== null ) {
00872                         $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
00873                 } else {
00874                         $this->mTouched = '1'; # Allow any pages to be cached
00875                 }
00876 
00877                 $this->mToken = null; // Don't run cryptographic functions till we need a token
00878                 $this->mEmailAuthenticated = null;
00879                 $this->mEmailToken = '';
00880                 $this->mEmailTokenExpires = null;
00881                 $this->mRegistration = wfTimestamp( TS_MW );
00882                 $this->mGroups = array();
00883 
00884                 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
00885 
00886                 wfProfileOut( __METHOD__ );
00887         }
00888 
00901         public function isItemLoaded( $item, $all = 'all' ) {
00902                 return ( $this->mLoadedItems === true && $all === 'all' ) ||
00903                         ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
00904         }
00905 
00911         private function setItemLoaded( $item ) {
00912                 if ( is_array( $this->mLoadedItems ) ) {
00913                         $this->mLoadedItems[$item] = true;
00914                 }
00915         }
00916 
00921         private function loadFromSession() {
00922                 global $wgExternalAuthType, $wgAutocreatePolicy;
00923 
00924                 $result = null;
00925                 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
00926                 if ( $result !== null ) {
00927                         return $result;
00928                 }
00929 
00930                 if ( $wgExternalAuthType && $wgAutocreatePolicy == 'view' ) {
00931                         $extUser = ExternalUser::newFromCookie();
00932                         if ( $extUser ) {
00933                                 # TODO: Automatically create the user here (or probably a bit
00934                                 # lower down, in fact)
00935                         }
00936                 }
00937 
00938                 $request = $this->getRequest();
00939 
00940                 $cookieId = $request->getCookie( 'UserID' );
00941                 $sessId = $request->getSessionData( 'wsUserID' );
00942 
00943                 if ( $cookieId !== null ) {
00944                         $sId = intval( $cookieId );
00945                         if( $sessId !== null && $cookieId != $sessId ) {
00946                                 wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
00947                                         cookie user ID ($sId) don't match!" );
00948                                 return false;
00949                         }
00950                         $request->setSessionData( 'wsUserID', $sId );
00951                 } elseif ( $sessId !== null && $sessId != 0 ) {
00952                         $sId = $sessId;
00953                 } else {
00954                         return false;
00955                 }
00956 
00957                 if ( $request->getSessionData( 'wsUserName' ) !== null ) {
00958                         $sName = $request->getSessionData( 'wsUserName' );
00959                 } elseif ( $request->getCookie( 'UserName' ) !== null ) {
00960                         $sName = $request->getCookie( 'UserName' );
00961                         $request->setSessionData( 'wsUserName', $sName );
00962                 } else {
00963                         return false;
00964                 }
00965 
00966                 $proposedUser = User::newFromId( $sId );
00967                 if ( !$proposedUser->isLoggedIn() ) {
00968                         # Not a valid ID
00969                         return false;
00970                 }
00971 
00972                 global $wgBlockDisablesLogin;
00973                 if( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
00974                         # User blocked and we've disabled blocked user logins
00975                         return false;
00976                 }
00977 
00978                 if ( $request->getSessionData( 'wsToken' ) ) {
00979                         $passwordCorrect = $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' );
00980                         $from = 'session';
00981                 } elseif ( $request->getCookie( 'Token' ) ) {
00982                         $passwordCorrect = $proposedUser->getToken( false ) === $request->getCookie( 'Token' );
00983                         $from = 'cookie';
00984                 } else {
00985                         # No session or persistent login cookie
00986                         return false;
00987                 }
00988 
00989                 if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
00990                         $this->loadFromUserObject( $proposedUser );
00991                         $request->setSessionData( 'wsToken', $this->mToken );
00992                         wfDebug( "User: logged in from $from\n" );
00993                         return true;
00994                 } else {
00995                         # Invalid credentials
00996                         wfDebug( "User: can't log in from $from, invalid credentials\n" );
00997                         return false;
00998                 }
00999         }
01000 
01007         public function loadFromDatabase() {
01008                 # Paranoia
01009                 $this->mId = intval( $this->mId );
01010 
01012                 if( !$this->mId ) {
01013                         $this->loadDefaults();
01014                         return false;
01015                 }
01016 
01017                 $dbr = wfGetDB( DB_MASTER );
01018                 $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ );
01019 
01020                 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
01021 
01022                 if ( $s !== false ) {
01023                         # Initialise user table data
01024                         $this->loadFromRow( $s );
01025                         $this->mGroups = null; // deferred
01026                         $this->getEditCount(); // revalidation for nulls
01027                         return true;
01028                 } else {
01029                         # Invalid user_id
01030                         $this->mId = 0;
01031                         $this->loadDefaults();
01032                         return false;
01033                 }
01034         }
01035 
01045         public function loadFromRow( $row, $data = null ) {
01046                 $all = true;
01047 
01048                 $this->mGroups = null; // deferred
01049 
01050                 if ( isset( $row->user_name ) ) {
01051                         $this->mName = $row->user_name;
01052                         $this->mFrom = 'name';
01053                         $this->setItemLoaded( 'name' );
01054                 } else {
01055                         $all = false;
01056                 }
01057 
01058                 if ( isset( $row->user_real_name ) ) {
01059                         $this->mRealName = $row->user_real_name;
01060                         $this->setItemLoaded( 'realname' );
01061                 } else {
01062                         $all = false;
01063                 }
01064 
01065                 if ( isset( $row->user_id ) ) {
01066                         $this->mId = intval( $row->user_id );
01067                         $this->mFrom = 'id';
01068                         $this->setItemLoaded( 'id' );
01069                 } else {
01070                         $all = false;
01071                 }
01072 
01073                 if ( isset( $row->user_editcount ) ) {
01074                         $this->mEditCount = $row->user_editcount;
01075                 } else {
01076                         $all = false;
01077                 }
01078 
01079                 if ( isset( $row->user_password ) ) {
01080                         $this->mPassword = $row->user_password;
01081                         $this->mNewpassword = $row->user_newpassword;
01082                         $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
01083                         $this->mEmail = $row->user_email;
01084                         if ( isset( $row->user_options ) ) {
01085                                 $this->decodeOptions( $row->user_options );
01086                         }
01087                         $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
01088                         $this->mToken = $row->user_token;
01089                         if ( $this->mToken == '' ) {
01090                                 $this->mToken = null;
01091                         }
01092                         $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
01093                         $this->mEmailToken = $row->user_email_token;
01094                         $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
01095                         $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
01096                 } else {
01097                         $all = false;
01098                 }
01099 
01100                 if ( $all ) {
01101                         $this->mLoadedItems = true;
01102                 }
01103 
01104                 if ( is_array( $data ) ) {
01105                         if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
01106                                 $this->mGroups = $data['user_groups'];
01107                         }
01108                         if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
01109                                 $this->loadOptions( $data['user_properties'] );
01110                         }
01111                 }
01112         }
01113 
01119         protected function loadFromUserObject( $user ) {
01120                 $user->load();
01121                 $user->loadGroups();
01122                 $user->loadOptions();
01123                 foreach ( self::$mCacheVars as $var ) {
01124                         $this->$var = $user->$var;
01125                 }
01126         }
01127 
01131         private function loadGroups() {
01132                 if ( is_null( $this->mGroups ) ) {
01133                         $dbr = wfGetDB( DB_MASTER );
01134                         $res = $dbr->select( 'user_groups',
01135                                 array( 'ug_group' ),
01136                                 array( 'ug_user' => $this->mId ),
01137                                 __METHOD__ );
01138                         $this->mGroups = array();
01139                         foreach ( $res as $row ) {
01140                                 $this->mGroups[] = $row->ug_group;
01141                         }
01142                 }
01143         }
01144 
01159         public function addAutopromoteOnceGroups( $event ) {
01160                 global $wgAutopromoteOnceLogInRC;
01161 
01162                 $toPromote = array();
01163                 if ( $this->getId() ) {
01164                         $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
01165                         if ( count( $toPromote ) ) {
01166                                 $oldGroups = $this->getGroups(); // previous groups
01167                                 foreach ( $toPromote as $group ) {
01168                                         $this->addGroup( $group );
01169                                 }
01170                                 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
01171 
01172                                 $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
01173                                 $logEntry->setPerformer( $this );
01174                                 $logEntry->setTarget( $this->getUserPage() );
01175                                 $logEntry->setParameters( array(
01176                                         '4::oldgroups' => $oldGroups,
01177                                         '5::newgroups' => $newGroups,
01178                                 ) );
01179                                 $logid = $logEntry->insert();
01180                                 if ( $wgAutopromoteOnceLogInRC ) {
01181                                         $logEntry->publish( $logid );
01182                                 }
01183                         }
01184                 }
01185                 return $toPromote;
01186         }
01187 
01196         public function clearInstanceCache( $reloadFrom = false ) {
01197                 $this->mNewtalk = -1;
01198                 $this->mDatePreference = null;
01199                 $this->mBlockedby = -1; # Unset
01200                 $this->mHash = false;
01201                 $this->mRights = null;
01202                 $this->mEffectiveGroups = null;
01203                 $this->mImplicitGroups = null;
01204                 $this->mOptions = null;
01205                 $this->mOptionsLoaded = false;
01206                 $this->mEditCount = null;
01207 
01208                 if ( $reloadFrom ) {
01209                         $this->mLoadedItems = array();
01210                         $this->mFrom = $reloadFrom;
01211                 }
01212         }
01213 
01220         public static function getDefaultOptions() {
01221                 global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
01222 
01223                 static $defOpt = null;
01224                 if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) {
01225                         // Disabling this for the unit tests, as they rely on being able to change $wgContLang
01226                         // mid-request and see that change reflected in the return value of this function.
01227                         // Which is insane and would never happen during normal MW operation
01228                         return $defOpt;
01229                 }
01230 
01231                 $defOpt = $wgDefaultUserOptions;
01232                 # default language setting
01233                 $defOpt['variant'] = $wgContLang->getCode();
01234                 $defOpt['language'] = $wgContLang->getCode();
01235                 foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
01236                         $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
01237                 }
01238                 $defOpt['skin'] = $wgDefaultSkin;
01239 
01240                 wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) );
01241 
01242                 return $defOpt;
01243         }
01244 
01251         public static function getDefaultOption( $opt ) {
01252                 $defOpts = self::getDefaultOptions();
01253                 if( isset( $defOpts[$opt] ) ) {
01254                         return $defOpts[$opt];
01255                 } else {
01256                         return null;
01257                 }
01258         }
01259 
01260 
01268         private function getBlockedStatus( $bFromSlave = true ) {
01269                 global $wgProxyWhitelist, $wgUser;
01270 
01271                 if ( -1 != $this->mBlockedby ) {
01272                         return;
01273                 }
01274 
01275                 wfProfileIn( __METHOD__ );
01276                 wfDebug( __METHOD__.": checking...\n" );
01277 
01278                 // Initialize data...
01279                 // Otherwise something ends up stomping on $this->mBlockedby when
01280                 // things get lazy-loaded later, causing false positive block hits
01281                 // due to -1 !== 0. Probably session-related... Nothing should be
01282                 // overwriting mBlockedby, surely?
01283                 $this->load();
01284 
01285                 # We only need to worry about passing the IP address to the Block generator if the
01286                 # user is not immune to autoblocks/hardblocks, and they are the current user so we
01287                 # know which IP address they're actually coming from
01288                 if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) {
01289                         $ip = $this->getRequest()->getIP();
01290                 } else {
01291                         $ip = null;
01292                 }
01293 
01294                 # User/IP blocking
01295                 $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
01296 
01297                 # Proxy blocking
01298                 if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
01299                         && !in_array( $ip, $wgProxyWhitelist ) )
01300                 {
01301                         # Local list
01302                         if ( self::isLocallyBlockedProxy( $ip ) ) {
01303                                 $block = new Block;
01304                                 $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
01305                                 $block->mReason = wfMessage( 'proxyblockreason' )->text();
01306                                 $block->setTarget( $ip );
01307                         } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
01308                                 $block = new Block;
01309                                 $block->setBlocker( wfMessage( 'sorbs' )->text() );
01310                                 $block->mReason = wfMessage( 'sorbsreason' )->text();
01311                                 $block->setTarget( $ip );
01312                         }
01313                 }
01314 
01315                 if ( $block instanceof Block ) {
01316                         wfDebug( __METHOD__ . ": Found block.\n" );
01317                         $this->mBlock = $block;
01318                         $this->mBlockedby = $block->getByName();
01319                         $this->mBlockreason = $block->mReason;
01320                         $this->mHideName = $block->mHideName;
01321                         $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
01322                 } else {
01323                         $this->mBlockedby = '';
01324                         $this->mHideName = 0;
01325                         $this->mAllowUsertalk = false;
01326                 }
01327 
01328                 # Extensions
01329                 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
01330 
01331                 wfProfileOut( __METHOD__ );
01332         }
01333 
01341         public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
01342                 global $wgEnableSorbs, $wgEnableDnsBlacklist,
01343                         $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
01344 
01345                 if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs )
01346                         return false;
01347 
01348                 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
01349                         return false;
01350 
01351                 $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
01352                 return $this->inDnsBlacklist( $ip, $urls );
01353         }
01354 
01362         public function inDnsBlacklist( $ip, $bases ) {
01363                 wfProfileIn( __METHOD__ );
01364 
01365                 $found = false;
01366                 // @todo FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
01367                 if( IP::isIPv4( $ip ) ) {
01368                         # Reverse IP, bug 21255
01369                         $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
01370 
01371                         foreach( (array)$bases as $base ) {
01372                                 # Make hostname
01373                                 # If we have an access key, use that too (ProjectHoneypot, etc.)
01374                                 if( is_array( $base ) ) {
01375                                         if( count( $base ) >= 2 ) {
01376                                                 # Access key is 1, base URL is 0
01377                                                 $host = "{$base[1]}.$ipReversed.{$base[0]}";
01378                                         } else {
01379                                                 $host = "$ipReversed.{$base[0]}";
01380                                         }
01381                                 } else {
01382                                         $host = "$ipReversed.$base";
01383                                 }
01384 
01385                                 # Send query
01386                                 $ipList = gethostbynamel( $host );
01387 
01388                                 if( $ipList ) {
01389                                         wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
01390                                         $found = true;
01391                                         break;
01392                                 } else {
01393                                         wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base.\n" );
01394                                 }
01395                         }
01396                 }
01397 
01398                 wfProfileOut( __METHOD__ );
01399                 return $found;
01400         }
01401 
01409         public static function isLocallyBlockedProxy( $ip ) {
01410                 global $wgProxyList;
01411 
01412                 if ( !$wgProxyList ) {
01413                         return false;
01414                 }
01415                 wfProfileIn( __METHOD__ );
01416 
01417                 if ( !is_array( $wgProxyList ) ) {
01418                         # Load from the specified file
01419                         $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
01420                 }
01421 
01422                 if ( !is_array( $wgProxyList ) ) {
01423                         $ret = false;
01424                 } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
01425                         $ret = true;
01426                 } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
01427                         # Old-style flipped proxy list
01428                         $ret = true;
01429                 } else {
01430                         $ret = false;
01431                 }
01432                 wfProfileOut( __METHOD__ );
01433                 return $ret;
01434         }
01435 
01441         public function isPingLimitable() {
01442                 global $wgRateLimitsExcludedIPs;
01443                 if( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
01444                         // No other good way currently to disable rate limits
01445                         // for specific IPs. :P
01446                         // But this is a crappy hack and should die.
01447                         return false;
01448                 }
01449                 return !$this->isAllowed('noratelimit');
01450         }
01451 
01462         public function pingLimiter( $action = 'edit' ) {
01463                 # Call the 'PingLimiter' hook
01464                 $result = false;
01465                 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
01466                         return $result;
01467                 }
01468 
01469                 global $wgRateLimits;
01470                 if( !isset( $wgRateLimits[$action] ) ) {
01471                         return false;
01472                 }
01473 
01474                 # Some groups shouldn't trigger the ping limiter, ever
01475                 if( !$this->isPingLimitable() )
01476                         return false;
01477 
01478                 global $wgMemc, $wgRateLimitLog;
01479                 wfProfileIn( __METHOD__ );
01480 
01481                 $limits = $wgRateLimits[$action];
01482                 $keys = array();
01483                 $id = $this->getId();
01484                 $ip = $this->getRequest()->getIP();
01485                 $userLimit = false;
01486 
01487                 if( isset( $limits['anon'] ) && $id == 0 ) {
01488                         $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
01489                 }
01490 
01491                 if( isset( $limits['user'] ) && $id != 0 ) {
01492                         $userLimit = $limits['user'];
01493                 }
01494                 if( $this->isNewbie() ) {
01495                         if( isset( $limits['newbie'] ) && $id != 0 ) {
01496                                 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
01497                         }
01498                         if( isset( $limits['ip'] ) ) {
01499                                 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
01500                         }
01501                         $matches = array();
01502                         if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
01503                                 $subnet = $matches[1];
01504                                 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
01505                         }
01506                 }
01507                 // Check for group-specific permissions
01508                 // If more than one group applies, use the group with the highest limit
01509                 foreach ( $this->getGroups() as $group ) {
01510                         if ( isset( $limits[$group] ) ) {
01511                                 if ( $userLimit === false || $limits[$group] > $userLimit ) {
01512                                         $userLimit = $limits[$group];
01513                                 }
01514                         }
01515                 }
01516                 // Set the user limit key
01517                 if ( $userLimit !== false ) {
01518                         wfDebug( __METHOD__ . ": effective user limit: $userLimit\n" );
01519                         $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
01520                 }
01521 
01522                 $triggered = false;
01523                 foreach( $keys as $key => $limit ) {
01524                         list( $max, $period ) = $limit;
01525                         $summary = "(limit $max in {$period}s)";
01526                         $count = $wgMemc->get( $key );
01527                         // Already pinged?
01528                         if( $count ) {
01529                                 if( $count >= $max ) {
01530                                         wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
01531                                         if( $wgRateLimitLog ) {
01532                                                 wfSuppressWarnings();
01533                                                 file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND );
01534                                                 wfRestoreWarnings();
01535                                         }
01536                                         $triggered = true;
01537                                 } else {
01538                                         wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
01539                                 }
01540                         } else {
01541                                 wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
01542                                 $wgMemc->add( $key, 0, intval( $period ) ); // first ping
01543                         }
01544                         $wgMemc->incr( $key );
01545                 }
01546 
01547                 wfProfileOut( __METHOD__ );
01548                 return $triggered;
01549         }
01550 
01557         public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
01558                 return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
01559         }
01560 
01567         public function getBlock( $bFromSlave = true ){
01568                 $this->getBlockedStatus( $bFromSlave );
01569                 return $this->mBlock instanceof Block ? $this->mBlock : null;
01570         }
01571 
01579         function isBlockedFrom( $title, $bFromSlave = false ) {
01580                 global $wgBlockAllowsUTEdit;
01581                 wfProfileIn( __METHOD__ );
01582 
01583                 $blocked = $this->isBlocked( $bFromSlave );
01584                 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
01585                 # If a user's name is suppressed, they cannot make edits anywhere
01586                 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
01587                   $title->getNamespace() == NS_USER_TALK ) {
01588                         $blocked = false;
01589                         wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
01590                 }
01591 
01592                 wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
01593 
01594                 wfProfileOut( __METHOD__ );
01595                 return $blocked;
01596         }
01597 
01602         public function blockedBy() {
01603                 $this->getBlockedStatus();
01604                 return $this->mBlockedby;
01605         }
01606 
01611         public function blockedFor() {
01612                 $this->getBlockedStatus();
01613                 return $this->mBlockreason;
01614         }
01615 
01620         public function getBlockId() {
01621                 $this->getBlockedStatus();
01622                 return ( $this->mBlock ? $this->mBlock->getId() : false );
01623         }
01624 
01633         public function isBlockedGlobally( $ip = '' ) {
01634                 if( $this->mBlockedGlobally !== null ) {
01635                         return $this->mBlockedGlobally;
01636                 }
01637                 // User is already an IP?
01638                 if( IP::isIPAddress( $this->getName() ) ) {
01639                         $ip = $this->getName();
01640                 } elseif( !$ip ) {
01641                         $ip = $this->getRequest()->getIP();
01642                 }
01643                 $blocked = false;
01644                 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
01645                 $this->mBlockedGlobally = (bool)$blocked;
01646                 return $this->mBlockedGlobally;
01647         }
01648 
01654         public function isLocked() {
01655                 if( $this->mLocked !== null ) {
01656                         return $this->mLocked;
01657                 }
01658                 global $wgAuth;
01659                 $authUser = $wgAuth->getUserInstance( $this );
01660                 $this->mLocked = (bool)$authUser->isLocked();
01661                 return $this->mLocked;
01662         }
01663 
01669         public function isHidden() {
01670                 if( $this->mHideName !== null ) {
01671                         return $this->mHideName;
01672                 }
01673                 $this->getBlockedStatus();
01674                 if( !$this->mHideName ) {
01675                         global $wgAuth;
01676                         $authUser = $wgAuth->getUserInstance( $this );
01677                         $this->mHideName = (bool)$authUser->isHidden();
01678                 }
01679                 return $this->mHideName;
01680         }
01681 
01686         public function getId() {
01687                 if( $this->mId === null && $this->mName !== null
01688                 && User::isIP( $this->mName ) ) {
01689                         // Special case, we know the user is anonymous
01690                         return 0;
01691                 } elseif( !$this->isItemLoaded( 'id' ) ) {
01692                         // Don't load if this was initialized from an ID
01693                         $this->load();
01694                 }
01695                 return $this->mId;
01696         }
01697 
01702         public function setId( $v ) {
01703                 $this->mId = $v;
01704                 $this->clearInstanceCache( 'id' );
01705         }
01706 
01711         public function getName() {
01712                 if ( $this->isItemLoaded( 'name', 'only' ) ) {
01713                         # Special case optimisation
01714                         return $this->mName;
01715                 } else {
01716                         $this->load();
01717                         if ( $this->mName === false ) {
01718                                 # Clean up IPs
01719                                 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
01720                         }
01721                         return $this->mName;
01722                 }
01723         }
01724 
01738         public function setName( $str ) {
01739                 $this->load();
01740                 $this->mName = $str;
01741         }
01742 
01747         public function getTitleKey() {
01748                 return str_replace( ' ', '_', $this->getName() );
01749         }
01750 
01755         public function getNewtalk() {
01756                 $this->load();
01757 
01758                 # Load the newtalk status if it is unloaded (mNewtalk=-1)
01759                 if( $this->mNewtalk === -1 ) {
01760                         $this->mNewtalk = false; # reset talk page status
01761 
01762                         # Check memcached separately for anons, who have no
01763                         # entire User object stored in there.
01764                         if( !$this->mId ) {
01765                                 global $wgDisableAnonTalk;
01766                                 if( $wgDisableAnonTalk ) {
01767                                         // Anon newtalk disabled by configuration.
01768                                         $this->mNewtalk = false;
01769                                 } else {
01770                                         global $wgMemc;
01771                                         $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
01772                                         $newtalk = $wgMemc->get( $key );
01773                                         if( strval( $newtalk ) !== '' ) {
01774                                                 $this->mNewtalk = (bool)$newtalk;
01775                                         } else {
01776                                                 // Since we are caching this, make sure it is up to date by getting it
01777                                                 // from the master
01778                                                 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
01779                                                 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
01780                                         }
01781                                 }
01782                         } else {
01783                                 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
01784                         }
01785                 }
01786 
01787                 return (bool)$this->mNewtalk;
01788         }
01789 
01794         public function getNewMessageLinks() {
01795                 $talks = array();
01796                 if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
01797                         return $talks;
01798                 } elseif( !$this->getNewtalk() ) {
01799                         return array();
01800                 }
01801                 $utp = $this->getTalkPage();
01802                 $dbr = wfGetDB( DB_SLAVE );
01803                 // Get the "last viewed rev" timestamp from the oldest message notification
01804                 $timestamp = $dbr->selectField( 'user_newtalk',
01805                         'MIN(user_last_timestamp)',
01806                         $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
01807                         __METHOD__ );
01808                 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
01809                 return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
01810         }
01811 
01821         protected function checkNewtalk( $field, $id, $fromMaster = false ) {
01822                 if ( $fromMaster ) {
01823                         $db = wfGetDB( DB_MASTER );
01824                 } else {
01825                         $db = wfGetDB( DB_SLAVE );
01826                 }
01827                 $ok = $db->selectField( 'user_newtalk', $field,
01828                         array( $field => $id ), __METHOD__ );
01829                 return $ok !== false;
01830         }
01831 
01839         protected function updateNewtalk( $field, $id, $curRev = null ) {
01840                 // Get timestamp of the talk page revision prior to the current one
01841                 $prevRev = $curRev ? $curRev->getPrevious() : false;
01842                 $ts = $prevRev ? $prevRev->getTimestamp() : null;
01843                 // Mark the user as having new messages since this revision
01844                 $dbw = wfGetDB( DB_MASTER );
01845                 $dbw->insert( 'user_newtalk',
01846                         array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
01847                         __METHOD__,
01848                         'IGNORE' );
01849                 if ( $dbw->affectedRows() ) {
01850                         wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
01851                         return true;
01852                 } else {
01853                         wfDebug( __METHOD__ . " already set ($field, $id)\n" );
01854                         return false;
01855                 }
01856         }
01857 
01864         protected function deleteNewtalk( $field, $id ) {
01865                 $dbw = wfGetDB( DB_MASTER );
01866                 $dbw->delete( 'user_newtalk',
01867                         array( $field => $id ),
01868                         __METHOD__ );
01869                 if ( $dbw->affectedRows() ) {
01870                         wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
01871                         return true;
01872                 } else {
01873                         wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
01874                         return false;
01875                 }
01876         }
01877 
01883         public function setNewtalk( $val, $curRev = null ) {
01884                 if( wfReadOnly() ) {
01885                         return;
01886                 }
01887 
01888                 $this->load();
01889                 $this->mNewtalk = $val;
01890 
01891                 if( $this->isAnon() ) {
01892                         $field = 'user_ip';
01893                         $id = $this->getName();
01894                 } else {
01895                         $field = 'user_id';
01896                         $id = $this->getId();
01897                 }
01898                 global $wgMemc;
01899 
01900                 if( $val ) {
01901                         $changed = $this->updateNewtalk( $field, $id, $curRev );
01902                 } else {
01903                         $changed = $this->deleteNewtalk( $field, $id );
01904                 }
01905 
01906                 if( $this->isAnon() ) {
01907                         // Anons have a separate memcached space, since
01908                         // user records aren't kept for them.
01909                         $key = wfMemcKey( 'newtalk', 'ip', $id );
01910                         $wgMemc->set( $key, $val ? 1 : 0, 1800 );
01911                 }
01912                 if ( $changed ) {
01913                         $this->invalidateCache();
01914                 }
01915         }
01916 
01922         private static function newTouchedTimestamp() {
01923                 global $wgClockSkewFudge;
01924                 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
01925         }
01926 
01934         private function clearSharedCache() {
01935                 $this->load();
01936                 if( $this->mId ) {
01937                         global $wgMemc;
01938                         $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
01939                 }
01940         }
01941 
01947         public function invalidateCache() {
01948                 if( wfReadOnly() ) {
01949                         return;
01950                 }
01951                 $this->load();
01952                 if( $this->mId ) {
01953                         $this->mTouched = self::newTouchedTimestamp();
01954 
01955                         $dbw = wfGetDB( DB_MASTER );
01956 
01957                         // Prevent contention slams by checking user_touched first
01958                         $now = $dbw->timestamp( $this->mTouched );
01959                         $needsPurge = $dbw->selectField( 'user', '1',
01960                                 array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) )
01961                         );
01962                         if ( $needsPurge ) {
01963                                 $dbw->update( 'user',
01964                                         array( 'user_touched' => $now ),
01965                                         array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) ),
01966                                         __METHOD__
01967                                 );
01968                         }
01969 
01970                         $this->clearSharedCache();
01971                 }
01972         }
01973 
01980         public function validateCache( $timestamp ) {
01981                 $this->load();
01982                 return ( $timestamp >= $this->mTouched );
01983         }
01984 
01989         public function getTouched() {
01990                 $this->load();
01991                 return $this->mTouched;
01992         }
01993 
02010         public function setPassword( $str ) {
02011                 global $wgAuth;
02012 
02013                 if( $str !== null ) {
02014                         if( !$wgAuth->allowPasswordChange() ) {
02015                                 throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
02016                         }
02017 
02018                         if( !$this->isValidPassword( $str ) ) {
02019                                 global $wgMinimalPasswordLength;
02020                                 $valid = $this->getPasswordValidity( $str );
02021                                 if ( is_array( $valid ) ) {
02022                                         $message = array_shift( $valid );
02023                                         $params = $valid;
02024                                 } else {
02025                                         $message = $valid;
02026                                         $params = array( $wgMinimalPasswordLength );
02027                                 }
02028                                 throw new PasswordError( wfMessage( $message, $params )->text() );
02029                         }
02030                 }
02031 
02032                 if( !$wgAuth->setPassword( $this, $str ) ) {
02033                         throw new PasswordError( wfMessage( 'externaldberror' )->text() );
02034                 }
02035 
02036                 $this->setInternalPassword( $str );
02037 
02038                 return true;
02039         }
02040 
02046         public function setInternalPassword( $str ) {
02047                 $this->load();
02048                 $this->setToken();
02049 
02050                 if( $str === null ) {
02051                         // Save an invalid hash...
02052                         $this->mPassword = '';
02053                 } else {
02054                         $this->mPassword = self::crypt( $str );
02055                 }
02056                 $this->mNewpassword = '';
02057                 $this->mNewpassTime = null;
02058         }
02059 
02065         public function getToken( $forceCreation = true ) {
02066                 $this->load();
02067                 if ( !$this->mToken && $forceCreation ) {
02068                         $this->setToken();
02069                 }
02070                 return $this->mToken;
02071         }
02072 
02079         public function setToken( $token = false ) {
02080                 $this->load();
02081                 if ( !$token ) {
02082                         $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH );
02083                 } else {
02084                         $this->mToken = $token;
02085                 }
02086         }
02087 
02094         public function setNewpassword( $str, $throttle = true ) {
02095                 $this->load();
02096                 $this->mNewpassword = self::crypt( $str );
02097                 if ( $throttle ) {
02098                         $this->mNewpassTime = wfTimestampNow();
02099                 }
02100         }
02101 
02107         public function isPasswordReminderThrottled() {
02108                 global $wgPasswordReminderResendTime;
02109                 $this->load();
02110                 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
02111                         return false;
02112                 }
02113                 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
02114                 return time() < $expiry;
02115         }
02116 
02121         public function getEmail() {
02122                 $this->load();
02123                 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
02124                 return $this->mEmail;
02125         }
02126 
02131         public function getEmailAuthenticationTimestamp() {
02132                 $this->load();
02133                 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
02134                 return $this->mEmailAuthenticated;
02135         }
02136 
02141         public function setEmail( $str ) {
02142                 $this->load();
02143                 if( $str == $this->mEmail ) {
02144                         return;
02145                 }
02146                 $this->mEmail = $str;
02147                 $this->invalidateEmail();
02148                 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
02149         }
02150 
02158         public function setEmailWithConfirmation( $str ) {
02159                 global $wgEnableEmail, $wgEmailAuthentication;
02160 
02161                 if ( !$wgEnableEmail ) {
02162                         return Status::newFatal( 'emaildisabled' );
02163                 }
02164 
02165                 $oldaddr = $this->getEmail();
02166                 if ( $str === $oldaddr ) {
02167                         return Status::newGood( true );
02168                 }
02169 
02170                 $this->setEmail( $str );
02171 
02172                 if ( $str !== '' && $wgEmailAuthentication ) {
02173                         # Send a confirmation request to the new address if needed
02174                         $type = $oldaddr != '' ? 'changed' : 'set';
02175                         $result = $this->sendConfirmationMail( $type );
02176                         if ( $result->isGood() ) {
02177                                 # Say the the caller that a confirmation mail has been sent
02178                                 $result->value = 'eauth';
02179                         }
02180                 } else {
02181                         $result = Status::newGood( true );
02182                 }
02183 
02184                 return $result;
02185         }
02186 
02191         public function getRealName() {
02192                 if ( !$this->isItemLoaded( 'realname' ) ) {
02193                         $this->load();
02194                 }
02195 
02196                 return $this->mRealName;
02197         }
02198 
02203         public function setRealName( $str ) {
02204                 $this->load();
02205                 $this->mRealName = $str;
02206         }
02207 
02218         public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
02219                 global $wgHiddenPrefs;
02220                 $this->loadOptions();
02221 
02222                 # We want 'disabled' preferences to always behave as the default value for
02223                 # users, even if they have set the option explicitly in their settings (ie they
02224                 # set it, and then it was disabled removing their ability to change it).  But
02225                 # we don't want to erase the preferences in the database in case the preference
02226                 # is re-enabled again.  So don't touch $mOptions, just override the returned value
02227                 if( in_array( $oname, $wgHiddenPrefs ) && !$ignoreHidden ){
02228                         return self::getDefaultOption( $oname );
02229                 }
02230 
02231                 if ( array_key_exists( $oname, $this->mOptions ) ) {
02232                         return $this->mOptions[$oname];
02233                 } else {
02234                         return $defaultOverride;
02235                 }
02236         }
02237 
02243         public function getOptions() {
02244                 global $wgHiddenPrefs;
02245                 $this->loadOptions();
02246                 $options = $this->mOptions;
02247 
02248                 # We want 'disabled' preferences to always behave as the default value for
02249                 # users, even if they have set the option explicitly in their settings (ie they
02250                 # set it, and then it was disabled removing their ability to change it).  But
02251                 # we don't want to erase the preferences in the database in case the preference
02252                 # is re-enabled again.  So don't touch $mOptions, just override the returned value
02253                 foreach( $wgHiddenPrefs as $pref ){
02254                         $default = self::getDefaultOption( $pref );
02255                         if( $default !== null ){
02256                                 $options[$pref] = $default;
02257                         }
02258                 }
02259 
02260                 return $options;
02261         }
02262 
02270         public function getBoolOption( $oname ) {
02271                 return (bool)$this->getOption( $oname );
02272         }
02273 
02282         public function getIntOption( $oname, $defaultOverride=0 ) {
02283                 $val = $this->getOption( $oname );
02284                 if( $val == '' ) {
02285                         $val = $defaultOverride;
02286                 }
02287                 return intval( $val );
02288         }
02289 
02296         public function setOption( $oname, $val ) {
02297                 $this->loadOptions();
02298 
02299                 // Explicitly NULL values should refer to defaults
02300                 if( is_null( $val ) ) {
02301                         $val = self::getDefaultOption( $oname );
02302                 }
02303 
02304                 $this->mOptions[$oname] = $val;
02305         }
02306 
02310         public function resetOptions() {
02311                 $this->load();
02312 
02313                 $this->mOptions = self::getDefaultOptions();
02314                 $this->mOptionsLoaded = true;
02315         }
02316 
02321         public function getDatePreference() {
02322                 // Important migration for old data rows
02323                 if ( is_null( $this->mDatePreference ) ) {
02324                         global $wgLang;
02325                         $value = $this->getOption( 'date' );
02326                         $map = $wgLang->getDatePreferenceMigrationMap();
02327                         if ( isset( $map[$value] ) ) {
02328                                 $value = $map[$value];
02329                         }
02330                         $this->mDatePreference = $value;
02331                 }
02332                 return $this->mDatePreference;
02333         }
02334 
02340         public function getStubThreshold() {
02341                 global $wgMaxArticleSize; # Maximum article size, in Kb
02342                 $threshold = intval( $this->getOption( 'stubthreshold' ) );
02343                 if ( $threshold > $wgMaxArticleSize * 1024 ) {
02344                         # If they have set an impossible value, disable the preference
02345                         # so we can use the parser cache again.
02346                         $threshold = 0;
02347                 }
02348                 return $threshold;
02349         }
02350 
02355         public function getRights() {
02356                 if ( is_null( $this->mRights ) ) {
02357                         $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
02358                         wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
02359                         // Force reindexation of rights when a hook has unset one of them
02360                         $this->mRights = array_values( array_unique( $this->mRights ) );
02361                 }
02362                 return $this->mRights;
02363         }
02364 
02370         public function getGroups() {
02371                 $this->load();
02372                 $this->loadGroups();
02373                 return $this->mGroups;
02374         }
02375 
02383         public function getEffectiveGroups( $recache = false ) {
02384                 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
02385                         wfProfileIn( __METHOD__ );
02386                         $this->mEffectiveGroups = array_unique( array_merge(
02387                                 $this->getGroups(), // explicit groups
02388                                 $this->getAutomaticGroups( $recache ) // implicit groups
02389                         ) );
02390                         # Hook for additional groups
02391                         wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
02392                         // Force reindexation of groups when a hook has unset one of them
02393                         $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
02394                         wfProfileOut( __METHOD__ );
02395                 }
02396                 return $this->mEffectiveGroups;
02397         }
02398 
02406         public function getAutomaticGroups( $recache = false ) {
02407                 if ( $recache || is_null( $this->mImplicitGroups ) ) {
02408                         wfProfileIn( __METHOD__ );
02409                         $this->mImplicitGroups = array( '*' );
02410                         if ( $this->getId() ) {
02411                                 $this->mImplicitGroups[] = 'user';
02412 
02413                                 $this->mImplicitGroups = array_unique( array_merge(
02414                                         $this->mImplicitGroups,
02415                                         Autopromote::getAutopromoteGroups( $this )
02416                                 ) );
02417                         }
02418                         if ( $recache ) {
02419                                 # Assure data consistency with rights/groups,
02420                                 # as getEffectiveGroups() depends on this function
02421                                 $this->mEffectiveGroups = null;
02422                         }
02423                         wfProfileOut( __METHOD__ );
02424                 }
02425                 return $this->mImplicitGroups;
02426         }
02427 
02437         public function getFormerGroups() {
02438                 if( is_null( $this->mFormerGroups ) ) {
02439                         $dbr = wfGetDB( DB_MASTER );
02440                         $res = $dbr->select( 'user_former_groups',
02441                                 array( 'ufg_group' ),
02442                                 array( 'ufg_user' => $this->mId ),
02443                                 __METHOD__ );
02444                         $this->mFormerGroups = array();
02445                         foreach( $res as $row ) {
02446                                 $this->mFormerGroups[] = $row->ufg_group;
02447                         }
02448                 }
02449                 return $this->mFormerGroups;
02450         }
02451 
02456         public function getEditCount() {
02457                 if( $this->getId() ) {
02458                         if ( !isset( $this->mEditCount ) ) {
02459                                 /* Populate the count, if it has not been populated yet */
02460                                 wfProfileIn( __METHOD__ );
02461                                 $dbr = wfGetDB( DB_SLAVE );
02462                                 // check if the user_editcount field has been initialized
02463                                 $count = $dbr->selectField(
02464                                         'user', 'user_editcount',
02465                                         array( 'user_id' => $this->mId ),
02466                                         __METHOD__
02467                                 );
02468 
02469                                 if( $count === null ) {
02470                                         // it has not been initialized. do so.
02471                                         $count = $this->initEditCount();
02472                                 }
02473                                 wfProfileOut( __METHOD__ );
02474                                 $this->mEditCount = intval( $count );
02475                         }
02476                         return $this->mEditCount;
02477                 } else {
02478                         /* nil */
02479                         return null;
02480                 }
02481         }
02482 
02488         public function addGroup( $group ) {
02489                 if( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) {
02490                         $dbw = wfGetDB( DB_MASTER );
02491                         if( $this->getId() ) {
02492                                 $dbw->insert( 'user_groups',
02493                                         array(
02494                                                 'ug_user'  => $this->getID(),
02495                                                 'ug_group' => $group,
02496                                         ),
02497                                         __METHOD__,
02498                                         array( 'IGNORE' ) );
02499                         }
02500                 }
02501                 $this->loadGroups();
02502                 $this->mGroups[] = $group;
02503                 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
02504 
02505                 $this->invalidateCache();
02506         }
02507 
02513         public function removeGroup( $group ) {
02514                 $this->load();
02515                 if( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) {
02516                         $dbw = wfGetDB( DB_MASTER );
02517                         $dbw->delete( 'user_groups',
02518                                 array(
02519                                         'ug_user'  => $this->getID(),
02520                                         'ug_group' => $group,
02521                                 ), __METHOD__ );
02522                         // Remember that the user was in this group
02523                         $dbw->insert( 'user_former_groups',
02524                                 array(
02525                                         'ufg_user'  => $this->getID(),
02526                                         'ufg_group' => $group,
02527                                 ),
02528                                 __METHOD__,
02529                                 array( 'IGNORE' ) );
02530                 }
02531                 $this->loadGroups();
02532                 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
02533                 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
02534 
02535                 $this->invalidateCache();
02536         }
02537 
02542         public function isLoggedIn() {
02543                 return $this->getID() != 0;
02544         }
02545 
02550         public function isAnon() {
02551                 return !$this->isLoggedIn();
02552         }
02553 
02562         public function isAllowedAny( /*...*/ ){
02563                 $permissions = func_get_args();
02564                 foreach( $permissions as $permission ){
02565                         if( $this->isAllowed( $permission ) ){
02566                                 return true;
02567                         }
02568                 }
02569                 return false;
02570         }
02571 
02577         public function isAllowedAll( /*...*/ ){
02578                 $permissions = func_get_args();
02579                 foreach( $permissions as $permission ){
02580                         if( !$this->isAllowed( $permission ) ){
02581                                 return false;
02582                         }
02583                 }
02584                 return true;
02585         }
02586 
02592         public function isAllowed( $action = '' ) {
02593                 if ( $action === '' ) {
02594                         return true; // In the spirit of DWIM
02595                 }
02596                 # Patrolling may not be enabled
02597                 if( $action === 'patrol' || $action === 'autopatrol' ) {
02598                         global $wgUseRCPatrol, $wgUseNPPatrol;
02599                         if( !$wgUseRCPatrol && !$wgUseNPPatrol )
02600                                 return false;
02601                 }
02602                 # Use strict parameter to avoid matching numeric 0 accidentally inserted
02603                 # by misconfiguration: 0 == 'foo'
02604                 return in_array( $action, $this->getRights(), true );
02605         }
02606 
02611         public function useRCPatrol() {
02612                 global $wgUseRCPatrol;
02613                 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
02614         }
02615 
02620         public function useNPPatrol() {
02621                 global $wgUseRCPatrol, $wgUseNPPatrol;
02622                 return( ( $wgUseRCPatrol || $wgUseNPPatrol ) && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) );
02623         }
02624 
02630         public function getRequest() {
02631                 if ( $this->mRequest ) {
02632                         return $this->mRequest;
02633                 } else {
02634                         global $wgRequest;
02635                         return $wgRequest;
02636                 }
02637         }
02638 
02645         public function getSkin() {
02646                 wfDeprecated( __METHOD__, '1.18' );
02647                 return RequestContext::getMain()->getSkin();
02648         }
02649 
02656         public function getWatchedItem( $title ) {
02657                 $key = $title->getNamespace() . ':' . $title->getDBkey();
02658 
02659                 if ( isset( $this->mWatchedItems[$key] ) ) {
02660                         return $this->mWatchedItems[$key];
02661                 }
02662 
02663                 if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
02664                         $this->mWatchedItems = array();
02665                 }
02666 
02667                 $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title );
02668                 return $this->mWatchedItems[$key];
02669         }
02670 
02676         public function isWatched( $title ) {
02677                 return $this->getWatchedItem( $title )->isWatched();
02678         }
02679 
02684         public function addWatch( $title ) {
02685                 $this->getWatchedItem( $title )->addWatch();
02686                 $this->invalidateCache();
02687         }
02688 
02693         public function removeWatch( $title ) {
02694                 $this->getWatchedItem( $title )->removeWatch();
02695                 $this->invalidateCache();
02696         }
02697 
02704         public function clearNotification( &$title ) {
02705                 global $wgUseEnotif, $wgShowUpdatedMarker;
02706 
02707                 # Do nothing if the database is locked to writes
02708                 if( wfReadOnly() ) {
02709                         return;
02710                 }
02711 
02712                 if( $title->getNamespace() == NS_USER_TALK &&
02713                         $title->getText() == $this->getName() ) {
02714                         if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) )
02715                                 return;
02716                         $this->setNewtalk( false );
02717                 }
02718 
02719                 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
02720                         return;
02721                 }
02722 
02723                 if( $this->isAnon() ) {
02724                         // Nothing else to do...
02725                         return;
02726                 }
02727 
02728                 // Only update the timestamp if the page is being watched.
02729                 // The query to find out if it is watched is cached both in memcached and per-invocation,
02730                 // and when it does have to be executed, it can be on a slave
02731                 // If this is the user's newtalk page, we always update the timestamp
02732                 $force = '';
02733                 if ( $title->getNamespace() == NS_USER_TALK &&
02734                         $title->getText() == $this->getName() )
02735                 {
02736                         $force = 'force';
02737                 }
02738 
02739                 $this->getWatchedItem( $title )->resetNotificationTimestamp( $force );
02740         }
02741 
02747         public function clearAllNotifications() {
02748                 global $wgUseEnotif, $wgShowUpdatedMarker;
02749                 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
02750                         $this->setNewtalk( false );
02751                         return;
02752                 }
02753                 $id = $this->getId();
02754                 if( $id != 0 )  {
02755                         $dbw = wfGetDB( DB_MASTER );
02756                         $dbw->update( 'watchlist',
02757                                 array( /* SET */
02758                                         'wl_notificationtimestamp' => null
02759                                 ), array( /* WHERE */
02760                                         'wl_user' => $id
02761                                 ), __METHOD__
02762                         );
02763                 #       We also need to clear here the "you have new message" notification for the own user_talk page
02764                 #       This is cleared one page view later in Article::viewUpdates();
02765                 }
02766         }
02767 
02774         private function decodeOptions( $str ) {
02775                 wfDeprecated( __METHOD__, '1.19' );
02776                 if( !$str )
02777                         return;
02778 
02779                 $this->mOptionsLoaded = true;
02780                 $this->mOptionOverrides = array();
02781 
02782                 // If an option is not set in $str, use the default value
02783                 $this->mOptions = self::getDefaultOptions();
02784 
02785                 $a = explode( "\n", $str );
02786                 foreach ( $a as $s ) {
02787                         $m = array();
02788                         if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
02789                                 $this->mOptions[$m[1]] = $m[2];
02790                                 $this->mOptionOverrides[$m[1]] = $m[2];
02791                         }
02792                 }
02793         }
02794 
02807         protected function setCookie( $name, $value, $exp = 0, $secure = null ) {
02808                 $this->getRequest()->response()->setcookie( $name, $value, $exp, null, null, $secure );
02809         }
02810 
02815         protected function clearCookie( $name ) {
02816                 $this->setCookie( $name, '', time() - 86400 );
02817         }
02818 
02826         public function setCookies( $request = null, $secure = null ) {
02827                 if ( $request === null ) {
02828                         $request = $this->getRequest();
02829                 }
02830 
02831                 $this->load();
02832                 if ( 0 == $this->mId ) return;
02833                 if ( !$this->mToken ) {
02834                         // When token is empty or NULL generate a new one and then save it to the database
02835                         // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
02836                         // Simply by setting every cell in the user_token column to NULL and letting them be
02837                         // regenerated as users log back into the wiki.
02838                         $this->setToken();
02839                         $this->saveSettings();
02840                 }
02841                 $session = array(
02842                         'wsUserID' => $this->mId,
02843                         'wsToken' => $this->mToken,
02844                         'wsUserName' => $this->getName()
02845                 );
02846                 $cookies = array(
02847                         'UserID' => $this->mId,
02848                         'UserName' => $this->getName(),
02849                 );
02850                 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
02851                         $cookies['Token'] = $this->mToken;
02852                 } else {
02853                         $cookies['Token'] = false;
02854                 }
02855 
02856                 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
02857 
02858                 foreach ( $session as $name => $value ) {
02859                         $request->setSessionData( $name, $value );
02860                 }
02861                 foreach ( $cookies as $name => $value ) {
02862                         if ( $value === false ) {
02863                                 $this->clearCookie( $name );
02864                         } else {
02865                                 $this->setCookie( $name, $value, 0, $secure );
02866                         }
02867                 }
02868 
02874                 if ( $request->getCheck( 'wpStickHTTPS' ) ) {
02875                         $this->setCookie( 'forceHTTPS', 'true', time() + 2592000, false ); //30 days
02876                 }
02877         }
02878 
02882         public function logout() {
02883                 if( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
02884                         $this->doLogout();
02885                 }
02886         }
02887 
02892         public function doLogout() {
02893                 $this->clearInstanceCache( 'defaults' );
02894 
02895                 $this->getRequest()->setSessionData( 'wsUserID', 0 );
02896 
02897                 $this->clearCookie( 'UserID' );
02898                 $this->clearCookie( 'Token' );
02899                 $this->clearCookie( 'forceHTTPS' );
02900 
02901                 # Remember when user logged out, to prevent seeing cached pages
02902                 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
02903         }
02904 
02909         public function saveSettings() {
02910                 global $wgAuth;
02911 
02912                 $this->load();
02913                 if ( wfReadOnly() ) { return; }
02914                 if ( 0 == $this->mId ) { return; }
02915 
02916                 $this->mTouched = self::newTouchedTimestamp();
02917                 if ( !$wgAuth->allowSetLocalPassword() ) {
02918                         $this->mPassword = '';
02919                 }
02920 
02921                 $dbw = wfGetDB( DB_MASTER );
02922                 $dbw->update( 'user',
02923                         array( /* SET */
02924                                 'user_name' => $this->mName,
02925                                 'user_password' => $this->mPassword,
02926                                 'user_newpassword' => $this->mNewpassword,
02927                                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
02928                                 'user_real_name' => $this->mRealName,
02929                                 'user_email' => $this->mEmail,
02930                                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
02931                                 'user_touched' => $dbw->timestamp( $this->mTouched ),
02932                                 'user_token' => strval( $this->mToken ),
02933                                 'user_email_token' => $this->mEmailToken,
02934                                 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
02935                         ), array( /* WHERE */
02936                                 'user_id' => $this->mId
02937                         ), __METHOD__
02938                 );
02939 
02940                 $this->saveOptions();
02941 
02942                 wfRunHooks( 'UserSaveSettings', array( $this ) );
02943                 $this->clearSharedCache();
02944                 $this->getUserPage()->invalidateCache();
02945         }
02946 
02951         public function idForName() {
02952                 $s = trim( $this->getName() );
02953                 if ( $s === '' ) return 0;
02954 
02955                 $dbr = wfGetDB( DB_SLAVE );
02956                 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
02957                 if ( $id === false ) {
02958                         $id = 0;
02959                 }
02960                 return $id;
02961         }
02962 
02979         public static function createNew( $name, $params = array() ) {
02980                 $user = new User;
02981                 $user->load();
02982                 if ( isset( $params['options'] ) ) {
02983                         $user->mOptions = $params['options'] + (array)$user->mOptions;
02984                         unset( $params['options'] );
02985                 }
02986                 $dbw = wfGetDB( DB_MASTER );
02987                 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
02988 
02989                 $fields = array(
02990                         'user_id' => $seqVal,
02991                         'user_name' => $name,
02992                         'user_password' => $user->mPassword,
02993                         'user_newpassword' => $user->mNewpassword,
02994                         'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
02995                         'user_email' => $user->mEmail,
02996                         'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
02997                         'user_real_name' => $user->mRealName,
02998                         'user_token' => strval( $user->mToken ),
02999                         'user_registration' => $dbw->timestamp( $user->mRegistration ),
03000                         'user_editcount' => 0,
03001                         'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ),
03002                 );
03003                 foreach ( $params as $name => $value ) {
03004                         $fields["user_$name"] = $value;
03005                 }
03006                 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
03007                 if ( $dbw->affectedRows() ) {
03008                         $newUser = User::newFromId( $dbw->insertId() );
03009                 } else {
03010                         $newUser = null;
03011                 }
03012                 return $newUser;
03013         }
03014 
03040         public function addToDatabase() {
03041                 $this->load();
03042 
03043                 $this->mTouched = self::newTouchedTimestamp();
03044 
03045                 $dbw = wfGetDB( DB_MASTER );
03046                 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
03047                 $dbw->insert( 'user',
03048                         array(
03049                                 'user_id' => $seqVal,
03050                                 'user_name' => $this->mName,
03051                                 'user_password' => $this->mPassword,
03052                                 'user_newpassword' => $this->mNewpassword,
03053                                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
03054                                 'user_email' => $this->mEmail,
03055                                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
03056                                 'user_real_name' => $this->mRealName,
03057                                 'user_token' => strval( $this->mToken ),
03058                                 'user_registration' => $dbw->timestamp( $this->mRegistration ),
03059                                 'user_editcount' => 0,
03060                                 'user_touched' => $dbw->timestamp( $this->mTouched ),
03061                         ), __METHOD__,
03062                         array( 'IGNORE' )
03063                 );
03064                 if ( !$dbw->affectedRows() ) {
03065                         $this->mId = $dbw->selectField( 'user', 'user_id',
03066                                 array( 'user_name' => $this->mName ), __METHOD__ );
03067                         $loaded = false;
03068                         if ( $this->mId ) {
03069                                 if ( $this->loadFromDatabase() ) {
03070                                         $loaded = true;
03071                                 }
03072                         }
03073                         if ( !$loaded ) {
03074                                 throw new MWException( __METHOD__. ": hit a key conflict attempting " .
03075                                         "to insert a user row, but then it doesn't exist when we select it!" );
03076                         }
03077                         return Status::newFatal( 'userexists' );
03078                 }
03079                 $this->mId = $dbw->insertId();
03080 
03081                 // Clear instance cache other than user table data, which is already accurate
03082                 $this->clearInstanceCache();
03083 
03084                 $this->saveOptions();
03085                 return Status::newGood();
03086         }
03087 
03093         public function spreadAnyEditBlock() {
03094                 if ( $this->isLoggedIn() && $this->isBlocked() ) {
03095                         return $this->spreadBlock();
03096                 }
03097                 return false;
03098         }
03099 
03105         protected function spreadBlock() {
03106                 wfDebug( __METHOD__ . "()\n" );
03107                 $this->load();
03108                 if ( $this->mId == 0 ) {
03109                         return false;
03110                 }
03111 
03112                 $userblock = Block::newFromTarget( $this->getName() );
03113                 if ( !$userblock ) {
03114                         return false;
03115                 }
03116 
03117                 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
03118         }
03119 
03134         public function getPageRenderingHash() {
03135                 wfDeprecated( __METHOD__, '1.17' );
03136 
03137                 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
03138                 if( $this->mHash ){
03139                         return $this->mHash;
03140                 }
03141 
03142                 // stubthreshold is only included below for completeness,
03143                 // since it disables the parser cache, its value will always
03144                 // be 0 when this function is called by parsercache.
03145 
03146                 $confstr =        $this->getOption( 'math' );
03147                 $confstr .= '!' . $this->getStubThreshold();
03148                 if ( $wgUseDynamicDates ) { # This is wrong (bug 24714)
03149                         $confstr .= '!' . $this->getDatePreference();
03150                 }
03151                 $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' );
03152                 $confstr .= '!' . $wgLang->getCode();
03153                 $confstr .= '!' . $this->getOption( 'thumbsize' );
03154                 // add in language specific options, if any
03155                 $extra = $wgContLang->getExtraHashOptions();
03156                 $confstr .= $extra;
03157 
03158                 // Since the skin could be overloading link(), it should be
03159                 // included here but in practice, none of our skins do that.
03160 
03161                 $confstr .= $wgRenderHashAppend;
03162 
03163                 // Give a chance for extensions to modify the hash, if they have
03164                 // extra options or other effects on the parser cache.
03165                 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
03166 
03167                 // Make it a valid memcached key fragment
03168                 $confstr = str_replace( ' ', '_', $confstr );
03169                 $this->mHash = $confstr;
03170                 return $confstr;
03171         }
03172 
03177         public function isBlockedFromCreateAccount() {
03178                 $this->getBlockedStatus();
03179                 if( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ){
03180                         return $this->mBlock;
03181                 }
03182 
03183                 # bug 13611: if the IP address the user is trying to create an account from is
03184                 # blocked with createaccount disabled, prevent new account creation there even
03185                 # when the user is logged in
03186                 if( $this->mBlockedFromCreateAccount === false ){
03187                         $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
03188                 }
03189                 return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
03190                         ? $this->mBlockedFromCreateAccount
03191                         : false;
03192         }
03193 
03198         public function isBlockedFromEmailuser() {
03199                 $this->getBlockedStatus();
03200                 return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
03201         }
03202 
03207         function isAllowedToCreateAccount() {
03208                 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
03209         }
03210 
03216         public function getUserPage() {
03217                 return Title::makeTitle( NS_USER, $this->getName() );
03218         }
03219 
03225         public function getTalkPage() {
03226                 $title = $this->getUserPage();
03227                 return $title->getTalkPage();
03228         }
03229 
03235         public function isNewbie() {
03236                 return !$this->isAllowed( 'autoconfirmed' );
03237         }
03238 
03244         public function checkPassword( $password ) {
03245                 global $wgAuth, $wgLegacyEncoding;
03246                 $this->load();
03247 
03248                 // Even though we stop people from creating passwords that
03249                 // are shorter than this, doesn't mean people wont be able
03250                 // to. Certain authentication plugins do NOT want to save
03251                 // domain passwords in a mysql database, so we should
03252                 // check this (in case $wgAuth->strict() is false).
03253                 if( !$this->isValidPassword( $password ) ) {
03254                         return false;
03255                 }
03256 
03257                 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
03258                         return true;
03259                 } elseif( $wgAuth->strict() ) {
03260                         /* Auth plugin doesn't allow local authentication */
03261                         return false;
03262                 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
03263                         /* Auth plugin doesn't allow local authentication for this user name */
03264                         return false;
03265                 }
03266                 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
03267                         return true;
03268                 } elseif ( $wgLegacyEncoding ) {
03269                         # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
03270                         # Check for this with iconv
03271                         $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
03272                         if ( $cp1252Password != $password &&
03273                                 self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) )
03274                         {
03275                                 return true;
03276                         }
03277                 }
03278                 return false;
03279         }
03280 
03289         public function checkTemporaryPassword( $plaintext ) {
03290                 global $wgNewPasswordExpiry;
03291 
03292                 $this->load();
03293                 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
03294                         if ( is_null( $this->mNewpassTime ) ) {
03295                                 return true;
03296                         }
03297                         $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
03298                         return ( time() < $expiry );
03299                 } else {
03300                         return false;
03301                 }
03302         }
03303 
03312         public function editToken( $salt = '', $request = null ) {
03313                 wfDeprecated( __METHOD__, '1.19' );
03314                 return $this->getEditToken( $salt, $request );
03315         }
03316 
03329         public function getEditToken( $salt = '', $request = null ) {
03330                 if ( $request == null ) {
03331                         $request = $this->getRequest();
03332                 }
03333 
03334                 if ( $this->isAnon() ) {
03335                         return EDIT_TOKEN_SUFFIX;
03336                 } else {
03337                         $token = $request->getSessionData( 'wsEditToken' );
03338                         if ( $token === null ) {
03339                                 $token = MWCryptRand::generateHex( 32 );
03340                                 $request->setSessionData( 'wsEditToken', $token );
03341                         }
03342                         if( is_array( $salt ) ) {
03343                                 $salt = implode( '|', $salt );
03344                         }
03345                         return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
03346                 }
03347         }
03348 
03356         public static function generateToken( $salt = '' ) {
03357                 return MWCryptRand::generateHex( 32 );
03358         }
03359 
03371         public function matchEditToken( $val, $salt = '', $request = null ) {
03372                 $sessionToken = $this->getEditToken( $salt, $request );
03373                 if ( $val != $sessionToken ) {
03374                         wfDebug( "User::matchEditToken: broken session data\n" );
03375                 }
03376                 return $val == $sessionToken;
03377         }
03378 
03388         public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
03389                 $sessionToken = $this->getEditToken( $salt, $request );
03390                 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
03391         }
03392 
03400         public function sendConfirmationMail( $type = 'created' ) {
03401                 global $wgLang;
03402                 $expiration = null; // gets passed-by-ref and defined in next line.
03403                 $token = $this->confirmationToken( $expiration );
03404                 $url = $this->confirmationTokenUrl( $token );
03405                 $invalidateURL = $this->invalidationTokenUrl( $token );
03406                 $this->saveSettings();
03407 
03408                 if ( $type == 'created' || $type === false ) {
03409                         $message = 'confirmemail_body';
03410                 } elseif ( $type === true ) {
03411                         $message = 'confirmemail_body_changed';
03412                 } else {
03413                         $message = 'confirmemail_body_' . $type;
03414                 }
03415 
03416                 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
03417                         wfMessage( $message,
03418                                 $this->getRequest()->getIP(),
03419                                 $this->getName(),
03420                                 $url,
03421                                 $wgLang->timeanddate( $expiration, false ),
03422                                 $invalidateURL,
03423                                 $wgLang->date( $expiration, false ),
03424                                 $wgLang->time( $expiration, false ) )->text() );
03425         }
03426 
03437         public function sendMail( $subject, $body, $from = null, $replyto = null ) {
03438                 if( is_null( $from ) ) {
03439                         global $wgPasswordSender, $wgPasswordSenderName;
03440                         $sender = new MailAddress( $wgPasswordSender, $wgPasswordSenderName );
03441                 } else {
03442                         $sender = new MailAddress( $from );
03443                 }
03444 
03445                 $to = new MailAddress( $this );
03446                 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
03447         }
03448 
03459         private function confirmationToken( &$expiration ) {
03460                 global $wgUserEmailConfirmationTokenExpiry;
03461                 $now = time();
03462                 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
03463                 $expiration = wfTimestamp( TS_MW, $expires );
03464                 $this->load();
03465                 $token = MWCryptRand::generateHex( 32 );
03466                 $hash = md5( $token );
03467                 $this->mEmailToken = $hash;
03468                 $this->mEmailTokenExpires = $expiration;
03469                 return $token;
03470         }
03471 
03477         private function confirmationTokenUrl( $token ) {
03478                 return $this->getTokenUrl( 'ConfirmEmail', $token );
03479         }
03480 
03486         private function invalidationTokenUrl( $token ) {
03487                 return $this->getTokenUrl( 'InvalidateEmail', $token );
03488         }
03489 
03504         protected function getTokenUrl( $page, $token ) {
03505                 // Hack to bypass localization of 'Special:'
03506                 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
03507                 return $title->getCanonicalUrl();
03508         }
03509 
03517         public function confirmEmail() {
03518                 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
03519                 wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
03520                 return true;
03521         }
03522 
03530         function invalidateEmail() {
03531                 $this->load();
03532                 $this->mEmailToken = null;
03533                 $this->mEmailTokenExpires = null;
03534                 $this->setEmailAuthenticationTimestamp( null );
03535                 wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
03536                 return true;
03537         }
03538 
03543         function setEmailAuthenticationTimestamp( $timestamp ) {
03544                 $this->load();
03545                 $this->mEmailAuthenticated = $timestamp;
03546                 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
03547         }
03548 
03554         public function canSendEmail() {
03555                 global $wgEnableEmail, $wgEnableUserEmail;
03556                 if( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
03557                         return false;
03558                 }
03559                 $canSend = $this->isEmailConfirmed();
03560                 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
03561                 return $canSend;
03562         }
03563 
03569         public function canReceiveEmail() {
03570                 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
03571         }
03572 
03583         public function isEmailConfirmed() {
03584                 global $wgEmailAuthentication;
03585                 $this->load();
03586                 $confirmed = true;
03587                 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
03588                         if( $this->isAnon() ) {
03589                                 return false;
03590                         }
03591                         if( !Sanitizer::validateEmail( $this->mEmail ) ) {
03592                                 return false;
03593                         }
03594                         if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
03595                                 return false;
03596                         }
03597                         return true;
03598                 } else {
03599                         return $confirmed;
03600                 }
03601         }
03602 
03607         public function isEmailConfirmationPending() {
03608                 global $wgEmailAuthentication;
03609                 return $wgEmailAuthentication &&
03610                         !$this->isEmailConfirmed() &&
03611                         $this->mEmailToken &&
03612                         $this->mEmailTokenExpires > wfTimestamp();
03613         }
03614 
03621         public function getRegistration() {
03622                 if ( $this->isAnon() ) {
03623                         return false;
03624                 }
03625                 $this->load();
03626                 return $this->mRegistration;
03627         }
03628 
03635         public function getFirstEditTimestamp() {
03636                 if( $this->getId() == 0 ) {
03637                         return false; // anons
03638                 }
03639                 $dbr = wfGetDB( DB_SLAVE );
03640                 $time = $dbr->selectField( 'revision', 'rev_timestamp',
03641                         array( 'rev_user' => $this->getId() ),
03642                         __METHOD__,
03643                         array( 'ORDER BY' => 'rev_timestamp ASC' )
03644                 );
03645                 if( !$time ) {
03646                         return false; // no edits
03647                 }
03648                 return wfTimestamp( TS_MW, $time );
03649         }
03650 
03657         public static function getGroupPermissions( $groups ) {
03658                 global $wgGroupPermissions, $wgRevokePermissions;
03659                 $rights = array();
03660                 // grant every granted permission first
03661                 foreach( $groups as $group ) {
03662                         if( isset( $wgGroupPermissions[$group] ) ) {
03663                                 $rights = array_merge( $rights,
03664                                         // array_filter removes empty items
03665                                         array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
03666                         }
03667                 }
03668                 // now revoke the revoked permissions
03669                 foreach( $groups as $group ) {
03670                         if( isset( $wgRevokePermissions[$group] ) ) {
03671                                 $rights = array_diff( $rights,
03672                                         array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
03673                         }
03674                 }
03675                 return array_unique( $rights );
03676         }
03677 
03684         public static function getGroupsWithPermission( $role ) {
03685                 global $wgGroupPermissions;
03686                 $allowedGroups = array();
03687                 foreach ( array_keys( $wgGroupPermissions ) as $group ) {
03688                         if ( self::groupHasPermission( $group, $role ) ) {
03689                                 $allowedGroups[] = $group;
03690                         }
03691                 }
03692                 return $allowedGroups;
03693         }
03694 
03702         public static function groupHasPermission( $group, $role ) {
03703                 global $wgGroupPermissions, $wgRevokePermissions;
03704                 return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
03705                         && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
03706         }
03707 
03714         public static function getGroupName( $group ) {
03715                 $msg = wfMessage( "group-$group" );
03716                 return $msg->isBlank() ? $group : $msg->text();
03717         }
03718 
03726         public static function getGroupMember( $group, $username = '#' ) {
03727                 $msg = wfMessage( "group-$group-member", $username );
03728                 return $msg->isBlank() ? $group : $msg->text();
03729         }
03730 
03737         public static function getAllGroups() {
03738                 global $wgGroupPermissions, $wgRevokePermissions;
03739                 return array_diff(
03740                         array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
03741                         self::getImplicitGroups()
03742                 );
03743         }
03744 
03749         public static function getAllRights() {
03750                 if ( self::$mAllRights === false ) {
03751                         global $wgAvailableRights;
03752                         if ( count( $wgAvailableRights ) ) {
03753                                 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
03754                         } else {
03755                                 self::$mAllRights = self::$mCoreRights;
03756                         }
03757                         wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
03758                 }
03759                 return self::$mAllRights;
03760         }
03761 
03766         public static function getImplicitGroups() {
03767                 global $wgImplicitGroups;
03768                 $groups = $wgImplicitGroups;
03769                 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );       #deprecated, use $wgImplictGroups instead
03770                 return $groups;
03771         }
03772 
03779         public static function getGroupPage( $group ) {
03780                 $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
03781                 if( $msg->exists() ) {
03782                         $title = Title::newFromText( $msg->text() );
03783                         if( is_object( $title ) )
03784                                 return $title;
03785                 }
03786                 return false;
03787         }
03788 
03797         public static function makeGroupLinkHTML( $group, $text = '' ) {
03798                 if( $text == '' ) {
03799                         $text = self::getGroupName( $group );
03800                 }
03801                 $title = self::getGroupPage( $group );
03802                 if( $title ) {
03803                         return Linker::link( $title, htmlspecialchars( $text ) );
03804                 } else {
03805                         return $text;
03806                 }
03807         }
03808 
03817         public static function makeGroupLinkWiki( $group, $text = '' ) {
03818                 if( $text == '' ) {
03819                         $text = self::getGroupName( $group );
03820                 }
03821                 $title = self::getGroupPage( $group );
03822                 if( $title ) {
03823                         $page = $title->getPrefixedText();
03824                         return "[[$page|$text]]";
03825                 } else {
03826                         return $text;
03827                 }
03828         }
03829 
03839         public static function changeableByGroup( $group ) {
03840                 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
03841 
03842                 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
03843                 if( empty( $wgAddGroups[$group] ) ) {
03844                         // Don't add anything to $groups
03845                 } elseif( $wgAddGroups[$group] === true ) {
03846                         // You get everything
03847                         $groups['add'] = self::getAllGroups();
03848                 } elseif( is_array( $wgAddGroups[$group] ) ) {
03849                         $groups['add'] = $wgAddGroups[$group];
03850                 }
03851 
03852                 // Same thing for remove
03853                 if( empty( $wgRemoveGroups[$group] ) ) {
03854                 } elseif( $wgRemoveGroups[$group] === true ) {
03855                         $groups['remove'] = self::getAllGroups();
03856                 } elseif( is_array( $wgRemoveGroups[$group] ) ) {
03857                         $groups['remove'] = $wgRemoveGroups[$group];
03858                 }
03859 
03860                 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
03861                 if( empty( $wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
03862                         foreach( $wgGroupsAddToSelf as $key => $value ) {
03863                                 if( is_int( $key ) ) {
03864                                         $wgGroupsAddToSelf['user'][] = $value;
03865                                 }
03866                         }
03867                 }
03868 
03869                 if( empty( $wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
03870                         foreach( $wgGroupsRemoveFromSelf as $key => $value ) {
03871                                 if( is_int( $key ) ) {
03872                                         $wgGroupsRemoveFromSelf['user'][] = $value;
03873                                 }
03874                         }
03875                 }
03876 
03877                 // Now figure out what groups the user can add to him/herself
03878                 if( empty( $wgGroupsAddToSelf[$group] ) ) {
03879                 } elseif( $wgGroupsAddToSelf[$group] === true ) {
03880                         // No idea WHY this would be used, but it's there
03881                         $groups['add-self'] = User::getAllGroups();
03882                 } elseif( is_array( $wgGroupsAddToSelf[$group] ) ) {
03883                         $groups['add-self'] = $wgGroupsAddToSelf[$group];
03884                 }
03885 
03886                 if( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
03887                 } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
03888                         $groups['remove-self'] = User::getAllGroups();
03889                 } elseif( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
03890                         $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
03891                 }
03892 
03893                 return $groups;
03894         }
03895 
03903         public function changeableGroups() {
03904                 if( $this->isAllowed( 'userrights' ) ) {
03905                         // This group gives the right to modify everything (reverse-
03906                         // compatibility with old "userrights lets you change
03907                         // everything")
03908                         // Using array_merge to make the groups reindexed
03909                         $all = array_merge( User::getAllGroups() );
03910                         return array(
03911                                 'add' => $all,
03912                                 'remove' => $all,
03913                                 'add-self' => array(),
03914                                 'remove-self' => array()
03915                         );
03916                 }
03917 
03918                 // Okay, it's not so simple, we will have to go through the arrays
03919                 $groups = array(
03920                         'add' => array(),
03921                         'remove' => array(),
03922                         'add-self' => array(),
03923                         'remove-self' => array()
03924                 );
03925                 $addergroups = $this->getEffectiveGroups();
03926 
03927                 foreach( $addergroups as $addergroup ) {
03928                         $groups = array_merge_recursive(
03929                                 $groups, $this->changeableByGroup( $addergroup )
03930                         );
03931                         $groups['add']    = array_unique( $groups['add'] );
03932                         $groups['remove'] = array_unique( $groups['remove'] );
03933                         $groups['add-self'] = array_unique( $groups['add-self'] );
03934                         $groups['remove-self'] = array_unique( $groups['remove-self'] );
03935                 }
03936                 return $groups;
03937         }
03938 
03943         public function incEditCount() {
03944                 if( !$this->isAnon() ) {
03945                         $dbw = wfGetDB( DB_MASTER );
03946                         $dbw->update(
03947                                 'user',
03948                                 array( 'user_editcount=user_editcount+1' ),
03949                                 array( 'user_id' => $this->getId() ),
03950                                 __METHOD__
03951                         );
03952 
03953                         // Lazy initialization check...
03954                         if( $dbw->affectedRows() == 0 ) {
03955                                 // Now here's a goddamn hack...
03956                                 $dbr = wfGetDB( DB_SLAVE );
03957                                 if( $dbr !== $dbw ) {
03958                                         // If we actually have a slave server, the count is
03959                                         // at least one behind because the current transaction
03960                                         // has not been committed and replicated.
03961                                         $this->initEditCount( 1 );
03962                                 } else {
03963                                         // But if DB_SLAVE is selecting the master, then the
03964                                         // count we just read includes the revision that was
03965                                         // just added in the working transaction.
03966                                         $this->initEditCount();
03967                                 }
03968                         }
03969                 }
03970                 // edit count in user cache too
03971                 $this->invalidateCache();
03972         }
03973 
03980         protected function initEditCount( $add = 0 ) {
03981                 // Pull from a slave to be less cruel to servers
03982                 // Accuracy isn't the point anyway here
03983                 $dbr = wfGetDB( DB_SLAVE );
03984                 $count = (int) $dbr->selectField(
03985                         'revision',
03986                         'COUNT(rev_user)',
03987                         array( 'rev_user' => $this->getId() ),
03988                         __METHOD__
03989                 );
03990                 $count = $count + $add;
03991 
03992                 $dbw = wfGetDB( DB_MASTER );
03993                 $dbw->update(
03994                         'user',
03995                         array( 'user_editcount' => $count ),
03996                         array( 'user_id' => $this->getId() ),
03997                         __METHOD__
03998                 );
03999 
04000                 return $count;
04001         }
04002 
04009         public static function getRightDescription( $right ) {
04010                 $key = "right-$right";
04011                 $msg = wfMessage( $key );
04012                 return $msg->isBlank() ? $right : $msg->text();
04013         }
04014 
04022         public static function oldCrypt( $password, $userId ) {
04023                 global $wgPasswordSalt;
04024                 if ( $wgPasswordSalt ) {
04025                         return md5( $userId . '-' . md5( $password ) );
04026                 } else {
04027                         return md5( $password );
04028                 }
04029         }
04030 
04040         public static function crypt( $password, $salt = false ) {
04041                 global $wgPasswordSalt;
04042 
04043                 $hash = '';
04044                 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
04045                         return $hash;
04046                 }
04047 
04048                 if( $wgPasswordSalt ) {
04049                         if ( $salt === false ) {
04050                                 $salt = MWCryptRand::generateHex( 8 );
04051                         }
04052                         return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
04053                 } else {
04054                         return ':A:' . md5( $password );
04055                 }
04056         }
04057 
04068         public static function comparePasswords( $hash, $password, $userId = false ) {
04069                 $type = substr( $hash, 0, 3 );
04070 
04071                 $result = false;
04072                 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
04073                         return $result;
04074                 }
04075 
04076                 if ( $type == ':A:' ) {
04077                         # Unsalted
04078                         return md5( $password ) === substr( $hash, 3 );
04079                 } elseif ( $type == ':B:' ) {
04080                         # Salted
04081                         list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
04082                         return md5( $salt.'-'.md5( $password ) ) === $realHash;
04083                 } else {
04084                         # Old-style
04085                         return self::oldCrypt( $password, $userId ) === $hash;
04086                 }
04087         }
04088 
04097         public function addNewUserLogEntry( $byEmail = false, $reason = '' ) {
04098                 global $wgUser, $wgContLang, $wgNewUserLog;
04099                 if( empty( $wgNewUserLog ) ) {
04100                         return true; // disabled
04101                 }
04102 
04103                 if( $this->getName() == $wgUser->getName() ) {
04104                         $action = 'create';
04105                 } else {
04106                         $action = 'create2';
04107                         if ( $byEmail ) {
04108                                 if ( $reason === '' ) {
04109                                         $reason = wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text();
04110                                 } else {
04111                                         $reason = $wgContLang->commaList( array(
04112                                                 $reason, wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text() ) );
04113                                 }
04114                         }
04115                 }
04116                 $log = new LogPage( 'newusers' );
04117                 return (int)$log->addEntry(
04118                         $action,
04119                         $this->getUserPage(),
04120                         $reason,
04121                         array( $this->getId() )
04122                 );
04123         }
04124 
04131         public function addNewUserLogEntryAutoCreate() {
04132                 global $wgNewUserLog;
04133                 if( !$wgNewUserLog ) {
04134                         return true; // disabled
04135                 }
04136                 $log = new LogPage( 'newusers', false );
04137                 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ), $this );
04138                 return true;
04139         }
04140 
04146         protected function loadOptions( $data = null ) {
04147                 global $wgContLang;
04148 
04149                 $this->load();
04150 
04151                 if ( $this->mOptionsLoaded ) {
04152                         return;
04153                 }
04154 
04155                 $this->mOptions = self::getDefaultOptions();
04156 
04157                 if ( !$this->getId() ) {
04158                         // For unlogged-in users, load language/variant options from request.
04159                         // There's no need to do it for logged-in users: they can set preferences,
04160                         // and handling of page content is done by $pageLang->getPreferredVariant() and such,
04161                         // so don't override user's choice (especially when the user chooses site default).
04162                         $variant = $wgContLang->getDefaultVariant();
04163                         $this->mOptions['variant'] = $variant;
04164                         $this->mOptions['language'] = $variant;
04165                         $this->mOptionsLoaded = true;
04166                         return;
04167                 }
04168 
04169                 // Maybe load from the object
04170                 if ( !is_null( $this->mOptionOverrides ) ) {
04171                         wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
04172                         foreach( $this->mOptionOverrides as $key => $value ) {
04173                                 $this->mOptions[$key] = $value;
04174                         }
04175                 } else {
04176                         if( !is_array( $data ) ) {
04177                                 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
04178                                 // Load from database
04179                                 $dbr = wfGetDB( DB_SLAVE );
04180 
04181                                 $res = $dbr->select(
04182                                         'user_properties',
04183                                         array( 'up_property', 'up_value' ),
04184                                         array( 'up_user' => $this->getId() ),
04185                                         __METHOD__
04186                                 );
04187 
04188                                 $this->mOptionOverrides = array();
04189                                 $data = array();
04190                                 foreach ( $res as $row ) {
04191                                         $data[$row->up_property] = $row->up_value;
04192                                 }
04193                         }
04194                         foreach ( $data as $property => $value ) {
04195                                 $this->mOptionOverrides[$property] = $value;
04196                                 $this->mOptions[$property] = $value;
04197                         }
04198                 }
04199 
04200                 $this->mOptionsLoaded = true;
04201 
04202                 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
04203         }
04204 
04208         protected function saveOptions() {
04209                 global $wgAllowPrefChange;
04210 
04211                 $this->loadOptions();
04212 
04213                 // Not using getOptions(), to keep hidden preferences in database
04214                 $saveOptions = $this->mOptions;
04215 
04216                 // Allow hooks to abort, for instance to save to a global profile.
04217                 // Reset options to default state before saving.
04218                 if( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
04219                         return;
04220                 }
04221 
04222                 $extuser = ExternalUser::newFromUser( $this );
04223                 $userId = $this->getId();
04224                 $insert_rows = array();
04225                 foreach( $saveOptions as $key => $value ) {
04226                         # Don't bother storing default values
04227                         $defaultOption = self::getDefaultOption( $key );
04228                         if ( ( is_null( $defaultOption ) &&
04229                                         !( $value === false || is_null( $value ) ) ) ||
04230                                         $value != $defaultOption ) {
04231                                 $insert_rows[] = array(
04232                                                 'up_user' => $userId,
04233                                                 'up_property' => $key,
04234                                                 'up_value' => $value,
04235                                         );
04236                         }
04237                         if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
04238                                 switch ( $wgAllowPrefChange[$key] ) {
04239                                         case 'local':
04240                                         case 'message':
04241                                                 break;
04242                                         case 'semiglobal':
04243                                         case 'global':
04244                                                 $extuser->setPref( $key, $value );
04245                                 }
04246                         }
04247                 }
04248 
04249                 $dbw = wfGetDB( DB_MASTER );
04250                 $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
04251                 $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
04252         }
04253 
04278         public static function passwordChangeInputAttribs() {
04279                 global $wgMinimalPasswordLength;
04280 
04281                 if ( $wgMinimalPasswordLength == 0 ) {
04282                         return array();
04283                 }
04284 
04285                 # Note that the pattern requirement will always be satisfied if the
04286                 # input is empty, so we need required in all cases.
04287                 #
04288                 # @todo FIXME: Bug 23769: This needs to not claim the password is required
04289                 # if e-mail confirmation is being used.  Since HTML5 input validation
04290                 # is b0rked anyway in some browsers, just return nothing.  When it's
04291                 # re-enabled, fix this code to not output required for e-mail
04292                 # registration.
04293                 #$ret = array( 'required' );
04294                 $ret = array();
04295 
04296                 # We can't actually do this right now, because Opera 9.6 will print out
04297                 # the entered password visibly in its error message!  When other
04298                 # browsers add support for this attribute, or Opera fixes its support,
04299                 # we can add support with a version check to avoid doing this on Opera
04300                 # versions where it will be a problem.  Reported to Opera as
04301                 # DSK-262266, but they don't have a public bug tracker for us to follow.
04302                 /*
04303                 if ( $wgMinimalPasswordLength > 1 ) {
04304                         $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
04305                         $ret['title'] = wfMessage( 'passwordtooshort' )
04306                                 ->numParams( $wgMinimalPasswordLength )->text();
04307                 }
04308                 */
04309 
04310                 return $ret;
04311         }
04312 
04318         public static function selectFields() {
04319                 return array(
04320                         'user_id',
04321                         'user_name',
04322                         'user_real_name',
04323                         'user_password',
04324                         'user_newpassword',
04325                         'user_newpass_time',
04326                         'user_email',
04327                         'user_touched',
04328                         'user_token',
04329                         'user_email_authenticated',
04330                         'user_email_token',
04331                         'user_email_token_expires',
04332                         'user_registration',
04333                         'user_editcount',
04334                 );
04335         }
04336 }