MediaWiki  REL1_20
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                                 $this->loadFromSession();
00290                                 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
00291                                 break;
00292                         default:
00293                                 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
00294                 }
00295                 wfProfileOut( __METHOD__ );
00296         }
00297 
00302         public function loadFromId() {
00303                 global $wgMemc;
00304                 if ( $this->mId == 0 ) {
00305                         $this->loadDefaults();
00306                         return false;
00307                 }
00308 
00309                 # Try cache
00310                 $key = wfMemcKey( 'user', 'id', $this->mId );
00311                 $data = $wgMemc->get( $key );
00312                 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
00313                         # Object is expired, load from DB
00314                         $data = false;
00315                 }
00316 
00317                 if ( !$data ) {
00318                         wfDebug( "User: cache miss for user {$this->mId}\n" );
00319                         # Load from DB
00320                         if ( !$this->loadFromDatabase() ) {
00321                                 # Can't load from ID, user is anonymous
00322                                 return false;
00323                         }
00324                         $this->saveToCache();
00325                 } else {
00326                         wfDebug( "User: got user {$this->mId} from cache\n" );
00327                         # Restore from cache
00328                         foreach ( self::$mCacheVars as $name ) {
00329                                 $this->$name = $data[$name];
00330                         }
00331                 }
00332                 return true;
00333         }
00334 
00338         public function saveToCache() {
00339                 $this->load();
00340                 $this->loadGroups();
00341                 $this->loadOptions();
00342                 if ( $this->isAnon() ) {
00343                         // Anonymous users are uncached
00344                         return;
00345                 }
00346                 $data = array();
00347                 foreach ( self::$mCacheVars as $name ) {
00348                         $data[$name] = $this->$name;
00349                 }
00350                 $data['mVersion'] = MW_USER_VERSION;
00351                 $key = wfMemcKey( 'user', 'id', $this->mId );
00352                 global $wgMemc;
00353                 $wgMemc->set( $key, $data );
00354         }
00355 
00358 
00375         public static function newFromName( $name, $validate = 'valid' ) {
00376                 if ( $validate === true ) {
00377                         $validate = 'valid';
00378                 }
00379                 $name = self::getCanonicalName( $name, $validate );
00380                 if ( $name === false ) {
00381                         return false;
00382                 } else {
00383                         # Create unloaded user object
00384                         $u = new User;
00385                         $u->mName = $name;
00386                         $u->mFrom = 'name';
00387                         $u->setItemLoaded( 'name' );
00388                         return $u;
00389                 }
00390         }
00391 
00398         public static function newFromId( $id ) {
00399                 $u = new User;
00400                 $u->mId = $id;
00401                 $u->mFrom = 'id';
00402                 $u->setItemLoaded( 'id' );
00403                 return $u;
00404         }
00405 
00416         public static function newFromConfirmationCode( $code ) {
00417                 $dbr = wfGetDB( DB_SLAVE );
00418                 $id = $dbr->selectField( 'user', 'user_id', array(
00419                         'user_email_token' => md5( $code ),
00420                         'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
00421                         ) );
00422                 if( $id !== false ) {
00423                         return User::newFromId( $id );
00424                 } else {
00425                         return null;
00426                 }
00427         }
00428 
00437         public static function newFromSession( WebRequest $request = null ) {
00438                 $user = new User;
00439                 $user->mFrom = 'session';
00440                 $user->mRequest = $request;
00441                 return $user;
00442         }
00443 
00457         public static function newFromRow( $row ) {
00458                 $user = new User;
00459                 $user->loadFromRow( $row );
00460                 return $user;
00461         }
00462 
00464 
00470         public static function whoIs( $id ) {
00471                 return UserCache::singleton()->getProp( $id, 'name' );
00472         }
00473 
00480         public static function whoIsReal( $id ) {
00481                 return UserCache::singleton()->getProp( $id, 'real_name' );
00482         }
00483 
00489         public static function idFromName( $name ) {
00490                 $nt = Title::makeTitleSafe( NS_USER, $name );
00491                 if( is_null( $nt ) ) {
00492                         # Illegal name
00493                         return null;
00494                 }
00495 
00496                 if ( isset( self::$idCacheByName[$name] ) ) {
00497                         return self::$idCacheByName[$name];
00498                 }
00499 
00500                 $dbr = wfGetDB( DB_SLAVE );
00501                 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
00502 
00503                 if ( $s === false ) {
00504                         $result = null;
00505                 } else {
00506                         $result = $s->user_id;
00507                 }
00508 
00509                 self::$idCacheByName[$name] = $result;
00510 
00511                 if ( count( self::$idCacheByName ) > 1000 ) {
00512                         self::$idCacheByName = array();
00513                 }
00514 
00515                 return $result;
00516         }
00517 
00521         public static function resetIdByNameCache() {
00522                 self::$idCacheByName = array();
00523         }
00524 
00541         public static function isIP( $name ) {
00542                 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name);
00543         }
00544 
00556         public static function isValidUserName( $name ) {
00557                 global $wgContLang, $wgMaxNameChars;
00558 
00559                 if ( $name == ''
00560                 || User::isIP( $name )
00561                 || strpos( $name, '/' ) !== false
00562                 || strlen( $name ) > $wgMaxNameChars
00563                 || $name != $wgContLang->ucfirst( $name ) ) {
00564                         wfDebugLog( 'username', __METHOD__ .
00565                                 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
00566                         return false;
00567                 }
00568 
00569 
00570                 // Ensure that the name can't be misresolved as a different title,
00571                 // such as with extra namespace keys at the start.
00572                 $parsed = Title::newFromText( $name );
00573                 if( is_null( $parsed )
00574                         || $parsed->getNamespace()
00575                         || strcmp( $name, $parsed->getPrefixedText() ) ) {
00576                         wfDebugLog( 'username', __METHOD__ .
00577                                 ": '$name' invalid due to ambiguous prefixes" );
00578                         return false;
00579                 }
00580 
00581                 // Check an additional blacklist of troublemaker characters.
00582                 // Should these be merged into the title char list?
00583                 $unicodeBlacklist = '/[' .
00584                         '\x{0080}-\x{009f}' . # iso-8859-1 control chars
00585                         '\x{00a0}' .          # non-breaking space
00586                         '\x{2000}-\x{200f}' . # various whitespace
00587                         '\x{2028}-\x{202f}' . # breaks and control chars
00588                         '\x{3000}' .          # ideographic space
00589                         '\x{e000}-\x{f8ff}' . # private use
00590                         ']/u';
00591                 if( preg_match( $unicodeBlacklist, $name ) ) {
00592                         wfDebugLog( 'username', __METHOD__ .
00593                                 ": '$name' invalid due to blacklisted characters" );
00594                         return false;
00595                 }
00596 
00597                 return true;
00598         }
00599 
00611         public static function isUsableName( $name ) {
00612                 global $wgReservedUsernames;
00613                 // Must be a valid username, obviously ;)
00614                 if ( !self::isValidUserName( $name ) ) {
00615                         return false;
00616                 }
00617 
00618                 static $reservedUsernames = false;
00619                 if ( !$reservedUsernames ) {
00620                         $reservedUsernames = $wgReservedUsernames;
00621                         wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
00622                 }
00623 
00624                 // Certain names may be reserved for batch processes.
00625                 foreach ( $reservedUsernames as $reserved ) {
00626                         if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
00627                                 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
00628                         }
00629                         if ( $reserved == $name ) {
00630                                 return false;
00631                         }
00632                 }
00633                 return true;
00634         }
00635 
00648         public static function isCreatableName( $name ) {
00649                 global $wgInvalidUsernameCharacters;
00650 
00651                 // Ensure that the username isn't longer than 235 bytes, so that
00652                 // (at least for the builtin skins) user javascript and css files
00653                 // will work. (bug 23080)
00654                 if( strlen( $name ) > 235 ) {
00655                         wfDebugLog( 'username', __METHOD__ .
00656                                 ": '$name' invalid due to length" );
00657                         return false;
00658                 }
00659 
00660                 // Preg yells if you try to give it an empty string
00661                 if( $wgInvalidUsernameCharacters !== '' ) {
00662                         if( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
00663                                 wfDebugLog( 'username', __METHOD__ .
00664                                         ": '$name' invalid due to wgInvalidUsernameCharacters" );
00665                                 return false;
00666                         }
00667                 }
00668 
00669                 return self::isUsableName( $name );
00670         }
00671 
00678         public function isValidPassword( $password ) {
00679                 //simple boolean wrapper for getPasswordValidity
00680                 return $this->getPasswordValidity( $password ) === true;
00681         }
00682 
00689         public function getPasswordValidity( $password ) {
00690                 global $wgMinimalPasswordLength, $wgContLang;
00691 
00692                 static $blockedLogins = array(
00693                         'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589
00694                         'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605
00695                 );
00696 
00697                 $result = false; //init $result to false for the internal checks
00698 
00699                 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
00700                         return $result;
00701 
00702                 if ( $result === false ) {
00703                         if( strlen( $password ) < $wgMinimalPasswordLength ) {
00704                                 return 'passwordtooshort';
00705                         } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
00706                                 return 'password-name-match';
00707                         } elseif ( isset( $blockedLogins[ $this->getName() ] ) && $password == $blockedLogins[ $this->getName() ] ) {
00708                                 return 'password-login-forbidden';
00709                         } else {
00710                                 //it seems weird returning true here, but this is because of the
00711                                 //initialization of $result to false above. If the hook is never run or it
00712                                 //doesn't modify $result, then we will likely get down into this if with
00713                                 //a valid password.
00714                                 return true;
00715                         }
00716                 } elseif( $result === true ) {
00717                         return true;
00718                 } else {
00719                         return $result; //the isValidPassword hook set a string $result and returned true
00720                 }
00721         }
00722 
00750         public static function isValidEmailAddr( $addr ) {
00751                 wfDeprecated( __METHOD__, '1.18' );
00752                 return Sanitizer::validateEmail( $addr );
00753         }
00754 
00767         public static function getCanonicalName( $name, $validate = 'valid' ) {
00768                 # Force usernames to capital
00769                 global $wgContLang;
00770                 $name = $wgContLang->ucfirst( $name );
00771 
00772                 # Reject names containing '#'; these will be cleaned up
00773                 # with title normalisation, but then it's too late to
00774                 # check elsewhere
00775                 if( strpos( $name, '#' ) !== false )
00776                         return false;
00777 
00778                 # Clean up name according to title rules
00779                 $t = ( $validate === 'valid' ) ?
00780                         Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
00781                 # Check for invalid titles
00782                 if( is_null( $t ) ) {
00783                         return false;
00784                 }
00785 
00786                 # Reject various classes of invalid names
00787                 global $wgAuth;
00788                 $name = $wgAuth->getCanonicalName( $t->getText() );
00789 
00790                 switch ( $validate ) {
00791                         case false:
00792                                 break;
00793                         case 'valid':
00794                                 if ( !User::isValidUserName( $name ) ) {
00795                                         $name = false;
00796                                 }
00797                                 break;
00798                         case 'usable':
00799                                 if ( !User::isUsableName( $name ) ) {
00800                                         $name = false;
00801                                 }
00802                                 break;
00803                         case 'creatable':
00804                                 if ( !User::isCreatableName( $name ) ) {
00805                                         $name = false;
00806                                 }
00807                                 break;
00808                         default:
00809                                 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
00810                 }
00811                 return $name;
00812         }
00813 
00821         public static function edits( $uid ) {
00822                 wfProfileIn( __METHOD__ );
00823                 $dbr = wfGetDB( DB_SLAVE );
00824                 // check if the user_editcount field has been initialized
00825                 $field = $dbr->selectField(
00826                         'user', 'user_editcount',
00827                         array( 'user_id' => $uid ),
00828                         __METHOD__
00829                 );
00830 
00831                 if( $field === null ) { // it has not been initialized. do so.
00832                         $dbw = wfGetDB( DB_MASTER );
00833                         $count = $dbr->selectField(
00834                                 'revision', 'count(*)',
00835                                 array( 'rev_user' => $uid ),
00836                                 __METHOD__
00837                         );
00838                         $dbw->update(
00839                                 'user',
00840                                 array( 'user_editcount' => $count ),
00841                                 array( 'user_id' => $uid ),
00842                                 __METHOD__
00843                         );
00844                 } else {
00845                         $count = $field;
00846                 }
00847                 wfProfileOut( __METHOD__ );
00848                 return $count;
00849         }
00850 
00856         public static function randomPassword() {
00857                 global $wgMinimalPasswordLength;
00858                 // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
00859                 $length = max( 10, $wgMinimalPasswordLength );
00860                 // Multiply by 1.25 to get the number of hex characters we need
00861                 $length = $length * 1.25;
00862                 // Generate random hex chars
00863                 $hex = MWCryptRand::generateHex( $length );
00864                 // Convert from base 16 to base 32 to get a proper password like string
00865                 return wfBaseConvert( $hex, 16, 32 );
00866         }
00867 
00876         public function loadDefaults( $name = false ) {
00877                 wfProfileIn( __METHOD__ );
00878 
00879                 $this->mId = 0;
00880                 $this->mName = $name;
00881                 $this->mRealName = '';
00882                 $this->mPassword = $this->mNewpassword = '';
00883                 $this->mNewpassTime = null;
00884                 $this->mEmail = '';
00885                 $this->mOptionOverrides = null;
00886                 $this->mOptionsLoaded = false;
00887 
00888                 $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
00889                 if( $loggedOut !== null ) {
00890                         $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
00891                 } else {
00892                         $this->mTouched = '0'; # Allow any pages to be cached
00893                 }
00894 
00895                 $this->mToken = null; // Don't run cryptographic functions till we need a token
00896                 $this->mEmailAuthenticated = null;
00897                 $this->mEmailToken = '';
00898                 $this->mEmailTokenExpires = null;
00899                 $this->mRegistration = wfTimestamp( TS_MW );
00900                 $this->mGroups = array();
00901 
00902                 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
00903 
00904                 wfProfileOut( __METHOD__ );
00905         }
00906 
00919         public function isItemLoaded( $item, $all = 'all' ) {
00920                 return ( $this->mLoadedItems === true && $all === 'all' ) ||
00921                         ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
00922         }
00923 
00929         private function setItemLoaded( $item ) {
00930                 if ( is_array( $this->mLoadedItems ) ) {
00931                         $this->mLoadedItems[$item] = true;
00932                 }
00933         }
00934 
00940         private function loadFromSession() {
00941                 global $wgExternalAuthType, $wgAutocreatePolicy;
00942 
00943                 $result = null;
00944                 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
00945                 if ( $result !== null ) {
00946                         return $result;
00947                 }
00948 
00949                 if ( $wgExternalAuthType && $wgAutocreatePolicy == 'view' ) {
00950                         $extUser = ExternalUser::newFromCookie();
00951                         if ( $extUser ) {
00952                                 # TODO: Automatically create the user here (or probably a bit
00953                                 # lower down, in fact)
00954                         }
00955                 }
00956 
00957                 $request = $this->getRequest();
00958 
00959                 $cookieId = $request->getCookie( 'UserID' );
00960                 $sessId = $request->getSessionData( 'wsUserID' );
00961 
00962                 if ( $cookieId !== null ) {
00963                         $sId = intval( $cookieId );
00964                         if( $sessId !== null && $cookieId != $sessId ) {
00965                                 $this->loadDefaults(); // Possible collision!
00966                                 wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
00967                                         cookie user ID ($sId) don't match!" );
00968                                 return false;
00969                         }
00970                         $request->setSessionData( 'wsUserID', $sId );
00971                 } elseif ( $sessId !== null && $sessId != 0 ) {
00972                         $sId = $sessId;
00973                 } else {
00974                         $this->loadDefaults();
00975                         return false;
00976                 }
00977 
00978                 if ( $request->getSessionData( 'wsUserName' ) !== null ) {
00979                         $sName = $request->getSessionData( 'wsUserName' );
00980                 } elseif ( $request->getCookie( 'UserName' ) !== null ) {
00981                         $sName = $request->getCookie( 'UserName' );
00982                         $request->setSessionData( 'wsUserName', $sName );
00983                 } else {
00984                         $this->loadDefaults();
00985                         return false;
00986                 }
00987 
00988                 $proposedUser = User::newFromId( $sId );
00989                 if ( !$proposedUser->isLoggedIn() ) {
00990                         # Not a valid ID
00991                         $this->loadDefaults();
00992                         return false;
00993                 }
00994 
00995                 global $wgBlockDisablesLogin;
00996                 if( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
00997                         # User blocked and we've disabled blocked user logins
00998                         $this->loadDefaults();
00999                         return false;
01000                 }
01001 
01002                 if ( $request->getSessionData( 'wsToken' ) ) {
01003                         $passwordCorrect = $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' );
01004                         $from = 'session';
01005                 } elseif ( $request->getCookie( 'Token' ) ) {
01006                         $passwordCorrect = $proposedUser->getToken( false ) === $request->getCookie( 'Token' );
01007                         $from = 'cookie';
01008                 } else {
01009                         # No session or persistent login cookie
01010                         $this->loadDefaults();
01011                         return false;
01012                 }
01013 
01014                 if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
01015                         $this->loadFromUserObject( $proposedUser );
01016                         $request->setSessionData( 'wsToken', $this->mToken );
01017                         wfDebug( "User: logged in from $from\n" );
01018                         return true;
01019                 } else {
01020                         # Invalid credentials
01021                         wfDebug( "User: can't log in from $from, invalid credentials\n" );
01022                         $this->loadDefaults();
01023                         return false;
01024                 }
01025         }
01026 
01033         public function loadFromDatabase() {
01034                 # Paranoia
01035                 $this->mId = intval( $this->mId );
01036 
01038                 if( !$this->mId ) {
01039                         $this->loadDefaults();
01040                         return false;
01041                 }
01042 
01043                 $dbr = wfGetDB( DB_MASTER );
01044                 $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ );
01045 
01046                 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
01047 
01048                 if ( $s !== false ) {
01049                         # Initialise user table data
01050                         $this->loadFromRow( $s );
01051                         $this->mGroups = null; // deferred
01052                         $this->getEditCount(); // revalidation for nulls
01053                         return true;
01054                 } else {
01055                         # Invalid user_id
01056                         $this->mId = 0;
01057                         $this->loadDefaults();
01058                         return false;
01059                 }
01060         }
01061 
01067         public function loadFromRow( $row ) {
01068                 $all = true;
01069 
01070                 $this->mGroups = null; // deferred
01071 
01072                 if ( isset( $row->user_name ) ) {
01073                         $this->mName = $row->user_name;
01074                         $this->mFrom = 'name';
01075                         $this->setItemLoaded( 'name' );
01076                 } else {
01077                         $all = false;
01078                 }
01079 
01080                 if ( isset( $row->user_real_name ) ) {
01081                         $this->mRealName = $row->user_real_name;
01082                         $this->setItemLoaded( 'realname' );
01083                 } else {
01084                         $all = false;
01085                 }
01086 
01087                 if ( isset( $row->user_id ) ) {
01088                         $this->mId = intval( $row->user_id );
01089                         $this->mFrom = 'id';
01090                         $this->setItemLoaded( 'id' );
01091                 } else {
01092                         $all = false;
01093                 }
01094 
01095                 if ( isset( $row->user_editcount ) ) {
01096                         $this->mEditCount = $row->user_editcount;
01097                 } else {
01098                         $all = false;
01099                 }
01100 
01101                 if ( isset( $row->user_password ) ) {
01102                         $this->mPassword = $row->user_password;
01103                         $this->mNewpassword = $row->user_newpassword;
01104                         $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
01105                         $this->mEmail = $row->user_email;
01106                         if ( isset( $row->user_options ) ) {
01107                                 $this->decodeOptions( $row->user_options );
01108                         }
01109                         $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
01110                         $this->mToken = $row->user_token;
01111                         if ( $this->mToken == '' ) {
01112                                 $this->mToken = null;
01113                         }
01114                         $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
01115                         $this->mEmailToken = $row->user_email_token;
01116                         $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
01117                         $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
01118                 } else {
01119                         $all = false;
01120                 }
01121 
01122                 if ( $all ) {
01123                         $this->mLoadedItems = true;
01124                 }
01125         }
01126 
01132         protected function loadFromUserObject( $user ) {
01133                 $user->load();
01134                 $user->loadGroups();
01135                 $user->loadOptions();
01136                 foreach ( self::$mCacheVars as $var ) {
01137                         $this->$var = $user->$var;
01138                 }
01139         }
01140 
01144         private function loadGroups() {
01145                 if ( is_null( $this->mGroups ) ) {
01146                         $dbr = wfGetDB( DB_MASTER );
01147                         $res = $dbr->select( 'user_groups',
01148                                 array( 'ug_group' ),
01149                                 array( 'ug_user' => $this->mId ),
01150                                 __METHOD__ );
01151                         $this->mGroups = array();
01152                         foreach ( $res as $row ) {
01153                                 $this->mGroups[] = $row->ug_group;
01154                         }
01155                 }
01156         }
01157 
01172         public function addAutopromoteOnceGroups( $event ) {
01173                 global $wgAutopromoteOnceLogInRC;
01174 
01175                 $toPromote = array();
01176                 if ( $this->getId() ) {
01177                         $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
01178                         if ( count( $toPromote ) ) {
01179                                 $oldGroups = $this->getGroups(); // previous groups
01180                                 foreach ( $toPromote as $group ) {
01181                                         $this->addGroup( $group );
01182                                 }
01183                                 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
01184 
01185                                 $log = new LogPage( 'rights', $wgAutopromoteOnceLogInRC /* in RC? */ );
01186                                 $log->addEntry( 'autopromote',
01187                                         $this->getUserPage(),
01188                                         '', // no comment
01189                                         // These group names are "list to texted"-ed in class LogPage.
01190                                         array( implode( ', ', $oldGroups ), implode( ', ', $newGroups ) )
01191                                 );
01192                         }
01193                 }
01194                 return $toPromote;
01195         }
01196 
01203         public function clearInstanceCache( $reloadFrom = false ) {
01204                 $this->mNewtalk = -1;
01205                 $this->mDatePreference = null;
01206                 $this->mBlockedby = -1; # Unset
01207                 $this->mHash = false;
01208                 $this->mRights = null;
01209                 $this->mEffectiveGroups = null;
01210                 $this->mImplicitGroups = null;
01211                 $this->mOptions = null;
01212 
01213                 if ( $reloadFrom ) {
01214                         $this->mLoadedItems = array();
01215                         $this->mFrom = $reloadFrom;
01216                 }
01217         }
01218 
01225         public static function getDefaultOptions() {
01226                 global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
01227 
01228                 $defOpt = $wgDefaultUserOptions;
01229                 # default language setting
01230                 $variant = $wgContLang->getDefaultVariant();
01231                 $defOpt['variant'] = $variant;
01232                 $defOpt['language'] = $variant;
01233                 foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
01234                         $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
01235                 }
01236                 $defOpt['skin'] = $wgDefaultSkin;
01237 
01238                 // FIXME: Ideally we'd cache the results of this function so the hook is only run once,
01239                 // but that breaks the parser tests because they rely on being able to change $wgContLang
01240                 // mid-request and see that change reflected in the return value of this function.
01241                 // Which is insane and would never happen during normal MW operation, but is also not
01242                 // likely to get fixed unless and until we context-ify everything.
01243                 // See also https://www.mediawiki.org/wiki/Special:Code/MediaWiki/101488#c25275
01244                 wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) );
01245 
01246                 return $defOpt;
01247         }
01248 
01255         public static function getDefaultOption( $opt ) {
01256                 $defOpts = self::getDefaultOptions();
01257                 if( isset( $defOpts[$opt] ) ) {
01258                         return $defOpts[$opt];
01259                 } else {
01260                         return null;
01261                 }
01262         }
01263 
01264 
01272         private function getBlockedStatus( $bFromSlave = true ) {
01273                 global $wgProxyWhitelist, $wgUser;
01274 
01275                 if ( -1 != $this->mBlockedby ) {
01276                         return;
01277                 }
01278 
01279                 wfProfileIn( __METHOD__ );
01280                 wfDebug( __METHOD__.": checking...\n" );
01281 
01282                 // Initialize data...
01283                 // Otherwise something ends up stomping on $this->mBlockedby when
01284                 // things get lazy-loaded later, causing false positive block hits
01285                 // due to -1 !== 0. Probably session-related... Nothing should be
01286                 // overwriting mBlockedby, surely?
01287                 $this->load();
01288 
01289                 # We only need to worry about passing the IP address to the Block generator if the
01290                 # user is not immune to autoblocks/hardblocks, and they are the current user so we
01291                 # know which IP address they're actually coming from
01292                 if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) {
01293                         $ip = $this->getRequest()->getIP();
01294                 } else {
01295                         $ip = null;
01296                 }
01297 
01298                 # User/IP blocking
01299                 $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
01300 
01301                 # Proxy blocking
01302                 if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
01303                         && !in_array( $ip, $wgProxyWhitelist ) )
01304                 {
01305                         # Local list
01306                         if ( self::isLocallyBlockedProxy( $ip ) ) {
01307                                 $block = new Block;
01308                                 $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
01309                                 $block->mReason = wfMessage( 'proxyblockreason' )->text();
01310                                 $block->setTarget( $ip );
01311                         } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
01312                                 $block = new Block;
01313                                 $block->setBlocker( wfMessage( 'sorbs' )->text() );
01314                                 $block->mReason = wfMessage( 'sorbsreason' )->text();
01315                                 $block->setTarget( $ip );
01316                         }
01317                 }
01318 
01319                 if ( $block instanceof Block ) {
01320                         wfDebug( __METHOD__ . ": Found block.\n" );
01321                         $this->mBlock = $block;
01322                         $this->mBlockedby = $block->getByName();
01323                         $this->mBlockreason = $block->mReason;
01324                         $this->mHideName = $block->mHideName;
01325                         $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
01326                 } else {
01327                         $this->mBlockedby = '';
01328                         $this->mHideName = 0;
01329                         $this->mAllowUsertalk = false;
01330                 }
01331 
01332                 # Extensions
01333                 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
01334 
01335                 wfProfileOut( __METHOD__ );
01336         }
01337 
01345         public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
01346                 global $wgEnableSorbs, $wgEnableDnsBlacklist,
01347                         $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
01348 
01349                 if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs )
01350                         return false;
01351 
01352                 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
01353                         return false;
01354 
01355                 $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
01356                 return $this->inDnsBlacklist( $ip, $urls );
01357         }
01358 
01366         public function inDnsBlacklist( $ip, $bases ) {
01367                 wfProfileIn( __METHOD__ );
01368 
01369                 $found = false;
01370                 // @todo FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
01371                 if( IP::isIPv4( $ip ) ) {
01372                         # Reverse IP, bug 21255
01373                         $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
01374 
01375                         foreach( (array)$bases as $base ) {
01376                                 # Make hostname
01377                                 # If we have an access key, use that too (ProjectHoneypot, etc.)
01378                                 if( is_array( $base ) ) {
01379                                         if( count( $base ) >= 2 ) {
01380                                                 # Access key is 1, base URL is 0
01381                                                 $host = "{$base[1]}.$ipReversed.{$base[0]}";
01382                                         } else {
01383                                                 $host = "$ipReversed.{$base[0]}";
01384                                         }
01385                                 } else {
01386                                         $host = "$ipReversed.$base";
01387                                 }
01388 
01389                                 # Send query
01390                                 $ipList = gethostbynamel( $host );
01391 
01392                                 if( $ipList ) {
01393                                         wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
01394                                         $found = true;
01395                                         break;
01396                                 } else {
01397                                         wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base.\n" );
01398                                 }
01399                         }
01400                 }
01401 
01402                 wfProfileOut( __METHOD__ );
01403                 return $found;
01404         }
01405 
01413         public static function isLocallyBlockedProxy( $ip ) {
01414                 global $wgProxyList;
01415 
01416                 if ( !$wgProxyList ) {
01417                         return false;
01418                 }
01419                 wfProfileIn( __METHOD__ );
01420 
01421                 if ( !is_array( $wgProxyList ) ) {
01422                         # Load from the specified file
01423                         $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
01424                 }
01425 
01426                 if ( !is_array( $wgProxyList ) ) {
01427                         $ret = false;
01428                 } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
01429                         $ret = true;
01430                 } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
01431                         # Old-style flipped proxy list
01432                         $ret = true;
01433                 } else {
01434                         $ret = false;
01435                 }
01436                 wfProfileOut( __METHOD__ );
01437                 return $ret;
01438         }
01439 
01445         public function isPingLimitable() {
01446                 global $wgRateLimitsExcludedIPs;
01447                 if( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
01448                         // No other good way currently to disable rate limits
01449                         // for specific IPs. :P
01450                         // But this is a crappy hack and should die.
01451                         return false;
01452                 }
01453                 return !$this->isAllowed('noratelimit');
01454         }
01455 
01466         public function pingLimiter( $action = 'edit' ) {
01467                 # Call the 'PingLimiter' hook
01468                 $result = false;
01469                 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
01470                         return $result;
01471                 }
01472 
01473                 global $wgRateLimits;
01474                 if( !isset( $wgRateLimits[$action] ) ) {
01475                         return false;
01476                 }
01477 
01478                 # Some groups shouldn't trigger the ping limiter, ever
01479                 if( !$this->isPingLimitable() )
01480                         return false;
01481 
01482                 global $wgMemc, $wgRateLimitLog;
01483                 wfProfileIn( __METHOD__ );
01484 
01485                 $limits = $wgRateLimits[$action];
01486                 $keys = array();
01487                 $id = $this->getId();
01488                 $ip = $this->getRequest()->getIP();
01489                 $userLimit = false;
01490 
01491                 if( isset( $limits['anon'] ) && $id == 0 ) {
01492                         $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
01493                 }
01494 
01495                 if( isset( $limits['user'] ) && $id != 0 ) {
01496                         $userLimit = $limits['user'];
01497                 }
01498                 if( $this->isNewbie() ) {
01499                         if( isset( $limits['newbie'] ) && $id != 0 ) {
01500                                 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
01501                         }
01502                         if( isset( $limits['ip'] ) ) {
01503                                 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
01504                         }
01505                         $matches = array();
01506                         if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
01507                                 $subnet = $matches[1];
01508                                 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
01509                         }
01510                 }
01511                 // Check for group-specific permissions
01512                 // If more than one group applies, use the group with the highest limit
01513                 foreach ( $this->getGroups() as $group ) {
01514                         if ( isset( $limits[$group] ) ) {
01515                                 if ( $userLimit === false || $limits[$group] > $userLimit ) {
01516                                         $userLimit = $limits[$group];
01517                                 }
01518                         }
01519                 }
01520                 // Set the user limit key
01521                 if ( $userLimit !== false ) {
01522                         wfDebug( __METHOD__ . ": effective user limit: $userLimit\n" );
01523                         $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
01524                 }
01525 
01526                 $triggered = false;
01527                 foreach( $keys as $key => $limit ) {
01528                         list( $max, $period ) = $limit;
01529                         $summary = "(limit $max in {$period}s)";
01530                         $count = $wgMemc->get( $key );
01531                         // Already pinged?
01532                         if( $count ) {
01533                                 if( $count >= $max ) {
01534                                         wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
01535                                         if( $wgRateLimitLog ) {
01536                                                 wfSuppressWarnings();
01537                                                 file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND );
01538                                                 wfRestoreWarnings();
01539                                         }
01540                                         $triggered = true;
01541                                 } else {
01542                                         wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
01543                                 }
01544                         } else {
01545                                 wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
01546                                 $wgMemc->add( $key, 0, intval( $period ) ); // first ping
01547                         }
01548                         $wgMemc->incr( $key );
01549                 }
01550 
01551                 wfProfileOut( __METHOD__ );
01552                 return $triggered;
01553         }
01554 
01561         public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
01562                 return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
01563         }
01564 
01571         public function getBlock( $bFromSlave = true ){
01572                 $this->getBlockedStatus( $bFromSlave );
01573                 return $this->mBlock instanceof Block ? $this->mBlock : null;
01574         }
01575 
01583         function isBlockedFrom( $title, $bFromSlave = false ) {
01584                 global $wgBlockAllowsUTEdit;
01585                 wfProfileIn( __METHOD__ );
01586 
01587                 $blocked = $this->isBlocked( $bFromSlave );
01588                 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
01589                 # If a user's name is suppressed, they cannot make edits anywhere
01590                 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
01591                   $title->getNamespace() == NS_USER_TALK ) {
01592                         $blocked = false;
01593                         wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
01594                 }
01595 
01596                 wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
01597 
01598                 wfProfileOut( __METHOD__ );
01599                 return $blocked;
01600         }
01601 
01606         public function blockedBy() {
01607                 $this->getBlockedStatus();
01608                 return $this->mBlockedby;
01609         }
01610 
01615         public function blockedFor() {
01616                 $this->getBlockedStatus();
01617                 return $this->mBlockreason;
01618         }
01619 
01624         public function getBlockId() {
01625                 $this->getBlockedStatus();
01626                 return ( $this->mBlock ? $this->mBlock->getId() : false );
01627         }
01628 
01637         public function isBlockedGlobally( $ip = '' ) {
01638                 if( $this->mBlockedGlobally !== null ) {
01639                         return $this->mBlockedGlobally;
01640                 }
01641                 // User is already an IP?
01642                 if( IP::isIPAddress( $this->getName() ) ) {
01643                         $ip = $this->getName();
01644                 } elseif( !$ip ) {
01645                         $ip = $this->getRequest()->getIP();
01646                 }
01647                 $blocked = false;
01648                 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
01649                 $this->mBlockedGlobally = (bool)$blocked;
01650                 return $this->mBlockedGlobally;
01651         }
01652 
01658         public function isLocked() {
01659                 if( $this->mLocked !== null ) {
01660                         return $this->mLocked;
01661                 }
01662                 global $wgAuth;
01663                 $authUser = $wgAuth->getUserInstance( $this );
01664                 $this->mLocked = (bool)$authUser->isLocked();
01665                 return $this->mLocked;
01666         }
01667 
01673         public function isHidden() {
01674                 if( $this->mHideName !== null ) {
01675                         return $this->mHideName;
01676                 }
01677                 $this->getBlockedStatus();
01678                 if( !$this->mHideName ) {
01679                         global $wgAuth;
01680                         $authUser = $wgAuth->getUserInstance( $this );
01681                         $this->mHideName = (bool)$authUser->isHidden();
01682                 }
01683                 return $this->mHideName;
01684         }
01685 
01690         public function getId() {
01691                 if( $this->mId === null && $this->mName !== null
01692                 && User::isIP( $this->mName ) ) {
01693                         // Special case, we know the user is anonymous
01694                         return 0;
01695                 } elseif( !$this->isItemLoaded( 'id' ) ) {
01696                         // Don't load if this was initialized from an ID
01697                         $this->load();
01698                 }
01699                 return $this->mId;
01700         }
01701 
01706         public function setId( $v ) {
01707                 $this->mId = $v;
01708                 $this->clearInstanceCache( 'id' );
01709         }
01710 
01715         public function getName() {
01716                 if ( $this->isItemLoaded( 'name', 'only' ) ) {
01717                         # Special case optimisation
01718                         return $this->mName;
01719                 } else {
01720                         $this->load();
01721                         if ( $this->mName === false ) {
01722                                 # Clean up IPs
01723                                 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
01724                         }
01725                         return $this->mName;
01726                 }
01727         }
01728 
01742         public function setName( $str ) {
01743                 $this->load();
01744                 $this->mName = $str;
01745         }
01746 
01751         public function getTitleKey() {
01752                 return str_replace( ' ', '_', $this->getName() );
01753         }
01754 
01759         public function getNewtalk() {
01760                 $this->load();
01761 
01762                 # Load the newtalk status if it is unloaded (mNewtalk=-1)
01763                 if( $this->mNewtalk === -1 ) {
01764                         $this->mNewtalk = false; # reset talk page status
01765 
01766                         # Check memcached separately for anons, who have no
01767                         # entire User object stored in there.
01768                         if( !$this->mId ) {
01769                                 global $wgDisableAnonTalk;
01770                                 if( $wgDisableAnonTalk ) {
01771                                         // Anon newtalk disabled by configuration.
01772                                         $this->mNewtalk = false;
01773                                 } else {
01774                                         global $wgMemc;
01775                                         $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
01776                                         $newtalk = $wgMemc->get( $key );
01777                                         if( strval( $newtalk ) !== '' ) {
01778                                                 $this->mNewtalk = (bool)$newtalk;
01779                                         } else {
01780                                                 // Since we are caching this, make sure it is up to date by getting it
01781                                                 // from the master
01782                                                 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
01783                                                 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
01784                                         }
01785                                 }
01786                         } else {
01787                                 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
01788                         }
01789                 }
01790 
01791                 return (bool)$this->mNewtalk;
01792         }
01793 
01798         public function getNewMessageLinks() {
01799                 $talks = array();
01800                 if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
01801                         return $talks;
01802                 } elseif( !$this->getNewtalk() ) {
01803                         return array();
01804                 }
01805                 $utp = $this->getTalkPage();
01806                 $dbr = wfGetDB( DB_SLAVE );
01807                 // Get the "last viewed rev" timestamp from the oldest message notification
01808                 $timestamp = $dbr->selectField( 'user_newtalk',
01809                         'MIN(user_last_timestamp)',
01810                         $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
01811                         __METHOD__ );
01812                 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
01813                 return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
01814         }
01815 
01825         protected function checkNewtalk( $field, $id, $fromMaster = false ) {
01826                 if ( $fromMaster ) {
01827                         $db = wfGetDB( DB_MASTER );
01828                 } else {
01829                         $db = wfGetDB( DB_SLAVE );
01830                 }
01831                 $ok = $db->selectField( 'user_newtalk', $field,
01832                         array( $field => $id ), __METHOD__ );
01833                 return $ok !== false;
01834         }
01835 
01843         protected function updateNewtalk( $field, $id, $curRev = null ) {
01844                 // Get timestamp of the talk page revision prior to the current one
01845                 $prevRev = $curRev ? $curRev->getPrevious() : false;
01846                 $ts = $prevRev ? $prevRev->getTimestamp() : null;
01847                 // Mark the user as having new messages since this revision
01848                 $dbw = wfGetDB( DB_MASTER );
01849                 $dbw->insert( 'user_newtalk',
01850                         array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
01851                         __METHOD__,
01852                         'IGNORE' );
01853                 if ( $dbw->affectedRows() ) {
01854                         wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
01855                         return true;
01856                 } else {
01857                         wfDebug( __METHOD__ . " already set ($field, $id)\n" );
01858                         return false;
01859                 }
01860         }
01861 
01868         protected function deleteNewtalk( $field, $id ) {
01869                 $dbw = wfGetDB( DB_MASTER );
01870                 $dbw->delete( 'user_newtalk',
01871                         array( $field => $id ),
01872                         __METHOD__ );
01873                 if ( $dbw->affectedRows() ) {
01874                         wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
01875                         return true;
01876                 } else {
01877                         wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
01878                         return false;
01879                 }
01880         }
01881 
01887         public function setNewtalk( $val, $curRev = null ) {
01888                 if( wfReadOnly() ) {
01889                         return;
01890                 }
01891 
01892                 $this->load();
01893                 $this->mNewtalk = $val;
01894 
01895                 if( $this->isAnon() ) {
01896                         $field = 'user_ip';
01897                         $id = $this->getName();
01898                 } else {
01899                         $field = 'user_id';
01900                         $id = $this->getId();
01901                 }
01902                 global $wgMemc;
01903 
01904                 if( $val ) {
01905                         $changed = $this->updateNewtalk( $field, $id, $curRev );
01906                 } else {
01907                         $changed = $this->deleteNewtalk( $field, $id );
01908                 }
01909 
01910                 if( $this->isAnon() ) {
01911                         // Anons have a separate memcached space, since
01912                         // user records aren't kept for them.
01913                         $key = wfMemcKey( 'newtalk', 'ip', $id );
01914                         $wgMemc->set( $key, $val ? 1 : 0, 1800 );
01915                 }
01916                 if ( $changed ) {
01917                         $this->invalidateCache();
01918                 }
01919         }
01920 
01926         private static function newTouchedTimestamp() {
01927                 global $wgClockSkewFudge;
01928                 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
01929         }
01930 
01938         private function clearSharedCache() {
01939                 $this->load();
01940                 if( $this->mId ) {
01941                         global $wgMemc;
01942                         $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
01943                 }
01944         }
01945 
01951         public function invalidateCache() {
01952                 if( wfReadOnly() ) {
01953                         return;
01954                 }
01955                 $this->load();
01956                 if( $this->mId ) {
01957                         $this->mTouched = self::newTouchedTimestamp();
01958 
01959                         $dbw = wfGetDB( DB_MASTER );
01960 
01961                         // Prevent contention slams by checking user_touched first
01962                         $now = $dbw->timestamp( $this->mTouched );
01963                         $needsPurge = $dbw->selectField( 'user', '1',
01964                                 array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) )
01965                         );
01966                         if ( $needsPurge ) {
01967                                 $dbw->update( 'user',
01968                                         array( 'user_touched' => $now ),
01969                                         array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) ),
01970                                         __METHOD__
01971                                 );
01972                         }
01973 
01974                         $this->clearSharedCache();
01975                 }
01976         }
01977 
01984         public function validateCache( $timestamp ) {
01985                 $this->load();
01986                 return ( $timestamp >= $this->mTouched );
01987         }
01988 
01993         public function getTouched() {
01994                 $this->load();
01995                 return $this->mTouched;
01996         }
01997 
02014         public function setPassword( $str ) {
02015                 global $wgAuth;
02016 
02017                 if( $str !== null ) {
02018                         if( !$wgAuth->allowPasswordChange() ) {
02019                                 throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
02020                         }
02021 
02022                         if( !$this->isValidPassword( $str ) ) {
02023                                 global $wgMinimalPasswordLength;
02024                                 $valid = $this->getPasswordValidity( $str );
02025                                 if ( is_array( $valid ) ) {
02026                                         $message = array_shift( $valid );
02027                                         $params = $valid;
02028                                 } else {
02029                                         $message = $valid;
02030                                         $params = array( $wgMinimalPasswordLength );
02031                                 }
02032                                 throw new PasswordError( wfMessage( $message, $params )->text() );
02033                         }
02034                 }
02035 
02036                 if( !$wgAuth->setPassword( $this, $str ) ) {
02037                         throw new PasswordError( wfMessage( 'externaldberror' )->text() );
02038                 }
02039 
02040                 $this->setInternalPassword( $str );
02041 
02042                 return true;
02043         }
02044 
02050         public function setInternalPassword( $str ) {
02051                 $this->load();
02052                 $this->setToken();
02053 
02054                 if( $str === null ) {
02055                         // Save an invalid hash...
02056                         $this->mPassword = '';
02057                 } else {
02058                         $this->mPassword = self::crypt( $str );
02059                 }
02060                 $this->mNewpassword = '';
02061                 $this->mNewpassTime = null;
02062         }
02063 
02069         public function getToken( $forceCreation = true ) {
02070                 $this->load();
02071                 if ( !$this->mToken && $forceCreation ) {
02072                         $this->setToken();
02073                 }
02074                 return $this->mToken;
02075         }
02076 
02083         public function setToken( $token = false ) {
02084                 $this->load();
02085                 if ( !$token ) {
02086                         $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH );
02087                 } else {
02088                         $this->mToken = $token;
02089                 }
02090         }
02091 
02098         public function setNewpassword( $str, $throttle = true ) {
02099                 $this->load();
02100                 $this->mNewpassword = self::crypt( $str );
02101                 if ( $throttle ) {
02102                         $this->mNewpassTime = wfTimestampNow();
02103                 }
02104         }
02105 
02111         public function isPasswordReminderThrottled() {
02112                 global $wgPasswordReminderResendTime;
02113                 $this->load();
02114                 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
02115                         return false;
02116                 }
02117                 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
02118                 return time() < $expiry;
02119         }
02120 
02125         public function getEmail() {
02126                 $this->load();
02127                 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
02128                 return $this->mEmail;
02129         }
02130 
02135         public function getEmailAuthenticationTimestamp() {
02136                 $this->load();
02137                 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
02138                 return $this->mEmailAuthenticated;
02139         }
02140 
02145         public function setEmail( $str ) {
02146                 $this->load();
02147                 if( $str == $this->mEmail ) {
02148                         return;
02149                 }
02150                 $this->mEmail = $str;
02151                 $this->invalidateEmail();
02152                 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
02153         }
02154 
02162         public function setEmailWithConfirmation( $str ) {
02163                 global $wgEnableEmail, $wgEmailAuthentication;
02164 
02165                 if ( !$wgEnableEmail ) {
02166                         return Status::newFatal( 'emaildisabled' );
02167                 }
02168 
02169                 $oldaddr = $this->getEmail();
02170                 if ( $str === $oldaddr ) {
02171                         return Status::newGood( true );
02172                 }
02173 
02174                 $this->setEmail( $str );
02175 
02176                 if ( $str !== '' && $wgEmailAuthentication ) {
02177                         # Send a confirmation request to the new address if needed
02178                         $type = $oldaddr != '' ? 'changed' : 'set';
02179                         $result = $this->sendConfirmationMail( $type );
02180                         if ( $result->isGood() ) {
02181                                 # Say the the caller that a confirmation mail has been sent
02182                                 $result->value = 'eauth';
02183                         }
02184                 } else {
02185                         $result = Status::newGood( true );
02186                 }
02187 
02188                 return $result;
02189         }
02190 
02195         public function getRealName() {
02196                 if ( !$this->isItemLoaded( 'realname' ) ) {
02197                         $this->load();
02198                 }
02199 
02200                 return $this->mRealName;
02201         }
02202 
02207         public function setRealName( $str ) {
02208                 $this->load();
02209                 $this->mRealName = $str;
02210         }
02211 
02222         public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
02223                 global $wgHiddenPrefs;
02224                 $this->loadOptions();
02225 
02226                 if ( is_null( $this->mOptions ) ) {
02227                         if($defaultOverride != '') {
02228                                 return $defaultOverride;
02229                         }
02230                         $this->mOptions = User::getDefaultOptions();
02231                 }
02232 
02233                 # We want 'disabled' preferences to always behave as the default value for
02234                 # users, even if they have set the option explicitly in their settings (ie they
02235                 # set it, and then it was disabled removing their ability to change it).  But
02236                 # we don't want to erase the preferences in the database in case the preference
02237                 # is re-enabled again.  So don't touch $mOptions, just override the returned value
02238                 if( in_array( $oname, $wgHiddenPrefs ) && !$ignoreHidden ){
02239                         return self::getDefaultOption( $oname );
02240                 }
02241 
02242                 if ( array_key_exists( $oname, $this->mOptions ) ) {
02243                         return $this->mOptions[$oname];
02244                 } else {
02245                         return $defaultOverride;
02246                 }
02247         }
02248 
02254         public function getOptions() {
02255                 global $wgHiddenPrefs;
02256                 $this->loadOptions();
02257                 $options = $this->mOptions;
02258 
02259                 # We want 'disabled' preferences to always behave as the default value for
02260                 # users, even if they have set the option explicitly in their settings (ie they
02261                 # set it, and then it was disabled removing their ability to change it).  But
02262                 # we don't want to erase the preferences in the database in case the preference
02263                 # is re-enabled again.  So don't touch $mOptions, just override the returned value
02264                 foreach( $wgHiddenPrefs as $pref ){
02265                         $default = self::getDefaultOption( $pref );
02266                         if( $default !== null ){
02267                                 $options[$pref] = $default;
02268                         }
02269                 }
02270 
02271                 return $options;
02272         }
02273 
02281         public function getBoolOption( $oname ) {
02282                 return (bool)$this->getOption( $oname );
02283         }
02284 
02293         public function getIntOption( $oname, $defaultOverride=0 ) {
02294                 $val = $this->getOption( $oname );
02295                 if( $val == '' ) {
02296                         $val = $defaultOverride;
02297                 }
02298                 return intval( $val );
02299         }
02300 
02307         public function setOption( $oname, $val ) {
02308                 $this->load();
02309                 $this->loadOptions();
02310 
02311                 // Explicitly NULL values should refer to defaults
02312                 if( is_null( $val ) ) {
02313                         $defaultOption = self::getDefaultOption( $oname );
02314                         if( !is_null( $defaultOption ) ) {
02315                                 $val = $defaultOption;
02316                         }
02317                 }
02318 
02319                 $this->mOptions[$oname] = $val;
02320         }
02321 
02325         public function resetOptions() {
02326                 $this->load();
02327 
02328                 $this->mOptions = self::getDefaultOptions();
02329                 $this->mOptionsLoaded = true;
02330         }
02331 
02336         public function getDatePreference() {
02337                 // Important migration for old data rows
02338                 if ( is_null( $this->mDatePreference ) ) {
02339                         global $wgLang;
02340                         $value = $this->getOption( 'date' );
02341                         $map = $wgLang->getDatePreferenceMigrationMap();
02342                         if ( isset( $map[$value] ) ) {
02343                                 $value = $map[$value];
02344                         }
02345                         $this->mDatePreference = $value;
02346                 }
02347                 return $this->mDatePreference;
02348         }
02349 
02355         public function getStubThreshold() {
02356                 global $wgMaxArticleSize; # Maximum article size, in Kb
02357                 $threshold = intval( $this->getOption( 'stubthreshold' ) );
02358                 if ( $threshold > $wgMaxArticleSize * 1024 ) {
02359                         # If they have set an impossible value, disable the preference
02360                         # so we can use the parser cache again.
02361                         $threshold = 0;
02362                 }
02363                 return $threshold;
02364         }
02365 
02370         public function getRights() {
02371                 if ( is_null( $this->mRights ) ) {
02372                         $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
02373                         wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
02374                         // Force reindexation of rights when a hook has unset one of them
02375                         $this->mRights = array_values( $this->mRights );
02376                 }
02377                 return $this->mRights;
02378         }
02379 
02385         public function getGroups() {
02386                 $this->load();
02387                 $this->loadGroups();
02388                 return $this->mGroups;
02389         }
02390 
02398         public function getEffectiveGroups( $recache = false ) {
02399                 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
02400                         wfProfileIn( __METHOD__ );
02401                         $this->mEffectiveGroups = array_unique( array_merge(
02402                                 $this->getGroups(), // explicit groups
02403                                 $this->getAutomaticGroups( $recache ) // implicit groups
02404                         ) );
02405                         # Hook for additional groups
02406                         wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
02407                         wfProfileOut( __METHOD__ );
02408                 }
02409                 return $this->mEffectiveGroups;
02410         }
02411 
02419         public function getAutomaticGroups( $recache = false ) {
02420                 if ( $recache || is_null( $this->mImplicitGroups ) ) {
02421                         wfProfileIn( __METHOD__ );
02422                         $this->mImplicitGroups = array( '*' );
02423                         if ( $this->getId() ) {
02424                                 $this->mImplicitGroups[] = 'user';
02425 
02426                                 $this->mImplicitGroups = array_unique( array_merge(
02427                                         $this->mImplicitGroups,
02428                                         Autopromote::getAutopromoteGroups( $this )
02429                                 ) );
02430                         }
02431                         if ( $recache ) {
02432                                 # Assure data consistency with rights/groups,
02433                                 # as getEffectiveGroups() depends on this function
02434                                 $this->mEffectiveGroups = null;
02435                         }
02436                         wfProfileOut( __METHOD__ );
02437                 }
02438                 return $this->mImplicitGroups;
02439         }
02440 
02450         public function getFormerGroups() {
02451                 if( is_null( $this->mFormerGroups ) ) {
02452                         $dbr = wfGetDB( DB_MASTER );
02453                         $res = $dbr->select( 'user_former_groups',
02454                                 array( 'ufg_group' ),
02455                                 array( 'ufg_user' => $this->mId ),
02456                                 __METHOD__ );
02457                         $this->mFormerGroups = array();
02458                         foreach( $res as $row ) {
02459                                 $this->mFormerGroups[] = $row->ufg_group;
02460                         }
02461                 }
02462                 return $this->mFormerGroups;
02463         }
02464 
02469         public function getEditCount() {
02470                 if( $this->getId() ) {
02471                         if ( !isset( $this->mEditCount ) ) {
02472                                 /* Populate the count, if it has not been populated yet */
02473                                 $this->mEditCount = User::edits( $this->mId );
02474                         }
02475                         return $this->mEditCount;
02476                 } else {
02477                         /* nil */
02478                         return null;
02479                 }
02480         }
02481 
02487         public function addGroup( $group ) {
02488                 if( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) {
02489                         $dbw = wfGetDB( DB_MASTER );
02490                         if( $this->getId() ) {
02491                                 $dbw->insert( 'user_groups',
02492                                         array(
02493                                                 'ug_user'  => $this->getID(),
02494                                                 'ug_group' => $group,
02495                                         ),
02496                                         __METHOD__,
02497                                         array( 'IGNORE' ) );
02498                         }
02499                 }
02500                 $this->loadGroups();
02501                 $this->mGroups[] = $group;
02502                 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
02503 
02504                 $this->invalidateCache();
02505         }
02506 
02512         public function removeGroup( $group ) {
02513                 $this->load();
02514                 if( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) {
02515                         $dbw = wfGetDB( DB_MASTER );
02516                         $dbw->delete( 'user_groups',
02517                                 array(
02518                                         'ug_user'  => $this->getID(),
02519                                         'ug_group' => $group,
02520                                 ), __METHOD__ );
02521                         // Remember that the user was in this group
02522                         $dbw->insert( 'user_former_groups',
02523                                 array(
02524                                         'ufg_user'  => $this->getID(),
02525                                         'ufg_group' => $group,
02526                                 ),
02527                                 __METHOD__,
02528                                 array( 'IGNORE' ) );
02529                 }
02530                 $this->loadGroups();
02531                 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
02532                 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
02533 
02534                 $this->invalidateCache();
02535         }
02536 
02541         public function isLoggedIn() {
02542                 return $this->getID() != 0;
02543         }
02544 
02549         public function isAnon() {
02550                 return !$this->isLoggedIn();
02551         }
02552 
02561         public function isAllowedAny( /*...*/ ){
02562                 $permissions = func_get_args();
02563                 foreach( $permissions as $permission ){
02564                         if( $this->isAllowed( $permission ) ){
02565                                 return true;
02566                         }
02567                 }
02568                 return false;
02569         }
02570 
02576         public function isAllowedAll( /*...*/ ){
02577                 $permissions = func_get_args();
02578                 foreach( $permissions as $permission ){
02579                         if( !$this->isAllowed( $permission ) ){
02580                                 return false;
02581                         }
02582                 }
02583                 return true;
02584         }
02585 
02591         public function isAllowed( $action = '' ) {
02592                 if ( $action === '' ) {
02593                         return true; // In the spirit of DWIM
02594                 }
02595                 # Patrolling may not be enabled
02596                 if( $action === 'patrol' || $action === 'autopatrol' ) {
02597                         global $wgUseRCPatrol, $wgUseNPPatrol;
02598                         if( !$wgUseRCPatrol && !$wgUseNPPatrol )
02599                                 return false;
02600                 }
02601                 # Use strict parameter to avoid matching numeric 0 accidentally inserted
02602                 # by misconfiguration: 0 == 'foo'
02603                 return in_array( $action, $this->getRights(), true );
02604         }
02605 
02610         public function useRCPatrol() {
02611                 global $wgUseRCPatrol;
02612                 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
02613         }
02614 
02619         public function useNPPatrol() {
02620                 global $wgUseRCPatrol, $wgUseNPPatrol;
02621                 return( ( $wgUseRCPatrol || $wgUseNPPatrol ) && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) );
02622         }
02623 
02629         public function getRequest() {
02630                 if ( $this->mRequest ) {
02631                         return $this->mRequest;
02632                 } else {
02633                         global $wgRequest;
02634                         return $wgRequest;
02635                 }
02636         }
02637 
02644         public function getSkin() {
02645                 wfDeprecated( __METHOD__, '1.18' );
02646                 return RequestContext::getMain()->getSkin();
02647         }
02648 
02655         public function getWatchedItem( $title ) {
02656                 $key = $title->getNamespace() . ':' . $title->getDBkey();
02657 
02658                 if ( isset( $this->mWatchedItems[$key] ) ) {
02659                         return $this->mWatchedItems[$key];
02660                 }
02661 
02662                 if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
02663                         $this->mWatchedItems = array();
02664                 }
02665 
02666                 $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title );
02667                 return $this->mWatchedItems[$key];
02668         }
02669 
02675         public function isWatched( $title ) {
02676                 return $this->getWatchedItem( $title )->isWatched();
02677         }
02678 
02683         public function addWatch( $title ) {
02684                 $this->getWatchedItem( $title )->addWatch();
02685                 $this->invalidateCache();
02686         }
02687 
02692         public function removeWatch( $title ) {
02693                 $this->getWatchedItem( $title )->removeWatch();
02694                 $this->invalidateCache();
02695         }
02696 
02703         public function clearNotification( &$title ) {
02704                 global $wgUseEnotif, $wgShowUpdatedMarker;
02705 
02706                 # Do nothing if the database is locked to writes
02707                 if( wfReadOnly() ) {
02708                         return;
02709                 }
02710 
02711                 if( $title->getNamespace() == NS_USER_TALK &&
02712                         $title->getText() == $this->getName() ) {
02713                         if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) )
02714                                 return;
02715                         $this->setNewtalk( false );
02716                 }
02717 
02718                 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
02719                         return;
02720                 }
02721 
02722                 if( $this->isAnon() ) {
02723                         // Nothing else to do...
02724                         return;
02725                 }
02726 
02727                 // Only update the timestamp if the page is being watched.
02728                 // The query to find out if it is watched is cached both in memcached and per-invocation,
02729                 // and when it does have to be executed, it can be on a slave
02730                 // If this is the user's newtalk page, we always update the timestamp
02731                 $force = '';
02732                 if ( $title->getNamespace() == NS_USER_TALK &&
02733                         $title->getText() == $this->getName() )
02734                 {
02735                         $force = 'force';
02736                 }
02737 
02738                 $this->getWatchedItem( $title )->resetNotificationTimestamp( $force );
02739         }
02740 
02746         public function clearAllNotifications() {
02747                 global $wgUseEnotif, $wgShowUpdatedMarker;
02748                 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
02749                         $this->setNewtalk( false );
02750                         return;
02751                 }
02752                 $id = $this->getId();
02753                 if( $id != 0 )  {
02754                         $dbw = wfGetDB( DB_MASTER );
02755                         $dbw->update( 'watchlist',
02756                                 array( /* SET */
02757                                         'wl_notificationtimestamp' => null
02758                                 ), array( /* WHERE */
02759                                         'wl_user' => $id
02760                                 ), __METHOD__
02761                         );
02762                 #       We also need to clear here the "you have new message" notification for the own user_talk page
02763                 #       This is cleared one page view later in Article::viewUpdates();
02764                 }
02765         }
02766 
02773         private function decodeOptions( $str ) {
02774                 wfDeprecated( __METHOD__, '1.19' );
02775                 if( !$str )
02776                         return;
02777 
02778                 $this->mOptionsLoaded = true;
02779                 $this->mOptionOverrides = array();
02780 
02781                 // If an option is not set in $str, use the default value
02782                 $this->mOptions = self::getDefaultOptions();
02783 
02784                 $a = explode( "\n", $str );
02785                 foreach ( $a as $s ) {
02786                         $m = array();
02787                         if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
02788                                 $this->mOptions[$m[1]] = $m[2];
02789                                 $this->mOptionOverrides[$m[1]] = $m[2];
02790                         }
02791                 }
02792         }
02793 
02802         protected function setCookie( $name, $value, $exp = 0 ) {
02803                 $this->getRequest()->response()->setcookie( $name, $value, $exp );
02804         }
02805 
02810         protected function clearCookie( $name ) {
02811                 $this->setCookie( $name, '', time() - 86400 );
02812         }
02813 
02820         public function setCookies( $request = null ) {
02821                 if ( $request === null ) {
02822                         $request = $this->getRequest();
02823                 }
02824 
02825                 $this->load();
02826                 if ( 0 == $this->mId ) return;
02827                 if ( !$this->mToken ) {
02828                         // When token is empty or NULL generate a new one and then save it to the database
02829                         // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
02830                         // Simply by setting every cell in the user_token column to NULL and letting them be
02831                         // regenerated as users log back into the wiki.
02832                         $this->setToken();
02833                         $this->saveSettings();
02834                 }
02835                 $session = array(
02836                         'wsUserID' => $this->mId,
02837                         'wsToken' => $this->mToken,
02838                         'wsUserName' => $this->getName()
02839                 );
02840                 $cookies = array(
02841                         'UserID' => $this->mId,
02842                         'UserName' => $this->getName(),
02843                 );
02844                 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
02845                         $cookies['Token'] = $this->mToken;
02846                 } else {
02847                         $cookies['Token'] = false;
02848                 }
02849 
02850                 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
02851 
02852                 foreach ( $session as $name => $value ) {
02853                         $request->setSessionData( $name, $value );
02854                 }
02855                 foreach ( $cookies as $name => $value ) {
02856                         if ( $value === false ) {
02857                                 $this->clearCookie( $name );
02858                         } else {
02859                                 $this->setCookie( $name, $value );
02860                         }
02861                 }
02862         }
02863 
02867         public function logout() {
02868                 if( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
02869                         $this->doLogout();
02870                 }
02871         }
02872 
02877         public function doLogout() {
02878                 $this->clearInstanceCache( 'defaults' );
02879 
02880                 $this->getRequest()->setSessionData( 'wsUserID', 0 );
02881 
02882                 $this->clearCookie( 'UserID' );
02883                 $this->clearCookie( 'Token' );
02884 
02885                 # Remember when user logged out, to prevent seeing cached pages
02886                 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
02887         }
02888 
02893         public function saveSettings() {
02894                 global $wgAuth;
02895 
02896                 $this->load();
02897                 if ( wfReadOnly() ) { return; }
02898                 if ( 0 == $this->mId ) { return; }
02899 
02900                 $this->mTouched = self::newTouchedTimestamp();
02901                 if ( !$wgAuth->allowSetLocalPassword() ) {
02902                         $this->mPassword = '';
02903                 }
02904 
02905                 $dbw = wfGetDB( DB_MASTER );
02906                 $dbw->update( 'user',
02907                         array( /* SET */
02908                                 'user_name' => $this->mName,
02909                                 'user_password' => $this->mPassword,
02910                                 'user_newpassword' => $this->mNewpassword,
02911                                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
02912                                 'user_real_name' => $this->mRealName,
02913                                 'user_email' => $this->mEmail,
02914                                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
02915                                 'user_touched' => $dbw->timestamp( $this->mTouched ),
02916                                 'user_token' => strval( $this->mToken ),
02917                                 'user_email_token' => $this->mEmailToken,
02918                                 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
02919                         ), array( /* WHERE */
02920                                 'user_id' => $this->mId
02921                         ), __METHOD__
02922                 );
02923 
02924                 $this->saveOptions();
02925 
02926                 wfRunHooks( 'UserSaveSettings', array( $this ) );
02927                 $this->clearSharedCache();
02928                 $this->getUserPage()->invalidateCache();
02929         }
02930 
02935         public function idForName() {
02936                 $s = trim( $this->getName() );
02937                 if ( $s === '' ) return 0;
02938 
02939                 $dbr = wfGetDB( DB_SLAVE );
02940                 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
02941                 if ( $id === false ) {
02942                         $id = 0;
02943                 }
02944                 return $id;
02945         }
02946 
02963         public static function createNew( $name, $params = array() ) {
02964                 $user = new User;
02965                 $user->load();
02966                 if ( isset( $params['options'] ) ) {
02967                         $user->mOptions = $params['options'] + (array)$user->mOptions;
02968                         unset( $params['options'] );
02969                 }
02970                 $dbw = wfGetDB( DB_MASTER );
02971                 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
02972 
02973                 $fields = array(
02974                         'user_id' => $seqVal,
02975                         'user_name' => $name,
02976                         'user_password' => $user->mPassword,
02977                         'user_newpassword' => $user->mNewpassword,
02978                         'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
02979                         'user_email' => $user->mEmail,
02980                         'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
02981                         'user_real_name' => $user->mRealName,
02982                         'user_token' => strval( $user->mToken ),
02983                         'user_registration' => $dbw->timestamp( $user->mRegistration ),
02984                         'user_editcount' => 0,
02985                         'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ),
02986                 );
02987                 foreach ( $params as $name => $value ) {
02988                         $fields["user_$name"] = $value;
02989                 }
02990                 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
02991                 if ( $dbw->affectedRows() ) {
02992                         $newUser = User::newFromId( $dbw->insertId() );
02993                 } else {
02994                         $newUser = null;
02995                 }
02996                 return $newUser;
02997         }
02998 
03002         public function addToDatabase() {
03003                 $this->load();
03004 
03005                 $this->mTouched = self::newTouchedTimestamp();
03006 
03007                 $dbw = wfGetDB( DB_MASTER );
03008                 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
03009                 $dbw->insert( 'user',
03010                         array(
03011                                 'user_id' => $seqVal,
03012                                 'user_name' => $this->mName,
03013                                 'user_password' => $this->mPassword,
03014                                 'user_newpassword' => $this->mNewpassword,
03015                                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
03016                                 'user_email' => $this->mEmail,
03017                                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
03018                                 'user_real_name' => $this->mRealName,
03019                                 'user_token' => strval( $this->mToken ),
03020                                 'user_registration' => $dbw->timestamp( $this->mRegistration ),
03021                                 'user_editcount' => 0,
03022                                 'user_touched' => $dbw->timestamp( $this->mTouched ),
03023                         ), __METHOD__
03024                 );
03025                 $this->mId = $dbw->insertId();
03026 
03027                 // Clear instance cache other than user table data, which is already accurate
03028                 $this->clearInstanceCache();
03029 
03030                 $this->saveOptions();
03031         }
03032 
03038         public function spreadAnyEditBlock() {
03039                 if ( $this->isLoggedIn() && $this->isBlocked() ) {
03040                         return $this->spreadBlock();
03041                 }
03042                 return false;
03043         }
03044 
03050         protected function spreadBlock() {
03051                 wfDebug( __METHOD__ . "()\n" );
03052                 $this->load();
03053                 if ( $this->mId == 0 ) {
03054                         return false;
03055                 }
03056 
03057                 $userblock = Block::newFromTarget( $this->getName() );
03058                 if ( !$userblock ) {
03059                         return false;
03060                 }
03061 
03062                 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
03063         }
03064 
03079         public function getPageRenderingHash() {
03080                 wfDeprecated( __METHOD__, '1.17' );
03081 
03082                 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang;
03083                 if( $this->mHash ){
03084                         return $this->mHash;
03085                 }
03086 
03087                 // stubthreshold is only included below for completeness,
03088                 // since it disables the parser cache, its value will always
03089                 // be 0 when this function is called by parsercache.
03090 
03091                 $confstr =        $this->getOption( 'math' );
03092                 $confstr .= '!' . $this->getStubThreshold();
03093                 if ( $wgUseDynamicDates ) { # This is wrong (bug 24714)
03094                         $confstr .= '!' . $this->getDatePreference();
03095                 }
03096                 $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' );
03097                 $confstr .= '!' . $wgLang->getCode();
03098                 $confstr .= '!' . $this->getOption( 'thumbsize' );
03099                 // add in language specific options, if any
03100                 $extra = $wgContLang->getExtraHashOptions();
03101                 $confstr .= $extra;
03102 
03103                 // Since the skin could be overloading link(), it should be
03104                 // included here but in practice, none of our skins do that.
03105 
03106                 $confstr .= $wgRenderHashAppend;
03107 
03108                 // Give a chance for extensions to modify the hash, if they have
03109                 // extra options or other effects on the parser cache.
03110                 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
03111 
03112                 // Make it a valid memcached key fragment
03113                 $confstr = str_replace( ' ', '_', $confstr );
03114                 $this->mHash = $confstr;
03115                 return $confstr;
03116         }
03117 
03122         public function isBlockedFromCreateAccount() {
03123                 $this->getBlockedStatus();
03124                 if( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ){
03125                         return $this->mBlock;
03126                 }
03127 
03128                 # bug 13611: if the IP address the user is trying to create an account from is
03129                 # blocked with createaccount disabled, prevent new account creation there even
03130                 # when the user is logged in
03131                 if( $this->mBlockedFromCreateAccount === false ){
03132                         $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
03133                 }
03134                 return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
03135                         ? $this->mBlockedFromCreateAccount
03136                         : false;
03137         }
03138 
03143         public function isBlockedFromEmailuser() {
03144                 $this->getBlockedStatus();
03145                 return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
03146         }
03147 
03152         function isAllowedToCreateAccount() {
03153                 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
03154         }
03155 
03161         public function getUserPage() {
03162                 return Title::makeTitle( NS_USER, $this->getName() );
03163         }
03164 
03170         public function getTalkPage() {
03171                 $title = $this->getUserPage();
03172                 return $title->getTalkPage();
03173         }
03174 
03180         public function isNewbie() {
03181                 return !$this->isAllowed( 'autoconfirmed' );
03182         }
03183 
03189         public function checkPassword( $password ) {
03190                 global $wgAuth, $wgLegacyEncoding;
03191                 $this->load();
03192 
03193                 // Even though we stop people from creating passwords that
03194                 // are shorter than this, doesn't mean people wont be able
03195                 // to. Certain authentication plugins do NOT want to save
03196                 // domain passwords in a mysql database, so we should
03197                 // check this (in case $wgAuth->strict() is false).
03198                 if( !$this->isValidPassword( $password ) ) {
03199                         return false;
03200                 }
03201 
03202                 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
03203                         return true;
03204                 } elseif( $wgAuth->strict() ) {
03205                         /* Auth plugin doesn't allow local authentication */
03206                         return false;
03207                 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
03208                         /* Auth plugin doesn't allow local authentication for this user name */
03209                         return false;
03210                 }
03211                 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
03212                         return true;
03213                 } elseif ( $wgLegacyEncoding ) {
03214                         # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
03215                         # Check for this with iconv
03216                         $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
03217                         if ( $cp1252Password != $password &&
03218                                 self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) )
03219                         {
03220                                 return true;
03221                         }
03222                 }
03223                 return false;
03224         }
03225 
03234         public function checkTemporaryPassword( $plaintext ) {
03235                 global $wgNewPasswordExpiry;
03236 
03237                 $this->load();
03238                 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
03239                         if ( is_null( $this->mNewpassTime ) ) {
03240                                 return true;
03241                         }
03242                         $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
03243                         return ( time() < $expiry );
03244                 } else {
03245                         return false;
03246                 }
03247         }
03248 
03257         public function editToken( $salt = '', $request = null ) {
03258                 wfDeprecated( __METHOD__, '1.19' );
03259                 return $this->getEditToken( $salt, $request );
03260         }
03261 
03274         public function getEditToken( $salt = '', $request = null ) {
03275                 if ( $request == null ) {
03276                         $request = $this->getRequest();
03277                 }
03278 
03279                 if ( $this->isAnon() ) {
03280                         return EDIT_TOKEN_SUFFIX;
03281                 } else {
03282                         $token = $request->getSessionData( 'wsEditToken' );
03283                         if ( $token === null ) {
03284                                 $token = MWCryptRand::generateHex( 32 );
03285                                 $request->setSessionData( 'wsEditToken', $token );
03286                         }
03287                         if( is_array( $salt ) ) {
03288                                 $salt = implode( '|', $salt );
03289                         }
03290                         return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
03291                 }
03292         }
03293 
03301         public static function generateToken( $salt = '' ) {
03302                 return MWCryptRand::generateHex( 32 );
03303         }
03304 
03316         public function matchEditToken( $val, $salt = '', $request = null ) {
03317                 $sessionToken = $this->getEditToken( $salt, $request );
03318                 if ( $val != $sessionToken ) {
03319                         wfDebug( "User::matchEditToken: broken session data\n" );
03320                 }
03321                 return $val == $sessionToken;
03322         }
03323 
03333         public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
03334                 $sessionToken = $this->getEditToken( $salt, $request );
03335                 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
03336         }
03337 
03345         public function sendConfirmationMail( $type = 'created' ) {
03346                 global $wgLang;
03347                 $expiration = null; // gets passed-by-ref and defined in next line.
03348                 $token = $this->confirmationToken( $expiration );
03349                 $url = $this->confirmationTokenUrl( $token );
03350                 $invalidateURL = $this->invalidationTokenUrl( $token );
03351                 $this->saveSettings();
03352 
03353                 if ( $type == 'created' || $type === false ) {
03354                         $message = 'confirmemail_body';
03355                 } elseif ( $type === true ) {
03356                         $message = 'confirmemail_body_changed';
03357                 } else {
03358                         $message = 'confirmemail_body_' . $type;
03359                 }
03360 
03361                 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
03362                         wfMessage( $message,
03363                                 $this->getRequest()->getIP(),
03364                                 $this->getName(),
03365                                 $url,
03366                                 $wgLang->timeanddate( $expiration, false ),
03367                                 $invalidateURL,
03368                                 $wgLang->date( $expiration, false ),
03369                                 $wgLang->time( $expiration, false ) )->text() );
03370         }
03371 
03382         public function sendMail( $subject, $body, $from = null, $replyto = null ) {
03383                 if( is_null( $from ) ) {
03384                         global $wgPasswordSender, $wgPasswordSenderName;
03385                         $sender = new MailAddress( $wgPasswordSender, $wgPasswordSenderName );
03386                 } else {
03387                         $sender = new MailAddress( $from );
03388                 }
03389 
03390                 $to = new MailAddress( $this );
03391                 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
03392         }
03393 
03404         private function confirmationToken( &$expiration ) {
03405                 global $wgUserEmailConfirmationTokenExpiry;
03406                 $now = time();
03407                 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
03408                 $expiration = wfTimestamp( TS_MW, $expires );
03409                 $this->load();
03410                 $token = MWCryptRand::generateHex( 32 );
03411                 $hash = md5( $token );
03412                 $this->mEmailToken = $hash;
03413                 $this->mEmailTokenExpires = $expiration;
03414                 return $token;
03415         }
03416 
03422         private function confirmationTokenUrl( $token ) {
03423                 return $this->getTokenUrl( 'ConfirmEmail', $token );
03424         }
03425 
03431         private function invalidationTokenUrl( $token ) {
03432                 return $this->getTokenUrl( 'InvalidateEmail', $token );
03433         }
03434 
03449         protected function getTokenUrl( $page, $token ) {
03450                 // Hack to bypass localization of 'Special:'
03451                 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
03452                 return $title->getCanonicalUrl();
03453         }
03454 
03462         public function confirmEmail() {
03463                 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
03464                 wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
03465                 return true;
03466         }
03467 
03475         function invalidateEmail() {
03476                 $this->load();
03477                 $this->mEmailToken = null;
03478                 $this->mEmailTokenExpires = null;
03479                 $this->setEmailAuthenticationTimestamp( null );
03480                 wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
03481                 return true;
03482         }
03483 
03488         function setEmailAuthenticationTimestamp( $timestamp ) {
03489                 $this->load();
03490                 $this->mEmailAuthenticated = $timestamp;
03491                 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
03492         }
03493 
03499         public function canSendEmail() {
03500                 global $wgEnableEmail, $wgEnableUserEmail;
03501                 if( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
03502                         return false;
03503                 }
03504                 $canSend = $this->isEmailConfirmed();
03505                 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
03506                 return $canSend;
03507         }
03508 
03514         public function canReceiveEmail() {
03515                 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
03516         }
03517 
03528         public function isEmailConfirmed() {
03529                 global $wgEmailAuthentication;
03530                 $this->load();
03531                 $confirmed = true;
03532                 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
03533                         if( $this->isAnon() ) {
03534                                 return false;
03535                         }
03536                         if( !Sanitizer::validateEmail( $this->mEmail ) ) {
03537                                 return false;
03538                         }
03539                         if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
03540                                 return false;
03541                         }
03542                         return true;
03543                 } else {
03544                         return $confirmed;
03545                 }
03546         }
03547 
03552         public function isEmailConfirmationPending() {
03553                 global $wgEmailAuthentication;
03554                 return $wgEmailAuthentication &&
03555                         !$this->isEmailConfirmed() &&
03556                         $this->mEmailToken &&
03557                         $this->mEmailTokenExpires > wfTimestamp();
03558         }
03559 
03566         public function getRegistration() {
03567                 if ( $this->isAnon() ) {
03568                         return false;
03569                 }
03570                 $this->load();
03571                 return $this->mRegistration;
03572         }
03573 
03580         public function getFirstEditTimestamp() {
03581                 if( $this->getId() == 0 ) {
03582                         return false; // anons
03583                 }
03584                 $dbr = wfGetDB( DB_SLAVE );
03585                 $time = $dbr->selectField( 'revision', 'rev_timestamp',
03586                         array( 'rev_user' => $this->getId() ),
03587                         __METHOD__,
03588                         array( 'ORDER BY' => 'rev_timestamp ASC' )
03589                 );
03590                 if( !$time ) {
03591                         return false; // no edits
03592                 }
03593                 return wfTimestamp( TS_MW, $time );
03594         }
03595 
03602         public static function getGroupPermissions( $groups ) {
03603                 global $wgGroupPermissions, $wgRevokePermissions;
03604                 $rights = array();
03605                 // grant every granted permission first
03606                 foreach( $groups as $group ) {
03607                         if( isset( $wgGroupPermissions[$group] ) ) {
03608                                 $rights = array_merge( $rights,
03609                                         // array_filter removes empty items
03610                                         array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
03611                         }
03612                 }
03613                 // now revoke the revoked permissions
03614                 foreach( $groups as $group ) {
03615                         if( isset( $wgRevokePermissions[$group] ) ) {
03616                                 $rights = array_diff( $rights,
03617                                         array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
03618                         }
03619                 }
03620                 return array_unique( $rights );
03621         }
03622 
03629         public static function getGroupsWithPermission( $role ) {
03630                 global $wgGroupPermissions;
03631                 $allowedGroups = array();
03632                 foreach ( $wgGroupPermissions as $group => $rights ) {
03633                         if ( isset( $rights[$role] ) && $rights[$role] ) {
03634                                 $allowedGroups[] = $group;
03635                         }
03636                 }
03637                 return $allowedGroups;
03638         }
03639 
03646         public static function getGroupName( $group ) {
03647                 $msg = wfMessage( "group-$group" );
03648                 return $msg->isBlank() ? $group : $msg->text();
03649         }
03650 
03658         public static function getGroupMember( $group, $username = '#' ) {
03659                 $msg = wfMessage( "group-$group-member", $username );
03660                 return $msg->isBlank() ? $group : $msg->text();
03661         }
03662 
03669         public static function getAllGroups() {
03670                 global $wgGroupPermissions, $wgRevokePermissions;
03671                 return array_diff(
03672                         array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
03673                         self::getImplicitGroups()
03674                 );
03675         }
03676 
03681         public static function getAllRights() {
03682                 if ( self::$mAllRights === false ) {
03683                         global $wgAvailableRights;
03684                         if ( count( $wgAvailableRights ) ) {
03685                                 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
03686                         } else {
03687                                 self::$mAllRights = self::$mCoreRights;
03688                         }
03689                         wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
03690                 }
03691                 return self::$mAllRights;
03692         }
03693 
03698         public static function getImplicitGroups() {
03699                 global $wgImplicitGroups;
03700                 $groups = $wgImplicitGroups;
03701                 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );       #deprecated, use $wgImplictGroups instead
03702                 return $groups;
03703         }
03704 
03711         public static function getGroupPage( $group ) {
03712                 $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
03713                 if( $msg->exists() ) {
03714                         $title = Title::newFromText( $msg->text() );
03715                         if( is_object( $title ) )
03716                                 return $title;
03717                 }
03718                 return false;
03719         }
03720 
03729         public static function makeGroupLinkHTML( $group, $text = '' ) {
03730                 if( $text == '' ) {
03731                         $text = self::getGroupName( $group );
03732                 }
03733                 $title = self::getGroupPage( $group );
03734                 if( $title ) {
03735                         return Linker::link( $title, htmlspecialchars( $text ) );
03736                 } else {
03737                         return $text;
03738                 }
03739         }
03740 
03749         public static function makeGroupLinkWiki( $group, $text = '' ) {
03750                 if( $text == '' ) {
03751                         $text = self::getGroupName( $group );
03752                 }
03753                 $title = self::getGroupPage( $group );
03754                 if( $title ) {
03755                         $page = $title->getPrefixedText();
03756                         return "[[$page|$text]]";
03757                 } else {
03758                         return $text;
03759                 }
03760         }
03761 
03771         public static function changeableByGroup( $group ) {
03772                 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
03773 
03774                 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
03775                 if( empty( $wgAddGroups[$group] ) ) {
03776                         // Don't add anything to $groups
03777                 } elseif( $wgAddGroups[$group] === true ) {
03778                         // You get everything
03779                         $groups['add'] = self::getAllGroups();
03780                 } elseif( is_array( $wgAddGroups[$group] ) ) {
03781                         $groups['add'] = $wgAddGroups[$group];
03782                 }
03783 
03784                 // Same thing for remove
03785                 if( empty( $wgRemoveGroups[$group] ) ) {
03786                 } elseif( $wgRemoveGroups[$group] === true ) {
03787                         $groups['remove'] = self::getAllGroups();
03788                 } elseif( is_array( $wgRemoveGroups[$group] ) ) {
03789                         $groups['remove'] = $wgRemoveGroups[$group];
03790                 }
03791 
03792                 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
03793                 if( empty( $wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
03794                         foreach( $wgGroupsAddToSelf as $key => $value ) {
03795                                 if( is_int( $key ) ) {
03796                                         $wgGroupsAddToSelf['user'][] = $value;
03797                                 }
03798                         }
03799                 }
03800 
03801                 if( empty( $wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
03802                         foreach( $wgGroupsRemoveFromSelf as $key => $value ) {
03803                                 if( is_int( $key ) ) {
03804                                         $wgGroupsRemoveFromSelf['user'][] = $value;
03805                                 }
03806                         }
03807                 }
03808 
03809                 // Now figure out what groups the user can add to him/herself
03810                 if( empty( $wgGroupsAddToSelf[$group] ) ) {
03811                 } elseif( $wgGroupsAddToSelf[$group] === true ) {
03812                         // No idea WHY this would be used, but it's there
03813                         $groups['add-self'] = User::getAllGroups();
03814                 } elseif( is_array( $wgGroupsAddToSelf[$group] ) ) {
03815                         $groups['add-self'] = $wgGroupsAddToSelf[$group];
03816                 }
03817 
03818                 if( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
03819                 } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
03820                         $groups['remove-self'] = User::getAllGroups();
03821                 } elseif( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
03822                         $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
03823                 }
03824 
03825                 return $groups;
03826         }
03827 
03835         public function changeableGroups() {
03836                 if( $this->isAllowed( 'userrights' ) ) {
03837                         // This group gives the right to modify everything (reverse-
03838                         // compatibility with old "userrights lets you change
03839                         // everything")
03840                         // Using array_merge to make the groups reindexed
03841                         $all = array_merge( User::getAllGroups() );
03842                         return array(
03843                                 'add' => $all,
03844                                 'remove' => $all,
03845                                 'add-self' => array(),
03846                                 'remove-self' => array()
03847                         );
03848                 }
03849 
03850                 // Okay, it's not so simple, we will have to go through the arrays
03851                 $groups = array(
03852                         'add' => array(),
03853                         'remove' => array(),
03854                         'add-self' => array(),
03855                         'remove-self' => array()
03856                 );
03857                 $addergroups = $this->getEffectiveGroups();
03858 
03859                 foreach( $addergroups as $addergroup ) {
03860                         $groups = array_merge_recursive(
03861                                 $groups, $this->changeableByGroup( $addergroup )
03862                         );
03863                         $groups['add']    = array_unique( $groups['add'] );
03864                         $groups['remove'] = array_unique( $groups['remove'] );
03865                         $groups['add-self'] = array_unique( $groups['add-self'] );
03866                         $groups['remove-self'] = array_unique( $groups['remove-self'] );
03867                 }
03868                 return $groups;
03869         }
03870 
03875         public function incEditCount() {
03876                 if( !$this->isAnon() ) {
03877                         $dbw = wfGetDB( DB_MASTER );
03878                         $dbw->update( 'user',
03879                                 array( 'user_editcount=user_editcount+1' ),
03880                                 array( 'user_id' => $this->getId() ),
03881                                 __METHOD__ );
03882 
03883                         // Lazy initialization check...
03884                         if( $dbw->affectedRows() == 0 ) {
03885                                 // Pull from a slave to be less cruel to servers
03886                                 // Accuracy isn't the point anyway here
03887                                 $dbr = wfGetDB( DB_SLAVE );
03888                                 $count = $dbr->selectField( 'revision',
03889                                         'COUNT(rev_user)',
03890                                         array( 'rev_user' => $this->getId() ),
03891                                         __METHOD__ );
03892 
03893                                 // Now here's a goddamn hack...
03894                                 if( $dbr !== $dbw ) {
03895                                         // If we actually have a slave server, the count is
03896                                         // at least one behind because the current transaction
03897                                         // has not been committed and replicated.
03898                                         $count++;
03899                                 } else {
03900                                         // But if DB_SLAVE is selecting the master, then the
03901                                         // count we just read includes the revision that was
03902                                         // just added in the working transaction.
03903                                 }
03904 
03905                                 $dbw->update( 'user',
03906                                         array( 'user_editcount' => $count ),
03907                                         array( 'user_id' => $this->getId() ),
03908                                         __METHOD__ );
03909                         }
03910                 }
03911                 // edit count in user cache too
03912                 $this->invalidateCache();
03913         }
03914 
03921         public static function getRightDescription( $right ) {
03922                 $key = "right-$right";
03923                 $msg = wfMessage( $key );
03924                 return $msg->isBlank() ? $right : $msg->text();
03925         }
03926 
03934         public static function oldCrypt( $password, $userId ) {
03935                 global $wgPasswordSalt;
03936                 if ( $wgPasswordSalt ) {
03937                         return md5( $userId . '-' . md5( $password ) );
03938                 } else {
03939                         return md5( $password );
03940                 }
03941         }
03942 
03952         public static function crypt( $password, $salt = false ) {
03953                 global $wgPasswordSalt;
03954 
03955                 $hash = '';
03956                 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
03957                         return $hash;
03958                 }
03959 
03960                 if( $wgPasswordSalt ) {
03961                         if ( $salt === false ) {
03962                                 $salt = MWCryptRand::generateHex( 8 );
03963                         }
03964                         return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
03965                 } else {
03966                         return ':A:' . md5( $password );
03967                 }
03968         }
03969 
03980         public static function comparePasswords( $hash, $password, $userId = false ) {
03981                 $type = substr( $hash, 0, 3 );
03982 
03983                 $result = false;
03984                 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
03985                         return $result;
03986                 }
03987 
03988                 if ( $type == ':A:' ) {
03989                         # Unsalted
03990                         return md5( $password ) === substr( $hash, 3 );
03991                 } elseif ( $type == ':B:' ) {
03992                         # Salted
03993                         list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
03994                         return md5( $salt.'-'.md5( $password ) ) === $realHash;
03995                 } else {
03996                         # Old-style
03997                         return self::oldCrypt( $password, $userId ) === $hash;
03998                 }
03999         }
04000 
04009         public function addNewUserLogEntry( $byEmail = false, $reason = '' ) {
04010                 global $wgUser, $wgContLang, $wgNewUserLog;
04011                 if( empty( $wgNewUserLog ) ) {
04012                         return true; // disabled
04013                 }
04014 
04015                 if( $this->getName() == $wgUser->getName() ) {
04016                         $action = 'create';
04017                 } else {
04018                         $action = 'create2';
04019                         if ( $byEmail ) {
04020                                 if ( $reason === '' ) {
04021                                         $reason = wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text();
04022                                 } else {
04023                                         $reason = $wgContLang->commaList( array(
04024                                                 $reason, wfMessage( 'newuserlog-byemail' )->inContentLanguage()->text() ) );
04025                                 }
04026                         }
04027                 }
04028                 $log = new LogPage( 'newusers' );
04029                 return (int)$log->addEntry(
04030                         $action,
04031                         $this->getUserPage(),
04032                         $reason,
04033                         array( $this->getId() )
04034                 );
04035         }
04036 
04043         public function addNewUserLogEntryAutoCreate() {
04044                 global $wgNewUserLog;
04045                 if( !$wgNewUserLog ) {
04046                         return true; // disabled
04047                 }
04048                 $log = new LogPage( 'newusers', false );
04049                 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ), $this );
04050                 return true;
04051         }
04052 
04056         protected function loadOptions() {
04057                 $this->load();
04058                 if ( $this->mOptionsLoaded || !$this->getId() )
04059                         return;
04060 
04061                 $this->mOptions = self::getDefaultOptions();
04062 
04063                 // Maybe load from the object
04064                 if ( !is_null( $this->mOptionOverrides ) ) {
04065                         wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
04066                         foreach( $this->mOptionOverrides as $key => $value ) {
04067                                 $this->mOptions[$key] = $value;
04068                         }
04069                 } else {
04070                         wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
04071                         // Load from database
04072                         $dbr = wfGetDB( DB_SLAVE );
04073 
04074                         $res = $dbr->select(
04075                                 'user_properties',
04076                                 array( 'up_property', 'up_value' ),
04077                                 array( 'up_user' => $this->getId() ),
04078                                 __METHOD__
04079                         );
04080 
04081                         $this->mOptionOverrides = array();
04082                         foreach ( $res as $row ) {
04083                                 $this->mOptionOverrides[$row->up_property] = $row->up_value;
04084                                 $this->mOptions[$row->up_property] = $row->up_value;
04085                         }
04086                 }
04087 
04088                 $this->mOptionsLoaded = true;
04089 
04090                 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
04091         }
04092 
04096         protected function saveOptions() {
04097                 global $wgAllowPrefChange;
04098 
04099                 $this->loadOptions();
04100 
04101                 // Not using getOptions(), to keep hidden preferences in database
04102                 $saveOptions = $this->mOptions;
04103 
04104                 // Allow hooks to abort, for instance to save to a global profile.
04105                 // Reset options to default state before saving.
04106                 if( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
04107                         return;
04108                 }
04109 
04110                 $extuser = ExternalUser::newFromUser( $this );
04111                 $userId = $this->getId();
04112                 $insert_rows = array();
04113                 foreach( $saveOptions as $key => $value ) {
04114                         # Don't bother storing default values
04115                         $defaultOption = self::getDefaultOption( $key );
04116                         if ( ( is_null( $defaultOption ) &&
04117                                         !( $value === false || is_null( $value ) ) ) ||
04118                                         $value != $defaultOption ) {
04119                                 $insert_rows[] = array(
04120                                                 'up_user' => $userId,
04121                                                 'up_property' => $key,
04122                                                 'up_value' => $value,
04123                                         );
04124                         }
04125                         if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
04126                                 switch ( $wgAllowPrefChange[$key] ) {
04127                                         case 'local':
04128                                         case 'message':
04129                                                 break;
04130                                         case 'semiglobal':
04131                                         case 'global':
04132                                                 $extuser->setPref( $key, $value );
04133                                 }
04134                         }
04135                 }
04136 
04137                 $dbw = wfGetDB( DB_MASTER );
04138                 $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
04139                 $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
04140         }
04141 
04166         public static function passwordChangeInputAttribs() {
04167                 global $wgMinimalPasswordLength;
04168 
04169                 if ( $wgMinimalPasswordLength == 0 ) {
04170                         return array();
04171                 }
04172 
04173                 # Note that the pattern requirement will always be satisfied if the
04174                 # input is empty, so we need required in all cases.
04175                 #
04176                 # @todo FIXME: Bug 23769: This needs to not claim the password is required
04177                 # if e-mail confirmation is being used.  Since HTML5 input validation
04178                 # is b0rked anyway in some browsers, just return nothing.  When it's
04179                 # re-enabled, fix this code to not output required for e-mail
04180                 # registration.
04181                 #$ret = array( 'required' );
04182                 $ret = array();
04183 
04184                 # We can't actually do this right now, because Opera 9.6 will print out
04185                 # the entered password visibly in its error message!  When other
04186                 # browsers add support for this attribute, or Opera fixes its support,
04187                 # we can add support with a version check to avoid doing this on Opera
04188                 # versions where it will be a problem.  Reported to Opera as
04189                 # DSK-262266, but they don't have a public bug tracker for us to follow.
04190                 /*
04191                 if ( $wgMinimalPasswordLength > 1 ) {
04192                         $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
04193                         $ret['title'] = wfMessage( 'passwordtooshort' )
04194                                 ->numParams( $wgMinimalPasswordLength )->text();
04195                 }
04196                 */
04197 
04198                 return $ret;
04199         }
04200 
04206         public static function selectFields() {
04207                 return array(
04208                         'user_id',
04209                         'user_name',
04210                         'user_real_name',
04211                         'user_password',
04212                         'user_newpassword',
04213                         'user_newpass_time',
04214                         'user_email',
04215                         'user_touched',
04216                         'user_token',
04217                         'user_email_authenticated',
04218                         'user_email_token',
04219                         'user_email_token_expires',
04220                         'user_registration',
04221                         'user_editcount',
04222                 );
04223         }
04224 }