MediaWiki  REL1_21
User.php
Go to the documentation of this file.
00001 <?php
00027 define( 'USER_TOKEN_LENGTH', 32 );
00028 
00033 define( 'MW_USER_VERSION', 8 );
00034 
00039 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
00040 
00045 class PasswordError extends MWException {
00046         // NOP
00047 }
00048 
00059 class User {
00064         const USER_TOKEN_LENGTH = USER_TOKEN_LENGTH;
00065         const MW_USER_VERSION = MW_USER_VERSION;
00066         const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
00067 
00071         const MAX_WATCHED_ITEMS_CACHE = 100;
00072 
00079         static $mCacheVars = array(
00080                 // user table
00081                 'mId',
00082                 'mName',
00083                 'mRealName',
00084                 'mPassword',
00085                 'mNewpassword',
00086                 'mNewpassTime',
00087                 'mEmail',
00088                 'mTouched',
00089                 'mToken',
00090                 'mEmailAuthenticated',
00091                 'mEmailToken',
00092                 'mEmailTokenExpires',
00093                 'mRegistration',
00094                 'mEditCount',
00095                 // user_groups table
00096                 'mGroups',
00097                 // user_properties table
00098                 'mOptionOverrides',
00099         );
00100 
00107         static $mCoreRights = array(
00108                 'apihighlimits',
00109                 'autoconfirmed',
00110                 'autopatrol',
00111                 'bigdelete',
00112                 'block',
00113                 'blockemail',
00114                 'bot',
00115                 'browsearchive',
00116                 'createaccount',
00117                 'createpage',
00118                 'createtalk',
00119                 'delete',
00120                 'deletedhistory',
00121                 'deletedtext',
00122                 'deletelogentry',
00123                 'deleterevision',
00124                 'edit',
00125                 'editinterface',
00126                 'editprotected',
00127                 'editusercssjs', #deprecated
00128                 'editusercss',
00129                 'edituserjs',
00130                 'hideuser',
00131                 'import',
00132                 'importupload',
00133                 'ipblock-exempt',
00134                 'markbotedits',
00135                 'mergehistory',
00136                 'minoredit',
00137                 'move',
00138                 'movefile',
00139                 'move-rootuserpages',
00140                 'move-subpages',
00141                 'nominornewtalk',
00142                 'noratelimit',
00143                 'override-export-depth',
00144                 'passwordreset',
00145                 'patrol',
00146                 'patrolmarks',
00147                 'protect',
00148                 'proxyunbannable',
00149                 'purge',
00150                 'read',
00151                 'reupload',
00152                 'reupload-own',
00153                 'reupload-shared',
00154                 'rollback',
00155                 'sendemail',
00156                 'siteadmin',
00157                 'suppressionlog',
00158                 'suppressredirect',
00159                 'suppressrevision',
00160                 'unblockself',
00161                 'undelete',
00162                 'unwatchedpages',
00163                 'upload',
00164                 'upload_by_url',
00165                 'userrights',
00166                 'userrights-interwiki',
00167                 'writeapi',
00168         );
00172         static $mAllRights = false;
00173 
00176         var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
00177                 $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
00178                 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount,
00179                 $mGroups, $mOptionOverrides;
00181 
00186         var $mOptionsLoaded;
00187 
00191         private $mLoadedItems = array();
00193 
00203         var $mFrom;
00204 
00208         var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mRights,
00209                 $mBlockreason, $mEffectiveGroups, $mImplicitGroups, $mFormerGroups, $mBlockedGlobally,
00210                 $mLocked, $mHideName, $mOptions;
00211 
00215         private $mRequest;
00216 
00220         var $mBlock;
00221 
00225         var $mAllowUsertalk;
00226 
00230         private $mBlockedFromCreateAccount = false;
00231 
00235         private $mWatchedItems = array();
00236 
00237         static $idCacheByName = array();
00238 
00249         function __construct() {
00250                 $this->clearInstanceCache( 'defaults' );
00251         }
00252 
00256         function __toString() {
00257                 return $this->getName();
00258         }
00259 
00263         public function load() {
00264                 if ( $this->mLoadedItems === true ) {
00265                         return;
00266                 }
00267                 wfProfileIn( __METHOD__ );
00268 
00269                 # Set it now to avoid infinite recursion in accessors
00270                 $this->mLoadedItems = true;
00271 
00272                 switch ( $this->mFrom ) {
00273                         case 'defaults':
00274                                 $this->loadDefaults();
00275                                 break;
00276                         case 'name':
00277                                 $this->mId = self::idFromName( $this->mName );
00278                                 if ( !$this->mId ) {
00279                                         # Nonexistent user placeholder object
00280                                         $this->loadDefaults( $this->mName );
00281                                 } else {
00282                                         $this->loadFromId();
00283                                 }
00284                                 break;
00285                         case 'id':
00286                                 $this->loadFromId();
00287                                 break;
00288                         case 'session':
00289                                 if( !$this->loadFromSession() ) {
00290                                         // Loading from session failed. Load defaults.
00291                                         $this->loadDefaults();
00292                                 }
00293                                 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
00294                                 break;
00295                         default:
00296                                 wfProfileOut( __METHOD__ );
00297                                 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
00298                 }
00299                 wfProfileOut( __METHOD__ );
00300         }
00301 
00306         public function loadFromId() {
00307                 global $wgMemc;
00308                 if ( $this->mId == 0 ) {
00309                         $this->loadDefaults();
00310                         return false;
00311                 }
00312 
00313                 # Try cache
00314                 $key = wfMemcKey( 'user', 'id', $this->mId );
00315                 $data = $wgMemc->get( $key );
00316                 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) {
00317                         # Object is expired, load from DB
00318                         $data = false;
00319                 }
00320 
00321                 if ( !$data ) {
00322                         wfDebug( "User: cache miss for user {$this->mId}\n" );
00323                         # Load from DB
00324                         if ( !$this->loadFromDatabase() ) {
00325                                 # Can't load from ID, user is anonymous
00326                                 return false;
00327                         }
00328                         $this->saveToCache();
00329                 } else {
00330                         wfDebug( "User: got user {$this->mId} from cache\n" );
00331                         # Restore from cache
00332                         foreach ( self::$mCacheVars as $name ) {
00333                                 $this->$name = $data[$name];
00334                         }
00335                 }
00336 
00337                 $this->mLoadedItems = true;
00338 
00339                 return true;
00340         }
00341 
00345         public function saveToCache() {
00346                 $this->load();
00347                 $this->loadGroups();
00348                 $this->loadOptions();
00349                 if ( $this->isAnon() ) {
00350                         // Anonymous users are uncached
00351                         return;
00352                 }
00353                 $data = array();
00354                 foreach ( self::$mCacheVars as $name ) {
00355                         $data[$name] = $this->$name;
00356                 }
00357                 $data['mVersion'] = MW_USER_VERSION;
00358                 $key = wfMemcKey( 'user', 'id', $this->mId );
00359                 global $wgMemc;
00360                 $wgMemc->set( $key, $data );
00361         }
00362 
00365 
00382         public static function newFromName( $name, $validate = 'valid' ) {
00383                 if ( $validate === true ) {
00384                         $validate = 'valid';
00385                 }
00386                 $name = self::getCanonicalName( $name, $validate );
00387                 if ( $name === false ) {
00388                         return false;
00389                 } else {
00390                         # Create unloaded user object
00391                         $u = new User;
00392                         $u->mName = $name;
00393                         $u->mFrom = 'name';
00394                         $u->setItemLoaded( 'name' );
00395                         return $u;
00396                 }
00397         }
00398 
00405         public static function newFromId( $id ) {
00406                 $u = new User;
00407                 $u->mId = $id;
00408                 $u->mFrom = 'id';
00409                 $u->setItemLoaded( 'id' );
00410                 return $u;
00411         }
00412 
00423         public static function newFromConfirmationCode( $code ) {
00424                 $dbr = wfGetDB( DB_SLAVE );
00425                 $id = $dbr->selectField( 'user', 'user_id', array(
00426                         'user_email_token' => md5( $code ),
00427                         'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
00428                         ) );
00429                 if( $id !== false ) {
00430                         return User::newFromId( $id );
00431                 } else {
00432                         return null;
00433                 }
00434         }
00435 
00443         public static function newFromSession( WebRequest $request = null ) {
00444                 $user = new User;
00445                 $user->mFrom = 'session';
00446                 $user->mRequest = $request;
00447                 return $user;
00448         }
00449 
00464         public static function newFromRow( $row, $data = null ) {
00465                 $user = new User;
00466                 $user->loadFromRow( $row, $data );
00467                 return $user;
00468         }
00469 
00471 
00477         public static function whoIs( $id ) {
00478                 return UserCache::singleton()->getProp( $id, 'name' );
00479         }
00480 
00487         public static function whoIsReal( $id ) {
00488                 return UserCache::singleton()->getProp( $id, 'real_name' );
00489         }
00490 
00496         public static function idFromName( $name ) {
00497                 $nt = Title::makeTitleSafe( NS_USER, $name );
00498                 if( is_null( $nt ) ) {
00499                         # Illegal name
00500                         return null;
00501                 }
00502 
00503                 if ( isset( self::$idCacheByName[$name] ) ) {
00504                         return self::$idCacheByName[$name];
00505                 }
00506 
00507                 $dbr = wfGetDB( DB_SLAVE );
00508                 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
00509 
00510                 if ( $s === false ) {
00511                         $result = null;
00512                 } else {
00513                         $result = $s->user_id;
00514                 }
00515 
00516                 self::$idCacheByName[$name] = $result;
00517 
00518                 if ( count( self::$idCacheByName ) > 1000 ) {
00519                         self::$idCacheByName = array();
00520                 }
00521 
00522                 return $result;
00523         }
00524 
00528         public static function resetIdByNameCache() {
00529                 self::$idCacheByName = array();
00530         }
00531 
00548         public static function isIP( $name ) {
00549                 return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name ) || IP::isIPv6( $name );
00550         }
00551 
00563         public static function isValidUserName( $name ) {
00564                 global $wgContLang, $wgMaxNameChars;
00565 
00566                 if ( $name == ''
00567                 || User::isIP( $name )
00568                 || strpos( $name, '/' ) !== false
00569                 || strlen( $name ) > $wgMaxNameChars
00570                 || $name != $wgContLang->ucfirst( $name ) ) {
00571                         wfDebugLog( 'username', __METHOD__ .
00572                                 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
00573                         return false;
00574                 }
00575 
00576                 // Ensure that the name can't be misresolved as a different title,
00577                 // such as with extra namespace keys at the start.
00578                 $parsed = Title::newFromText( $name );
00579                 if( is_null( $parsed )
00580                         || $parsed->getNamespace()
00581                         || strcmp( $name, $parsed->getPrefixedText() ) ) {
00582                         wfDebugLog( 'username', __METHOD__ .
00583                                 ": '$name' invalid due to ambiguous prefixes" );
00584                         return false;
00585                 }
00586 
00587                 // Check an additional blacklist of troublemaker characters.
00588                 // Should these be merged into the title char list?
00589                 $unicodeBlacklist = '/[' .
00590                         '\x{0080}-\x{009f}' . # iso-8859-1 control chars
00591                         '\x{00a0}' .          # non-breaking space
00592                         '\x{2000}-\x{200f}' . # various whitespace
00593                         '\x{2028}-\x{202f}' . # breaks and control chars
00594                         '\x{3000}' .          # ideographic space
00595                         '\x{e000}-\x{f8ff}' . # private use
00596                         ']/u';
00597                 if( preg_match( $unicodeBlacklist, $name ) ) {
00598                         wfDebugLog( 'username', __METHOD__ .
00599                                 ": '$name' invalid due to blacklisted characters" );
00600                         return false;
00601                 }
00602 
00603                 return true;
00604         }
00605 
00617         public static function isUsableName( $name ) {
00618                 global $wgReservedUsernames;
00619                 // Must be a valid username, obviously ;)
00620                 if ( !self::isValidUserName( $name ) ) {
00621                         return false;
00622                 }
00623 
00624                 static $reservedUsernames = false;
00625                 if ( !$reservedUsernames ) {
00626                         $reservedUsernames = $wgReservedUsernames;
00627                         wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
00628                 }
00629 
00630                 // Certain names may be reserved for batch processes.
00631                 foreach ( $reservedUsernames as $reserved ) {
00632                         if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
00633                                 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
00634                         }
00635                         if ( $reserved == $name ) {
00636                                 return false;
00637                         }
00638                 }
00639                 return true;
00640         }
00641 
00654         public static function isCreatableName( $name ) {
00655                 global $wgInvalidUsernameCharacters;
00656 
00657                 // Ensure that the username isn't longer than 235 bytes, so that
00658                 // (at least for the builtin skins) user javascript and css files
00659                 // will work. (bug 23080)
00660                 if( strlen( $name ) > 235 ) {
00661                         wfDebugLog( 'username', __METHOD__ .
00662                                 ": '$name' invalid due to length" );
00663                         return false;
00664                 }
00665 
00666                 // Preg yells if you try to give it an empty string
00667                 if( $wgInvalidUsernameCharacters !== '' ) {
00668                         if( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
00669                                 wfDebugLog( 'username', __METHOD__ .
00670                                         ": '$name' invalid due to wgInvalidUsernameCharacters" );
00671                                 return false;
00672                         }
00673                 }
00674 
00675                 return self::isUsableName( $name );
00676         }
00677 
00684         public function isValidPassword( $password ) {
00685                 //simple boolean wrapper for getPasswordValidity
00686                 return $this->getPasswordValidity( $password ) === true;
00687         }
00688 
00695         public function getPasswordValidity( $password ) {
00696                 global $wgMinimalPasswordLength, $wgContLang;
00697 
00698                 static $blockedLogins = array(
00699                         'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589
00700                         'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605
00701                 );
00702 
00703                 $result = false; //init $result to false for the internal checks
00704 
00705                 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
00706                         return $result;
00707 
00708                 if ( $result === false ) {
00709                         if( strlen( $password ) < $wgMinimalPasswordLength ) {
00710                                 return 'passwordtooshort';
00711                         } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
00712                                 return 'password-name-match';
00713                         } elseif ( isset( $blockedLogins[ $this->getName() ] ) && $password == $blockedLogins[ $this->getName() ] ) {
00714                                 return 'password-login-forbidden';
00715                         } else {
00716                                 //it seems weird returning true here, but this is because of the
00717                                 //initialization of $result to false above. If the hook is never run or it
00718                                 //doesn't modify $result, then we will likely get down into this if with
00719                                 //a valid password.
00720                                 return true;
00721                         }
00722                 } elseif( $result === true ) {
00723                         return true;
00724                 } else {
00725                         return $result; //the isValidPassword hook set a string $result and returned true
00726                 }
00727         }
00728 
00756         public static function isValidEmailAddr( $addr ) {
00757                 wfDeprecated( __METHOD__, '1.18' );
00758                 return Sanitizer::validateEmail( $addr );
00759         }
00760 
00774         public static function getCanonicalName( $name, $validate = 'valid' ) {
00775                 # Force usernames to capital
00776                 global $wgContLang;
00777                 $name = $wgContLang->ucfirst( $name );
00778 
00779                 # Reject names containing '#'; these will be cleaned up
00780                 # with title normalisation, but then it's too late to
00781                 # check elsewhere
00782                 if( strpos( $name, '#' ) !== false )
00783                         return false;
00784 
00785                 # Clean up name according to title rules
00786                 $t = ( $validate === 'valid' ) ?
00787                         Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
00788                 # Check for invalid titles
00789                 if( is_null( $t ) ) {
00790                         return false;
00791                 }
00792 
00793                 # Reject various classes of invalid names
00794                 global $wgAuth;
00795                 $name = $wgAuth->getCanonicalName( $t->getText() );
00796 
00797                 switch ( $validate ) {
00798                         case false:
00799                                 break;
00800                         case 'valid':
00801                                 if ( !User::isValidUserName( $name ) ) {
00802                                         $name = false;
00803                                 }
00804                                 break;
00805                         case 'usable':
00806                                 if ( !User::isUsableName( $name ) ) {
00807                                         $name = false;
00808                                 }
00809                                 break;
00810                         case 'creatable':
00811                                 if ( !User::isCreatableName( $name ) ) {
00812                                         $name = false;
00813                                 }
00814                                 break;
00815                         default:
00816                                 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
00817                 }
00818                 return $name;
00819         }
00820 
00829         public static function edits( $uid ) {
00830                 wfDeprecated( __METHOD__, '1.21' );
00831                 $user = self::newFromId( $uid );
00832                 return $user->getEditCount();
00833         }
00834 
00840         public static function randomPassword() {
00841                 global $wgMinimalPasswordLength;
00842                 // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
00843                 $length = max( 10, $wgMinimalPasswordLength );
00844                 // Multiply by 1.25 to get the number of hex characters we need
00845                 $length = $length * 1.25;
00846                 // Generate random hex chars
00847                 $hex = MWCryptRand::generateHex( $length );
00848                 // Convert from base 16 to base 32 to get a proper password like string
00849                 return wfBaseConvert( $hex, 16, 32 );
00850         }
00851 
00860         public function loadDefaults( $name = false ) {
00861                 wfProfileIn( __METHOD__ );
00862 
00863                 $this->mId = 0;
00864                 $this->mName = $name;
00865                 $this->mRealName = '';
00866                 $this->mPassword = $this->mNewpassword = '';
00867                 $this->mNewpassTime = null;
00868                 $this->mEmail = '';
00869                 $this->mOptionOverrides = null;
00870                 $this->mOptionsLoaded = false;
00871 
00872                 $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
00873                 if( $loggedOut !== null ) {
00874                         $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
00875                 } else {
00876                         $this->mTouched = '1'; # Allow any pages to be cached
00877                 }
00878 
00879                 $this->mToken = null; // Don't run cryptographic functions till we need a token
00880                 $this->mEmailAuthenticated = null;
00881                 $this->mEmailToken = '';
00882                 $this->mEmailTokenExpires = null;
00883                 $this->mRegistration = wfTimestamp( TS_MW );
00884                 $this->mGroups = array();
00885 
00886                 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
00887 
00888                 wfProfileOut( __METHOD__ );
00889         }
00890 
00903         public function isItemLoaded( $item, $all = 'all' ) {
00904                 return ( $this->mLoadedItems === true && $all === 'all' ) ||
00905                         ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
00906         }
00907 
00913         private function setItemLoaded( $item ) {
00914                 if ( is_array( $this->mLoadedItems ) ) {
00915                         $this->mLoadedItems[$item] = true;
00916                 }
00917         }
00918 
00923         private function loadFromSession() {
00924                 global $wgExternalAuthType, $wgAutocreatePolicy;
00925 
00926                 $result = null;
00927                 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
00928                 if ( $result !== null ) {
00929                         return $result;
00930                 }
00931 
00932                 if ( $wgExternalAuthType && $wgAutocreatePolicy == 'view' ) {
00933                         $extUser = ExternalUser::newFromCookie();
00934                         if ( $extUser ) {
00935                                 # TODO: Automatically create the user here (or probably a bit
00936                                 # lower down, in fact)
00937                         }
00938                 }
00939 
00940                 $request = $this->getRequest();
00941 
00942                 $cookieId = $request->getCookie( 'UserID' );
00943                 $sessId = $request->getSessionData( 'wsUserID' );
00944 
00945                 if ( $cookieId !== null ) {
00946                         $sId = intval( $cookieId );
00947                         if( $sessId !== null && $cookieId != $sessId ) {
00948                                 wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
00949                                         cookie user ID ($sId) don't match!" );
00950                                 return false;
00951                         }
00952                         $request->setSessionData( 'wsUserID', $sId );
00953                 } elseif ( $sessId !== null && $sessId != 0 ) {
00954                         $sId = $sessId;
00955                 } else {
00956                         return false;
00957                 }
00958 
00959                 if ( $request->getSessionData( 'wsUserName' ) !== null ) {
00960                         $sName = $request->getSessionData( 'wsUserName' );
00961                 } elseif ( $request->getCookie( 'UserName' ) !== null ) {
00962                         $sName = $request->getCookie( 'UserName' );
00963                         $request->setSessionData( 'wsUserName', $sName );
00964                 } else {
00965                         return false;
00966                 }
00967 
00968                 $proposedUser = User::newFromId( $sId );
00969                 if ( !$proposedUser->isLoggedIn() ) {
00970                         # Not a valid ID
00971                         return false;
00972                 }
00973 
00974                 global $wgBlockDisablesLogin;
00975                 if( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
00976                         # User blocked and we've disabled blocked user logins
00977                         return false;
00978                 }
00979 
00980                 if ( $request->getSessionData( 'wsToken' ) ) {
00981                         $passwordCorrect = ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
00982                         $from = 'session';
00983                 } elseif ( $request->getCookie( 'Token' ) ) {
00984                         # Get the token from DB/cache and clean it up to remove garbage padding.
00985                         # This deals with historical problems with bugs and the default column value.
00986                         $token = rtrim( $proposedUser->getToken( false ) ); // correct token
00987                         // Make comparison in constant time (bug 61346)
00988                         $passwordCorrect = strlen( $token ) && $this->compareSecrets( $token, $request->getCookie( 'Token' ) );
00989                         $from = 'cookie';
00990                 } else {
00991                         # No session or persistent login cookie
00992                         return false;
00993                 }
00994 
00995                 if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
00996                         $this->loadFromUserObject( $proposedUser );
00997                         $request->setSessionData( 'wsToken', $this->mToken );
00998                         wfDebug( "User: logged in from $from\n" );
00999                         return true;
01000                 } else {
01001                         # Invalid credentials
01002                         wfDebug( "User: can't log in from $from, invalid credentials\n" );
01003                         return false;
01004                 }
01005         }
01006 
01013         protected function compareSecrets( $answer, $test ) {
01014                 if ( strlen( $answer ) !== strlen( $test ) ) {
01015                         $passwordCorrect = false;
01016                 } else {
01017                         $result = 0;
01018                         for ( $i = 0; $i < strlen( $answer ); $i++ ) {
01019                                 $result |= ord( $answer{$i} ) ^ ord( $test{$i} );
01020                         }
01021                         $passwordCorrect = ( $result == 0 );
01022                 }
01023                 return $passwordCorrect;
01024         }
01025 
01032         public function loadFromDatabase() {
01033                 # Paranoia
01034                 $this->mId = intval( $this->mId );
01035 
01037                 if( !$this->mId ) {
01038                         $this->loadDefaults();
01039                         return false;
01040                 }
01041 
01042                 $dbr = wfGetDB( DB_MASTER );
01043                 $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ );
01044 
01045                 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
01046 
01047                 if ( $s !== false ) {
01048                         # Initialise user table data
01049                         $this->loadFromRow( $s );
01050                         $this->mGroups = null; // deferred
01051                         $this->getEditCount(); // revalidation for nulls
01052                         return true;
01053                 } else {
01054                         # Invalid user_id
01055                         $this->mId = 0;
01056                         $this->loadDefaults();
01057                         return false;
01058                 }
01059         }
01060 
01070         public function loadFromRow( $row, $data = null ) {
01071                 $all = true;
01072 
01073                 $this->mGroups = null; // deferred
01074 
01075                 if ( isset( $row->user_name ) ) {
01076                         $this->mName = $row->user_name;
01077                         $this->mFrom = 'name';
01078                         $this->setItemLoaded( 'name' );
01079                 } else {
01080                         $all = false;
01081                 }
01082 
01083                 if ( isset( $row->user_real_name ) ) {
01084                         $this->mRealName = $row->user_real_name;
01085                         $this->setItemLoaded( 'realname' );
01086                 } else {
01087                         $all = false;
01088                 }
01089 
01090                 if ( isset( $row->user_id ) ) {
01091                         $this->mId = intval( $row->user_id );
01092                         $this->mFrom = 'id';
01093                         $this->setItemLoaded( 'id' );
01094                 } else {
01095                         $all = false;
01096                 }
01097 
01098                 if ( isset( $row->user_editcount ) ) {
01099                         $this->mEditCount = $row->user_editcount;
01100                 } else {
01101                         $all = false;
01102                 }
01103 
01104                 if ( isset( $row->user_password ) ) {
01105                         $this->mPassword = $row->user_password;
01106                         $this->mNewpassword = $row->user_newpassword;
01107                         $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
01108                         $this->mEmail = $row->user_email;
01109                         if ( isset( $row->user_options ) ) {
01110                                 $this->decodeOptions( $row->user_options );
01111                         }
01112                         $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
01113                         $this->mToken = $row->user_token;
01114                         if ( $this->mToken == '' ) {
01115                                 $this->mToken = null;
01116                         }
01117                         $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
01118                         $this->mEmailToken = $row->user_email_token;
01119                         $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
01120                         $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
01121                 } else {
01122                         $all = false;
01123                 }
01124 
01125                 if ( $all ) {
01126                         $this->mLoadedItems = true;
01127                 }
01128 
01129                 if ( is_array( $data ) ) {
01130                         if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
01131                                 $this->mGroups = $data['user_groups'];
01132                         }
01133                         if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
01134                                 $this->loadOptions( $data['user_properties'] );
01135                         }
01136                 }
01137         }
01138 
01144         protected function loadFromUserObject( $user ) {
01145                 $user->load();
01146                 $user->loadGroups();
01147                 $user->loadOptions();
01148                 foreach ( self::$mCacheVars as $var ) {
01149                         $this->$var = $user->$var;
01150                 }
01151         }
01152 
01156         private function loadGroups() {
01157                 if ( is_null( $this->mGroups ) ) {
01158                         $dbr = wfGetDB( DB_MASTER );
01159                         $res = $dbr->select( 'user_groups',
01160                                 array( 'ug_group' ),
01161                                 array( 'ug_user' => $this->mId ),
01162                                 __METHOD__ );
01163                         $this->mGroups = array();
01164                         foreach ( $res as $row ) {
01165                                 $this->mGroups[] = $row->ug_group;
01166                         }
01167                 }
01168         }
01169 
01184         public function addAutopromoteOnceGroups( $event ) {
01185                 global $wgAutopromoteOnceLogInRC;
01186 
01187                 $toPromote = array();
01188                 if ( $this->getId() ) {
01189                         $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
01190                         if ( count( $toPromote ) ) {
01191                                 $oldGroups = $this->getGroups(); // previous groups
01192                                 foreach ( $toPromote as $group ) {
01193                                         $this->addGroup( $group );
01194                                 }
01195                                 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
01196 
01197                                 $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
01198                                 $logEntry->setPerformer( $this );
01199                                 $logEntry->setTarget( $this->getUserPage() );
01200                                 $logEntry->setParameters( array(
01201                                         '4::oldgroups' => $oldGroups,
01202                                         '5::newgroups' => $newGroups,
01203                                 ) );
01204                                 $logid = $logEntry->insert();
01205                                 if ( $wgAutopromoteOnceLogInRC ) {
01206                                         $logEntry->publish( $logid );
01207                                 }
01208                         }
01209                 }
01210                 return $toPromote;
01211         }
01212 
01221         public function clearInstanceCache( $reloadFrom = false ) {
01222                 $this->mNewtalk = -1;
01223                 $this->mDatePreference = null;
01224                 $this->mBlockedby = -1; # Unset
01225                 $this->mHash = false;
01226                 $this->mRights = null;
01227                 $this->mEffectiveGroups = null;
01228                 $this->mImplicitGroups = null;
01229                 $this->mGroups = null;
01230                 $this->mOptions = null;
01231                 $this->mOptionsLoaded = false;
01232                 $this->mEditCount = null;
01233 
01234                 if ( $reloadFrom ) {
01235                         $this->mLoadedItems = array();
01236                         $this->mFrom = $reloadFrom;
01237                 }
01238         }
01239 
01246         public static function getDefaultOptions() {
01247                 global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
01248 
01249                 static $defOpt = null;
01250                 if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) {
01251                         // Disabling this for the unit tests, as they rely on being able to change $wgContLang
01252                         // mid-request and see that change reflected in the return value of this function.
01253                         // Which is insane and would never happen during normal MW operation
01254                         return $defOpt;
01255                 }
01256 
01257                 $defOpt = $wgDefaultUserOptions;
01258                 # default language setting
01259                 $defOpt['variant'] = $wgContLang->getCode();
01260                 $defOpt['language'] = $wgContLang->getCode();
01261                 foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
01262                         $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
01263                 }
01264                 $defOpt['skin'] = $wgDefaultSkin;
01265 
01266                 wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) );
01267 
01268                 return $defOpt;
01269         }
01270 
01277         public static function getDefaultOption( $opt ) {
01278                 $defOpts = self::getDefaultOptions();
01279                 if( isset( $defOpts[$opt] ) ) {
01280                         return $defOpts[$opt];
01281                 } else {
01282                         return null;
01283                 }
01284         }
01285 
01293         private function getBlockedStatus( $bFromSlave = true ) {
01294                 global $wgProxyWhitelist, $wgUser;
01295 
01296                 if ( -1 != $this->mBlockedby ) {
01297                         return;
01298                 }
01299 
01300                 wfProfileIn( __METHOD__ );
01301                 wfDebug( __METHOD__.": checking...\n" );
01302 
01303                 // Initialize data...
01304                 // Otherwise something ends up stomping on $this->mBlockedby when
01305                 // things get lazy-loaded later, causing false positive block hits
01306                 // due to -1 !== 0. Probably session-related... Nothing should be
01307                 // overwriting mBlockedby, surely?
01308                 $this->load();
01309 
01310                 # We only need to worry about passing the IP address to the Block generator if the
01311                 # user is not immune to autoblocks/hardblocks, and they are the current user so we
01312                 # know which IP address they're actually coming from
01313                 if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) {
01314                         $ip = $this->getRequest()->getIP();
01315                 } else {
01316                         $ip = null;
01317                 }
01318 
01319                 # User/IP blocking
01320                 $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
01321 
01322                 # Proxy blocking
01323                 if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
01324                         && !in_array( $ip, $wgProxyWhitelist ) )
01325                 {
01326                         # Local list
01327                         if ( self::isLocallyBlockedProxy( $ip ) ) {
01328                                 $block = new Block;
01329                                 $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
01330                                 $block->mReason = wfMessage( 'proxyblockreason' )->text();
01331                                 $block->setTarget( $ip );
01332                         } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
01333                                 $block = new Block;
01334                                 $block->setBlocker( wfMessage( 'sorbs' )->text() );
01335                                 $block->mReason = wfMessage( 'sorbsreason' )->text();
01336                                 $block->setTarget( $ip );
01337                         }
01338                 }
01339 
01340                 if ( $block instanceof Block ) {
01341                         wfDebug( __METHOD__ . ": Found block.\n" );
01342                         $this->mBlock = $block;
01343                         $this->mBlockedby = $block->getByName();
01344                         $this->mBlockreason = $block->mReason;
01345                         $this->mHideName = $block->mHideName;
01346                         $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
01347                 } else {
01348                         $this->mBlockedby = '';
01349                         $this->mHideName = 0;
01350                         $this->mAllowUsertalk = false;
01351                 }
01352 
01353                 # Extensions
01354                 wfRunHooks( 'GetBlockedStatus', array( &$this ) );
01355 
01356                 wfProfileOut( __METHOD__ );
01357         }
01358 
01366         public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
01367                 global $wgEnableSorbs, $wgEnableDnsBlacklist,
01368                         $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
01369 
01370                 if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs )
01371                         return false;
01372 
01373                 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
01374                         return false;
01375 
01376                 $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
01377                 return $this->inDnsBlacklist( $ip, $urls );
01378         }
01379 
01387         public function inDnsBlacklist( $ip, $bases ) {
01388                 wfProfileIn( __METHOD__ );
01389 
01390                 $found = false;
01391                 // @todo FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
01392                 if( IP::isIPv4( $ip ) ) {
01393                         # Reverse IP, bug 21255
01394                         $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
01395 
01396                         foreach( (array)$bases as $base ) {
01397                                 # Make hostname
01398                                 # If we have an access key, use that too (ProjectHoneypot, etc.)
01399                                 if( is_array( $base ) ) {
01400                                         if( count( $base ) >= 2 ) {
01401                                                 # Access key is 1, base URL is 0
01402                                                 $host = "{$base[1]}.$ipReversed.{$base[0]}";
01403                                         } else {
01404                                                 $host = "$ipReversed.{$base[0]}";
01405                                         }
01406                                 } else {
01407                                         $host = "$ipReversed.$base";
01408                                 }
01409 
01410                                 # Send query
01411                                 $ipList = gethostbynamel( $host );
01412 
01413                                 if( $ipList ) {
01414                                         wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
01415                                         $found = true;
01416                                         break;
01417                                 } else {
01418                                         wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base.\n" );
01419                                 }
01420                         }
01421                 }
01422 
01423                 wfProfileOut( __METHOD__ );
01424                 return $found;
01425         }
01426 
01434         public static function isLocallyBlockedProxy( $ip ) {
01435                 global $wgProxyList;
01436 
01437                 if ( !$wgProxyList ) {
01438                         return false;
01439                 }
01440                 wfProfileIn( __METHOD__ );
01441 
01442                 if ( !is_array( $wgProxyList ) ) {
01443                         # Load from the specified file
01444                         $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
01445                 }
01446 
01447                 if ( !is_array( $wgProxyList ) ) {
01448                         $ret = false;
01449                 } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
01450                         $ret = true;
01451                 } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
01452                         # Old-style flipped proxy list
01453                         $ret = true;
01454                 } else {
01455                         $ret = false;
01456                 }
01457                 wfProfileOut( __METHOD__ );
01458                 return $ret;
01459         }
01460 
01466         public function isPingLimitable() {
01467                 global $wgRateLimitsExcludedIPs;
01468                 if( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
01469                         // No other good way currently to disable rate limits
01470                         // for specific IPs. :P
01471                         // But this is a crappy hack and should die.
01472                         return false;
01473                 }
01474                 return !$this->isAllowed( 'noratelimit' );
01475         }
01476 
01487         public function pingLimiter( $action = 'edit' ) {
01488                 # Call the 'PingLimiter' hook
01489                 $result = false;
01490                 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, &$result ) ) ) {
01491                         return $result;
01492                 }
01493 
01494                 global $wgRateLimits;
01495                 if( !isset( $wgRateLimits[$action] ) ) {
01496                         return false;
01497                 }
01498 
01499                 # Some groups shouldn't trigger the ping limiter, ever
01500                 if( !$this->isPingLimitable() )
01501                         return false;
01502 
01503                 global $wgMemc, $wgRateLimitLog;
01504                 wfProfileIn( __METHOD__ );
01505 
01506                 $limits = $wgRateLimits[$action];
01507                 $keys = array();
01508                 $id = $this->getId();
01509                 $ip = $this->getRequest()->getIP();
01510                 $userLimit = false;
01511 
01512                 if( isset( $limits['anon'] ) && $id == 0 ) {
01513                         $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
01514                 }
01515 
01516                 if( isset( $limits['user'] ) && $id != 0 ) {
01517                         $userLimit = $limits['user'];
01518                 }
01519                 if( $this->isNewbie() ) {
01520                         if( isset( $limits['newbie'] ) && $id != 0 ) {
01521                                 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
01522                         }
01523                         if( isset( $limits['ip'] ) ) {
01524                                 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
01525                         }
01526                         $matches = array();
01527                         if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
01528                                 $subnet = $matches[1];
01529                                 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
01530                         }
01531                 }
01532                 // Check for group-specific permissions
01533                 // If more than one group applies, use the group with the highest limit
01534                 foreach ( $this->getGroups() as $group ) {
01535                         if ( isset( $limits[$group] ) ) {
01536                                 if ( $userLimit === false || $limits[$group] > $userLimit ) {
01537                                         $userLimit = $limits[$group];
01538                                 }
01539                         }
01540                 }
01541                 // Set the user limit key
01542                 if ( $userLimit !== false ) {
01543                         wfDebug( __METHOD__ . ": effective user limit: $userLimit\n" );
01544                         $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
01545                 }
01546 
01547                 $triggered = false;
01548                 foreach( $keys as $key => $limit ) {
01549                         list( $max, $period ) = $limit;
01550                         $summary = "(limit $max in {$period}s)";
01551                         $count = $wgMemc->get( $key );
01552                         // Already pinged?
01553                         if( $count ) {
01554                                 if( $count >= $max ) {
01555                                         wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
01556                                         if( $wgRateLimitLog ) {
01557                                                 wfSuppressWarnings();
01558                                                 file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND );
01559                                                 wfRestoreWarnings();
01560                                         }
01561                                         $triggered = true;
01562                                 } else {
01563                                         wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
01564                                 }
01565                         } else {
01566                                 wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
01567                                 $wgMemc->add( $key, 0, intval( $period ) ); // first ping
01568                         }
01569                         $wgMemc->incr( $key );
01570                 }
01571 
01572                 wfProfileOut( __METHOD__ );
01573                 return $triggered;
01574         }
01575 
01582         public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site
01583                 return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
01584         }
01585 
01592         public function getBlock( $bFromSlave = true ) {
01593                 $this->getBlockedStatus( $bFromSlave );
01594                 return $this->mBlock instanceof Block ? $this->mBlock : null;
01595         }
01596 
01604         function isBlockedFrom( $title, $bFromSlave = false ) {
01605                 global $wgBlockAllowsUTEdit;
01606                 wfProfileIn( __METHOD__ );
01607 
01608                 $blocked = $this->isBlocked( $bFromSlave );
01609                 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
01610                 # If a user's name is suppressed, they cannot make edits anywhere
01611                 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
01612                   $title->getNamespace() == NS_USER_TALK ) {
01613                         $blocked = false;
01614                         wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
01615                 }
01616 
01617                 wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
01618 
01619                 wfProfileOut( __METHOD__ );
01620                 return $blocked;
01621         }
01622 
01627         public function blockedBy() {
01628                 $this->getBlockedStatus();
01629                 return $this->mBlockedby;
01630         }
01631 
01636         public function blockedFor() {
01637                 $this->getBlockedStatus();
01638                 return $this->mBlockreason;
01639         }
01640 
01645         public function getBlockId() {
01646                 $this->getBlockedStatus();
01647                 return ( $this->mBlock ? $this->mBlock->getId() : false );
01648         }
01649 
01658         public function isBlockedGlobally( $ip = '' ) {
01659                 if( $this->mBlockedGlobally !== null ) {
01660                         return $this->mBlockedGlobally;
01661                 }
01662                 // User is already an IP?
01663                 if( IP::isIPAddress( $this->getName() ) ) {
01664                         $ip = $this->getName();
01665                 } elseif( !$ip ) {
01666                         $ip = $this->getRequest()->getIP();
01667                 }
01668                 $blocked = false;
01669                 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
01670                 $this->mBlockedGlobally = (bool)$blocked;
01671                 return $this->mBlockedGlobally;
01672         }
01673 
01679         public function isLocked() {
01680                 if( $this->mLocked !== null ) {
01681                         return $this->mLocked;
01682                 }
01683                 global $wgAuth;
01684                 $authUser = $wgAuth->getUserInstance( $this );
01685                 $this->mLocked = (bool)$authUser->isLocked();
01686                 return $this->mLocked;
01687         }
01688 
01694         public function isHidden() {
01695                 if( $this->mHideName !== null ) {
01696                         return $this->mHideName;
01697                 }
01698                 $this->getBlockedStatus();
01699                 if( !$this->mHideName ) {
01700                         global $wgAuth;
01701                         $authUser = $wgAuth->getUserInstance( $this );
01702                         $this->mHideName = (bool)$authUser->isHidden();
01703                 }
01704                 return $this->mHideName;
01705         }
01706 
01711         public function getId() {
01712                 if( $this->mId === null && $this->mName !== null
01713                 && User::isIP( $this->mName ) ) {
01714                         // Special case, we know the user is anonymous
01715                         return 0;
01716                 } elseif( !$this->isItemLoaded( 'id' ) ) {
01717                         // Don't load if this was initialized from an ID
01718                         $this->load();
01719                 }
01720                 return $this->mId;
01721         }
01722 
01727         public function setId( $v ) {
01728                 $this->mId = $v;
01729                 $this->clearInstanceCache( 'id' );
01730         }
01731 
01736         public function getName() {
01737                 if ( $this->isItemLoaded( 'name', 'only' ) ) {
01738                         # Special case optimisation
01739                         return $this->mName;
01740                 } else {
01741                         $this->load();
01742                         if ( $this->mName === false ) {
01743                                 # Clean up IPs
01744                                 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
01745                         }
01746                         return $this->mName;
01747                 }
01748         }
01749 
01763         public function setName( $str ) {
01764                 $this->load();
01765                 $this->mName = $str;
01766         }
01767 
01772         public function getTitleKey() {
01773                 return str_replace( ' ', '_', $this->getName() );
01774         }
01775 
01780         public function getNewtalk() {
01781                 $this->load();
01782 
01783                 # Load the newtalk status if it is unloaded (mNewtalk=-1)
01784                 if( $this->mNewtalk === -1 ) {
01785                         $this->mNewtalk = false; # reset talk page status
01786 
01787                         # Check memcached separately for anons, who have no
01788                         # entire User object stored in there.
01789                         if( !$this->mId ) {
01790                                 global $wgDisableAnonTalk;
01791                                 if( $wgDisableAnonTalk ) {
01792                                         // Anon newtalk disabled by configuration.
01793                                         $this->mNewtalk = false;
01794                                 } else {
01795                                         global $wgMemc;
01796                                         $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
01797                                         $newtalk = $wgMemc->get( $key );
01798                                         if( strval( $newtalk ) !== '' ) {
01799                                                 $this->mNewtalk = (bool)$newtalk;
01800                                         } else {
01801                                                 // Since we are caching this, make sure it is up to date by getting it
01802                                                 // from the master
01803                                                 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
01804                                                 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
01805                                         }
01806                                 }
01807                         } else {
01808                                 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
01809                         }
01810                 }
01811 
01812                 return (bool)$this->mNewtalk;
01813         }
01814 
01819         public function getNewMessageLinks() {
01820                 $talks = array();
01821                 if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
01822                         return $talks;
01823                 } elseif( !$this->getNewtalk() ) {
01824                         return array();
01825                 }
01826                 $utp = $this->getTalkPage();
01827                 $dbr = wfGetDB( DB_SLAVE );
01828                 // Get the "last viewed rev" timestamp from the oldest message notification
01829                 $timestamp = $dbr->selectField( 'user_newtalk',
01830                         'MIN(user_last_timestamp)',
01831                         $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
01832                         __METHOD__ );
01833                 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
01834                 return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
01835         }
01836 
01846         protected function checkNewtalk( $field, $id, $fromMaster = false ) {
01847                 if ( $fromMaster ) {
01848                         $db = wfGetDB( DB_MASTER );
01849                 } else {
01850                         $db = wfGetDB( DB_SLAVE );
01851                 }
01852                 $ok = $db->selectField( 'user_newtalk', $field,
01853                         array( $field => $id ), __METHOD__ );
01854                 return $ok !== false;
01855         }
01856 
01864         protected function updateNewtalk( $field, $id, $curRev = null ) {
01865                 // Get timestamp of the talk page revision prior to the current one
01866                 $prevRev = $curRev ? $curRev->getPrevious() : false;
01867                 $ts = $prevRev ? $prevRev->getTimestamp() : null;
01868                 // Mark the user as having new messages since this revision
01869                 $dbw = wfGetDB( DB_MASTER );
01870                 $dbw->insert( 'user_newtalk',
01871                         array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
01872                         __METHOD__,
01873                         'IGNORE' );
01874                 if ( $dbw->affectedRows() ) {
01875                         wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
01876                         return true;
01877                 } else {
01878                         wfDebug( __METHOD__ . " already set ($field, $id)\n" );
01879                         return false;
01880                 }
01881         }
01882 
01889         protected function deleteNewtalk( $field, $id ) {
01890                 $dbw = wfGetDB( DB_MASTER );
01891                 $dbw->delete( 'user_newtalk',
01892                         array( $field => $id ),
01893                         __METHOD__ );
01894                 if ( $dbw->affectedRows() ) {
01895                         wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
01896                         return true;
01897                 } else {
01898                         wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
01899                         return false;
01900                 }
01901         }
01902 
01908         public function setNewtalk( $val, $curRev = null ) {
01909                 if( wfReadOnly() ) {
01910                         return;
01911                 }
01912 
01913                 $this->load();
01914                 $this->mNewtalk = $val;
01915 
01916                 if( $this->isAnon() ) {
01917                         $field = 'user_ip';
01918                         $id = $this->getName();
01919                 } else {
01920                         $field = 'user_id';
01921                         $id = $this->getId();
01922                 }
01923                 global $wgMemc;
01924 
01925                 if( $val ) {
01926                         $changed = $this->updateNewtalk( $field, $id, $curRev );
01927                 } else {
01928                         $changed = $this->deleteNewtalk( $field, $id );
01929                 }
01930 
01931                 if( $this->isAnon() ) {
01932                         // Anons have a separate memcached space, since
01933                         // user records aren't kept for them.
01934                         $key = wfMemcKey( 'newtalk', 'ip', $id );
01935                         $wgMemc->set( $key, $val ? 1 : 0, 1800 );
01936                 }
01937                 if ( $changed ) {
01938                         $this->invalidateCache();
01939                 }
01940         }
01941 
01947         private static function newTouchedTimestamp() {
01948                 global $wgClockSkewFudge;
01949                 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
01950         }
01951 
01959         private function clearSharedCache() {
01960                 $this->load();
01961                 if( $this->mId ) {
01962                         global $wgMemc;
01963                         $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
01964                 }
01965         }
01966 
01972         public function invalidateCache() {
01973                 if( wfReadOnly() ) {
01974                         return;
01975                 }
01976                 $this->load();
01977                 if( $this->mId ) {
01978                         $this->mTouched = self::newTouchedTimestamp();
01979 
01980                         $dbw = wfGetDB( DB_MASTER );
01981 
01982                         // Prevent contention slams by checking user_touched first
01983                         $now = $dbw->timestamp( $this->mTouched );
01984                         $needsPurge = $dbw->selectField( 'user', '1',
01985                                 array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) )
01986                         );
01987                         if ( $needsPurge ) {
01988                                 $dbw->update( 'user',
01989                                         array( 'user_touched' => $now ),
01990                                         array( 'user_id' => $this->mId, 'user_touched < ' . $dbw->addQuotes( $now ) ),
01991                                         __METHOD__
01992                                 );
01993                         }
01994 
01995                         $this->clearSharedCache();
01996                 }
01997         }
01998 
02005         public function validateCache( $timestamp ) {
02006                 $this->load();
02007                 return ( $timestamp >= $this->mTouched );
02008         }
02009 
02014         public function getTouched() {
02015                 $this->load();
02016                 return $this->mTouched;
02017         }
02018 
02035         public function setPassword( $str ) {
02036                 global $wgAuth;
02037 
02038                 if( $str !== null ) {
02039                         if( !$wgAuth->allowPasswordChange() ) {
02040                                 throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
02041                         }
02042 
02043                         if( !$this->isValidPassword( $str ) ) {
02044                                 global $wgMinimalPasswordLength;
02045                                 $valid = $this->getPasswordValidity( $str );
02046                                 if ( is_array( $valid ) ) {
02047                                         $message = array_shift( $valid );
02048                                         $params = $valid;
02049                                 } else {
02050                                         $message = $valid;
02051                                         $params = array( $wgMinimalPasswordLength );
02052                                 }
02053                                 throw new PasswordError( wfMessage( $message, $params )->text() );
02054                         }
02055                 }
02056 
02057                 if( !$wgAuth->setPassword( $this, $str ) ) {
02058                         throw new PasswordError( wfMessage( 'externaldberror' )->text() );
02059                 }
02060 
02061                 $this->setInternalPassword( $str );
02062 
02063                 return true;
02064         }
02065 
02073         public function setInternalPassword( $str ) {
02074                 $this->load();
02075                 $this->setToken();
02076 
02077                 if( $str === null ) {
02078                         // Save an invalid hash...
02079                         $this->mPassword = '';
02080                 } else {
02081                         $this->mPassword = self::crypt( $str );
02082                 }
02083                 $this->mNewpassword = '';
02084                 $this->mNewpassTime = null;
02085         }
02086 
02092         public function getToken( $forceCreation = true ) {
02093                 $this->load();
02094                 if ( !$this->mToken && $forceCreation ) {
02095                         $this->setToken();
02096                 }
02097                 return $this->mToken;
02098         }
02099 
02106         public function setToken( $token = false ) {
02107                 $this->load();
02108                 if ( !$token ) {
02109                         $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH );
02110                 } else {
02111                         $this->mToken = $token;
02112                 }
02113         }
02114 
02121         public function setNewpassword( $str, $throttle = true ) {
02122                 $this->load();
02123                 $this->mNewpassword = self::crypt( $str );
02124                 if ( $throttle ) {
02125                         $this->mNewpassTime = wfTimestampNow();
02126                 }
02127         }
02128 
02134         public function isPasswordReminderThrottled() {
02135                 global $wgPasswordReminderResendTime;
02136                 $this->load();
02137                 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
02138                         return false;
02139                 }
02140                 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
02141                 return time() < $expiry;
02142         }
02143 
02148         public function getEmail() {
02149                 $this->load();
02150                 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
02151                 return $this->mEmail;
02152         }
02153 
02158         public function getEmailAuthenticationTimestamp() {
02159                 $this->load();
02160                 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
02161                 return $this->mEmailAuthenticated;
02162         }
02163 
02168         public function setEmail( $str ) {
02169                 $this->load();
02170                 if( $str == $this->mEmail ) {
02171                         return;
02172                 }
02173                 $this->mEmail = $str;
02174                 $this->invalidateEmail();
02175                 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
02176         }
02177 
02185         public function setEmailWithConfirmation( $str ) {
02186                 global $wgEnableEmail, $wgEmailAuthentication;
02187 
02188                 if ( !$wgEnableEmail ) {
02189                         return Status::newFatal( 'emaildisabled' );
02190                 }
02191 
02192                 $oldaddr = $this->getEmail();
02193                 if ( $str === $oldaddr ) {
02194                         return Status::newGood( true );
02195                 }
02196 
02197                 $this->setEmail( $str );
02198 
02199                 if ( $str !== '' && $wgEmailAuthentication ) {
02200                         # Send a confirmation request to the new address if needed
02201                         $type = $oldaddr != '' ? 'changed' : 'set';
02202                         $result = $this->sendConfirmationMail( $type );
02203                         if ( $result->isGood() ) {
02204                                 # Say the the caller that a confirmation mail has been sent
02205                                 $result->value = 'eauth';
02206                         }
02207                 } else {
02208                         $result = Status::newGood( true );
02209                 }
02210 
02211                 return $result;
02212         }
02213 
02218         public function getRealName() {
02219                 if ( !$this->isItemLoaded( 'realname' ) ) {
02220                         $this->load();
02221                 }
02222 
02223                 return $this->mRealName;
02224         }
02225 
02230         public function setRealName( $str ) {
02231                 $this->load();
02232                 $this->mRealName = $str;
02233         }
02234 
02245         public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
02246                 global $wgHiddenPrefs;
02247                 $this->loadOptions();
02248 
02249                 # We want 'disabled' preferences to always behave as the default value for
02250                 # users, even if they have set the option explicitly in their settings (ie they
02251                 # set it, and then it was disabled removing their ability to change it).  But
02252                 # we don't want to erase the preferences in the database in case the preference
02253                 # is re-enabled again.  So don't touch $mOptions, just override the returned value
02254                 if( in_array( $oname, $wgHiddenPrefs ) && !$ignoreHidden ) {
02255                         return self::getDefaultOption( $oname );
02256                 }
02257 
02258                 if ( array_key_exists( $oname, $this->mOptions ) ) {
02259                         return $this->mOptions[$oname];
02260                 } else {
02261                         return $defaultOverride;
02262                 }
02263         }
02264 
02270         public function getOptions() {
02271                 global $wgHiddenPrefs;
02272                 $this->loadOptions();
02273                 $options = $this->mOptions;
02274 
02275                 # We want 'disabled' preferences to always behave as the default value for
02276                 # users, even if they have set the option explicitly in their settings (ie they
02277                 # set it, and then it was disabled removing their ability to change it).  But
02278                 # we don't want to erase the preferences in the database in case the preference
02279                 # is re-enabled again.  So don't touch $mOptions, just override the returned value
02280                 foreach( $wgHiddenPrefs as $pref ) {
02281                         $default = self::getDefaultOption( $pref );
02282                         if( $default !== null ) {
02283                                 $options[$pref] = $default;
02284                         }
02285                 }
02286 
02287                 return $options;
02288         }
02289 
02297         public function getBoolOption( $oname ) {
02298                 return (bool)$this->getOption( $oname );
02299         }
02300 
02309         public function getIntOption( $oname, $defaultOverride = 0 ) {
02310                 $val = $this->getOption( $oname );
02311                 if( $val == '' ) {
02312                         $val = $defaultOverride;
02313                 }
02314                 return intval( $val );
02315         }
02316 
02323         public function setOption( $oname, $val ) {
02324                 $this->loadOptions();
02325 
02326                 // Explicitly NULL values should refer to defaults
02327                 if( is_null( $val ) ) {
02328                         $val = self::getDefaultOption( $oname );
02329                 }
02330 
02331                 $this->mOptions[$oname] = $val;
02332         }
02333 
02355         public static function listOptionKinds() {
02356                 return array(
02357                         'registered',
02358                         'registered-multiselect',
02359                         'registered-checkmatrix',
02360                         'userjs',
02361                         'unused'
02362                 );
02363         }
02364 
02376         public function getOptionKinds( IContextSource $context, $options = null ) {
02377                 $this->loadOptions();
02378                 if ( $options === null ) {
02379                         $options = $this->mOptions;
02380                 }
02381 
02382                 $prefs = Preferences::getPreferences( $this, $context );
02383                 $mapping = array();
02384 
02385                 // Multiselect and checkmatrix options are stored in the database with
02386                 // one key per option, each having a boolean value. Extract those keys.
02387                 $multiselectOptions = array();
02388                 foreach ( $prefs as $name => $info ) {
02389                         if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
02390                                         ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
02391                                 $opts = HTMLFormField::flattenOptions( $info['options'] );
02392                                 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
02393 
02394                                 foreach ( $opts as $value ) {
02395                                         $multiselectOptions["$prefix$value"] = true;
02396                                 }
02397 
02398                                 unset( $prefs[$name] );
02399                         }
02400                 }
02401                 $checkmatrixOptions = array();
02402                 foreach ( $prefs as $name => $info ) {
02403                         if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
02404                                         ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
02405                                 $columns = HTMLFormField::flattenOptions( $info['columns'] );
02406                                 $rows = HTMLFormField::flattenOptions( $info['rows'] );
02407                                 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
02408 
02409                                 foreach ( $columns as $column ) {
02410                                         foreach ( $rows as $row ) {
02411                                                 $checkmatrixOptions["$prefix-$column-$row"] = true;
02412                                         }
02413                                 }
02414 
02415                                 unset( $prefs[$name] );
02416                         }
02417                 }
02418 
02419                 // $value is ignored
02420                 foreach ( $options as $key => $value ) {
02421                         if ( isset( $prefs[$key] ) ) {
02422                                 $mapping[$key] = 'registered';
02423                         } elseif( isset( $multiselectOptions[$key] ) ) {
02424                                 $mapping[$key] = 'registered-multiselect';
02425                         } elseif( isset( $checkmatrixOptions[$key] ) ) {
02426                                 $mapping[$key] = 'registered-checkmatrix';
02427                         } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
02428                                 $mapping[$key] = 'userjs';
02429                         } else {
02430                                 $mapping[$key] = 'unused';
02431                         }
02432                 }
02433 
02434                 return $mapping;
02435         }
02436 
02451         public function resetOptions(
02452                 $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ),
02453                 IContextSource $context = null
02454         ) {
02455                 $this->load();
02456                 $defaultOptions = self::getDefaultOptions();
02457 
02458                 if ( !is_array( $resetKinds ) ) {
02459                         $resetKinds = array( $resetKinds );
02460                 }
02461 
02462                 if ( in_array( 'all', $resetKinds ) ) {
02463                         $newOptions = $defaultOptions;
02464                 } else {
02465                         if ( $context === null ) {
02466                                 $context = RequestContext::getMain();
02467                         }
02468 
02469                         $optionKinds = $this->getOptionKinds( $context );
02470                         $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
02471                         $newOptions = array();
02472 
02473                         // Use default values for the options that should be deleted, and
02474                         // copy old values for the ones that shouldn't.
02475                         foreach ( $this->mOptions as $key => $value ) {
02476                                 if ( in_array( $optionKinds[$key], $resetKinds ) ) {
02477                                         if ( array_key_exists( $key, $defaultOptions ) ) {
02478                                                 $newOptions[$key] = $defaultOptions[$key];
02479                                         }
02480                                 } else {
02481                                         $newOptions[$key] = $value;
02482                                 }
02483                         }
02484                 }
02485 
02486                 $this->mOptions = $newOptions;
02487                 $this->mOptionsLoaded = true;
02488         }
02489 
02494         public function getDatePreference() {
02495                 // Important migration for old data rows
02496                 if ( is_null( $this->mDatePreference ) ) {
02497                         global $wgLang;
02498                         $value = $this->getOption( 'date' );
02499                         $map = $wgLang->getDatePreferenceMigrationMap();
02500                         if ( isset( $map[$value] ) ) {
02501                                 $value = $map[$value];
02502                         }
02503                         $this->mDatePreference = $value;
02504                 }
02505                 return $this->mDatePreference;
02506         }
02507 
02513         public function getStubThreshold() {
02514                 global $wgMaxArticleSize; # Maximum article size, in Kb
02515                 $threshold = $this->getIntOption( 'stubthreshold' );
02516                 if ( $threshold > $wgMaxArticleSize * 1024 ) {
02517                         # If they have set an impossible value, disable the preference
02518                         # so we can use the parser cache again.
02519                         $threshold = 0;
02520                 }
02521                 return $threshold;
02522         }
02523 
02528         public function getRights() {
02529                 if ( is_null( $this->mRights ) ) {
02530                         $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
02531                         wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
02532                         // Force reindexation of rights when a hook has unset one of them
02533                         $this->mRights = array_values( array_unique( $this->mRights ) );
02534                 }
02535                 return $this->mRights;
02536         }
02537 
02543         public function getGroups() {
02544                 $this->load();
02545                 $this->loadGroups();
02546                 return $this->mGroups;
02547         }
02548 
02556         public function getEffectiveGroups( $recache = false ) {
02557                 if ( $recache || is_null( $this->mEffectiveGroups ) ) {
02558                         wfProfileIn( __METHOD__ );
02559                         $this->mEffectiveGroups = array_unique( array_merge(
02560                                 $this->getGroups(), // explicit groups
02561                                 $this->getAutomaticGroups( $recache ) // implicit groups
02562                         ) );
02563                         # Hook for additional groups
02564                         wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
02565                         // Force reindexation of groups when a hook has unset one of them
02566                         $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
02567                         wfProfileOut( __METHOD__ );
02568                 }
02569                 return $this->mEffectiveGroups;
02570         }
02571 
02579         public function getAutomaticGroups( $recache = false ) {
02580                 if ( $recache || is_null( $this->mImplicitGroups ) ) {
02581                         wfProfileIn( __METHOD__ );
02582                         $this->mImplicitGroups = array( '*' );
02583                         if ( $this->getId() ) {
02584                                 $this->mImplicitGroups[] = 'user';
02585 
02586                                 $this->mImplicitGroups = array_unique( array_merge(
02587                                         $this->mImplicitGroups,
02588                                         Autopromote::getAutopromoteGroups( $this )
02589                                 ) );
02590                         }
02591                         if ( $recache ) {
02592                                 # Assure data consistency with rights/groups,
02593                                 # as getEffectiveGroups() depends on this function
02594                                 $this->mEffectiveGroups = null;
02595                         }
02596                         wfProfileOut( __METHOD__ );
02597                 }
02598                 return $this->mImplicitGroups;
02599         }
02600 
02610         public function getFormerGroups() {
02611                 if( is_null( $this->mFormerGroups ) ) {
02612                         $dbr = wfGetDB( DB_MASTER );
02613                         $res = $dbr->select( 'user_former_groups',
02614                                 array( 'ufg_group' ),
02615                                 array( 'ufg_user' => $this->mId ),
02616                                 __METHOD__ );
02617                         $this->mFormerGroups = array();
02618                         foreach( $res as $row ) {
02619                                 $this->mFormerGroups[] = $row->ufg_group;
02620                         }
02621                 }
02622                 return $this->mFormerGroups;
02623         }
02624 
02629         public function getEditCount() {
02630                 if ( !$this->getId() ) {
02631                         return null;
02632                 }
02633 
02634                 if ( !isset( $this->mEditCount ) ) {
02635                         /* Populate the count, if it has not been populated yet */
02636                         wfProfileIn( __METHOD__ );
02637                         $dbr = wfGetDB( DB_SLAVE );
02638                         // check if the user_editcount field has been initialized
02639                         $count = $dbr->selectField(
02640                                 'user', 'user_editcount',
02641                                 array( 'user_id' => $this->mId ),
02642                                 __METHOD__
02643                         );
02644 
02645                         if( $count === null ) {
02646                                 // it has not been initialized. do so.
02647                                 $count = $this->initEditCount();
02648                         }
02649                         $this->mEditCount = intval( $count );
02650                         wfProfileOut( __METHOD__ );
02651                 }
02652                 return $this->mEditCount;
02653         }
02654 
02660         public function addGroup( $group ) {
02661                 if( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) {
02662                         $dbw = wfGetDB( DB_MASTER );
02663                         if( $this->getId() ) {
02664                                 $dbw->insert( 'user_groups',
02665                                         array(
02666                                                 'ug_user' => $this->getID(),
02667                                                 'ug_group' => $group,
02668                                         ),
02669                                         __METHOD__,
02670                                         array( 'IGNORE' ) );
02671                         }
02672                 }
02673                 $this->loadGroups();
02674                 $this->mGroups[] = $group;
02675                 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
02676 
02677                 $this->invalidateCache();
02678         }
02679 
02685         public function removeGroup( $group ) {
02686                 $this->load();
02687                 if( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) {
02688                         $dbw = wfGetDB( DB_MASTER );
02689                         $dbw->delete( 'user_groups',
02690                                 array(
02691                                         'ug_user' => $this->getID(),
02692                                         'ug_group' => $group,
02693                                 ), __METHOD__ );
02694                         // Remember that the user was in this group
02695                         $dbw->insert( 'user_former_groups',
02696                                 array(
02697                                         'ufg_user' => $this->getID(),
02698                                         'ufg_group' => $group,
02699                                 ),
02700                                 __METHOD__,
02701                                 array( 'IGNORE' ) );
02702                 }
02703                 $this->loadGroups();
02704                 $this->mGroups = array_diff( $this->mGroups, array( $group ) );
02705                 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) );
02706 
02707                 $this->invalidateCache();
02708         }
02709 
02714         public function isLoggedIn() {
02715                 return $this->getID() != 0;
02716         }
02717 
02722         public function isAnon() {
02723                 return !$this->isLoggedIn();
02724         }
02725 
02734         public function isAllowedAny( /*...*/ ) {
02735                 $permissions = func_get_args();
02736                 foreach( $permissions as $permission ) {
02737                         if( $this->isAllowed( $permission ) ) {
02738                                 return true;
02739                         }
02740                 }
02741                 return false;
02742         }
02743 
02749         public function isAllowedAll( /*...*/ ) {
02750                 $permissions = func_get_args();
02751                 foreach( $permissions as $permission ) {
02752                         if( !$this->isAllowed( $permission ) ) {
02753                                 return false;
02754                         }
02755                 }
02756                 return true;
02757         }
02758 
02764         public function isAllowed( $action = '' ) {
02765                 if ( $action === '' ) {
02766                         return true; // In the spirit of DWIM
02767                 }
02768                 # Patrolling may not be enabled
02769                 if( $action === 'patrol' || $action === 'autopatrol' ) {
02770                         global $wgUseRCPatrol, $wgUseNPPatrol;
02771                         if( !$wgUseRCPatrol && !$wgUseNPPatrol )
02772                                 return false;
02773                 }
02774                 # Use strict parameter to avoid matching numeric 0 accidentally inserted
02775                 # by misconfiguration: 0 == 'foo'
02776                 return in_array( $action, $this->getRights(), true );
02777         }
02778 
02783         public function useRCPatrol() {
02784                 global $wgUseRCPatrol;
02785                 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
02786         }
02787 
02792         public function useNPPatrol() {
02793                 global $wgUseRCPatrol, $wgUseNPPatrol;
02794                 return( ( $wgUseRCPatrol || $wgUseNPPatrol ) && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) );
02795         }
02796 
02802         public function getRequest() {
02803                 if ( $this->mRequest ) {
02804                         return $this->mRequest;
02805                 } else {
02806                         global $wgRequest;
02807                         return $wgRequest;
02808                 }
02809         }
02810 
02817         public function getSkin() {
02818                 wfDeprecated( __METHOD__, '1.18' );
02819                 return RequestContext::getMain()->getSkin();
02820         }
02821 
02828         public function getWatchedItem( $title ) {
02829                 $key = $title->getNamespace() . ':' . $title->getDBkey();
02830 
02831                 if ( isset( $this->mWatchedItems[$key] ) ) {
02832                         return $this->mWatchedItems[$key];
02833                 }
02834 
02835                 if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
02836                         $this->mWatchedItems = array();
02837                 }
02838 
02839                 $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title );
02840                 return $this->mWatchedItems[$key];
02841         }
02842 
02848         public function isWatched( $title ) {
02849                 return $this->getWatchedItem( $title )->isWatched();
02850         }
02851 
02856         public function addWatch( $title ) {
02857                 $this->getWatchedItem( $title )->addWatch();
02858                 $this->invalidateCache();
02859         }
02860 
02865         public function removeWatch( $title ) {
02866                 $this->getWatchedItem( $title )->removeWatch();
02867                 $this->invalidateCache();
02868         }
02869 
02876         public function clearNotification( &$title ) {
02877                 global $wgUseEnotif, $wgShowUpdatedMarker;
02878 
02879                 # Do nothing if the database is locked to writes
02880                 if( wfReadOnly() ) {
02881                         return;
02882                 }
02883 
02884                 if( $title->getNamespace() == NS_USER_TALK &&
02885                         $title->getText() == $this->getName() ) {
02886                         if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) )
02887                                 return;
02888                         $this->setNewtalk( false );
02889                 }
02890 
02891                 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
02892                         return;
02893                 }
02894 
02895                 if( $this->isAnon() ) {
02896                         // Nothing else to do...
02897                         return;
02898                 }
02899 
02900                 // Only update the timestamp if the page is being watched.
02901                 // The query to find out if it is watched is cached both in memcached and per-invocation,
02902                 // and when it does have to be executed, it can be on a slave
02903                 // If this is the user's newtalk page, we always update the timestamp
02904                 $force = '';
02905                 if ( $title->getNamespace() == NS_USER_TALK &&
02906                         $title->getText() == $this->getName() )
02907                 {
02908                         $force = 'force';
02909                 }
02910 
02911                 $this->getWatchedItem( $title )->resetNotificationTimestamp( $force );
02912         }
02913 
02919         public function clearAllNotifications() {
02920                 if ( wfReadOnly() ) {
02921                         return;
02922                 }
02923 
02924                 global $wgUseEnotif, $wgShowUpdatedMarker;
02925                 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
02926                         $this->setNewtalk( false );
02927                         return;
02928                 }
02929                 $id = $this->getId();
02930                 if( $id != 0 ) {
02931                         $dbw = wfGetDB( DB_MASTER );
02932                         $dbw->update( 'watchlist',
02933                                 array( /* SET */
02934                                         'wl_notificationtimestamp' => null
02935                                 ), array( /* WHERE */
02936                                         'wl_user' => $id
02937                                 ), __METHOD__
02938                         );
02939                 #       We also need to clear here the "you have new message" notification for the own user_talk page
02940                 #       This is cleared one page view later in Article::viewUpdates();
02941                 }
02942         }
02943 
02950         private function decodeOptions( $str ) {
02951                 wfDeprecated( __METHOD__, '1.19' );
02952                 if( !$str )
02953                         return;
02954 
02955                 $this->mOptionsLoaded = true;
02956                 $this->mOptionOverrides = array();
02957 
02958                 // If an option is not set in $str, use the default value
02959                 $this->mOptions = self::getDefaultOptions();
02960 
02961                 $a = explode( "\n", $str );
02962                 foreach ( $a as $s ) {
02963                         $m = array();
02964                         if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
02965                                 $this->mOptions[$m[1]] = $m[2];
02966                                 $this->mOptionOverrides[$m[1]] = $m[2];
02967                         }
02968                 }
02969         }
02970 
02983         protected function setCookie( $name, $value, $exp = 0, $secure = null ) {
02984                 $this->getRequest()->response()->setcookie( $name, $value, $exp, null, null, $secure );
02985         }
02986 
02991         protected function clearCookie( $name ) {
02992                 $this->setCookie( $name, '', time() - 86400 );
02993         }
02994 
03002         public function setCookies( $request = null, $secure = null ) {
03003                 if ( $request === null ) {
03004                         $request = $this->getRequest();
03005                 }
03006 
03007                 $this->load();
03008                 if ( 0 == $this->mId ) {
03009                         return;
03010                 }
03011                 if ( !$this->mToken ) {
03012                         // When token is empty or NULL generate a new one and then save it to the database
03013                         // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
03014                         // Simply by setting every cell in the user_token column to NULL and letting them be
03015                         // regenerated as users log back into the wiki.
03016                         $this->setToken();
03017                         $this->saveSettings();
03018                 }
03019                 $session = array(
03020                         'wsUserID' => $this->mId,
03021                         'wsToken' => $this->mToken,
03022                         'wsUserName' => $this->getName()
03023                 );
03024                 $cookies = array(
03025                         'UserID' => $this->mId,
03026                         'UserName' => $this->getName(),
03027                 );
03028                 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
03029                         $cookies['Token'] = $this->mToken;
03030                 } else {
03031                         $cookies['Token'] = false;
03032                 }
03033 
03034                 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
03035 
03036                 foreach ( $session as $name => $value ) {
03037                         $request->setSessionData( $name, $value );
03038                 }
03039                 foreach ( $cookies as $name => $value ) {
03040                         if ( $value === false ) {
03041                                 $this->clearCookie( $name );
03042                         } else {
03043                                 $this->setCookie( $name, $value, 0, $secure );
03044                         }
03045                 }
03046 
03052                 if ( $request->getCheck( 'wpStickHTTPS' ) ) {
03053                         $this->setCookie( 'forceHTTPS', 'true', time() + 2592000, false ); //30 days
03054                 }
03055         }
03056 
03060         public function logout() {
03061                 if( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
03062                         $this->doLogout();
03063                 }
03064         }
03065 
03070         public function doLogout() {
03071                 $this->clearInstanceCache( 'defaults' );
03072 
03073                 $this->getRequest()->setSessionData( 'wsUserID', 0 );
03074 
03075                 $this->clearCookie( 'UserID' );
03076                 $this->clearCookie( 'Token' );
03077                 $this->clearCookie( 'forceHTTPS' );
03078 
03079                 # Remember when user logged out, to prevent seeing cached pages
03080                 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 );
03081         }
03082 
03087         public function saveSettings() {
03088                 global $wgAuth;
03089 
03090                 $this->load();
03091                 if ( wfReadOnly() ) { return; }
03092                 if ( 0 == $this->mId ) { return; }
03093 
03094                 $this->mTouched = self::newTouchedTimestamp();
03095                 if ( !$wgAuth->allowSetLocalPassword() ) {
03096                         $this->mPassword = '';
03097                 }
03098 
03099                 $dbw = wfGetDB( DB_MASTER );
03100                 $dbw->update( 'user',
03101                         array( /* SET */
03102                                 'user_name' => $this->mName,
03103                                 'user_password' => $this->mPassword,
03104                                 'user_newpassword' => $this->mNewpassword,
03105                                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
03106                                 'user_real_name' => $this->mRealName,
03107                                 'user_email' => $this->mEmail,
03108                                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
03109                                 'user_touched' => $dbw->timestamp( $this->mTouched ),
03110                                 'user_token' => strval( $this->mToken ),
03111                                 'user_email_token' => $this->mEmailToken,
03112                                 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
03113                         ), array( /* WHERE */
03114                                 'user_id' => $this->mId
03115                         ), __METHOD__
03116                 );
03117 
03118                 $this->saveOptions();
03119 
03120                 wfRunHooks( 'UserSaveSettings', array( $this ) );
03121                 $this->clearSharedCache();
03122                 $this->getUserPage()->invalidateCache();
03123         }
03124 
03129         public function idForName() {
03130                 $s = trim( $this->getName() );
03131                 if ( $s === '' ) return 0;
03132 
03133                 $dbr = wfGetDB( DB_SLAVE );
03134                 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
03135                 if ( $id === false ) {
03136                         $id = 0;
03137                 }
03138                 return $id;
03139         }
03140 
03157         public static function createNew( $name, $params = array() ) {
03158                 $user = new User;
03159                 $user->load();
03160                 $user->setToken(); // init token
03161                 if ( isset( $params['options'] ) ) {
03162                         $user->mOptions = $params['options'] + (array)$user->mOptions;
03163                         unset( $params['options'] );
03164                 }
03165                 $dbw = wfGetDB( DB_MASTER );
03166                 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
03167 
03168                 $fields = array(
03169                         'user_id' => $seqVal,
03170                         'user_name' => $name,
03171                         'user_password' => $user->mPassword,
03172                         'user_newpassword' => $user->mNewpassword,
03173                         'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
03174                         'user_email' => $user->mEmail,
03175                         'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
03176                         'user_real_name' => $user->mRealName,
03177                         'user_token' => strval( $user->mToken ),
03178                         'user_registration' => $dbw->timestamp( $user->mRegistration ),
03179                         'user_editcount' => 0,
03180                         'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ),
03181                 );
03182                 foreach ( $params as $name => $value ) {
03183                         $fields["user_$name"] = $value;
03184                 }
03185                 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
03186                 if ( $dbw->affectedRows() ) {
03187                         $newUser = User::newFromId( $dbw->insertId() );
03188                 } else {
03189                         $newUser = null;
03190                 }
03191                 return $newUser;
03192         }
03193 
03220         public function addToDatabase() {
03221                 $this->load();
03222                 if ( !$this->mToken ) {
03223                         $this->setToken(); // init token
03224                 }
03225 
03226                 $this->mTouched = self::newTouchedTimestamp();
03227 
03228                 $dbw = wfGetDB( DB_MASTER );
03229                 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
03230                 $dbw->insert( 'user',
03231                         array(
03232                                 'user_id' => $seqVal,
03233                                 'user_name' => $this->mName,
03234                                 'user_password' => $this->mPassword,
03235                                 'user_newpassword' => $this->mNewpassword,
03236                                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
03237                                 'user_email' => $this->mEmail,
03238                                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
03239                                 'user_real_name' => $this->mRealName,
03240                                 'user_token' => strval( $this->mToken ),
03241                                 'user_registration' => $dbw->timestamp( $this->mRegistration ),
03242                                 'user_editcount' => 0,
03243                                 'user_touched' => $dbw->timestamp( $this->mTouched ),
03244                         ), __METHOD__,
03245                         array( 'IGNORE' )
03246                 );
03247                 if ( !$dbw->affectedRows() ) {
03248                         $this->mId = $dbw->selectField( 'user', 'user_id',
03249                                 array( 'user_name' => $this->mName ), __METHOD__ );
03250                         $loaded = false;
03251                         if ( $this->mId ) {
03252                                 if ( $this->loadFromDatabase() ) {
03253                                         $loaded = true;
03254                                 }
03255                         }
03256                         if ( !$loaded ) {
03257                                 throw new MWException( __METHOD__. ": hit a key conflict attempting " .
03258                                         "to insert a user row, but then it doesn't exist when we select it!" );
03259                         }
03260                         return Status::newFatal( 'userexists' );
03261                 }
03262                 $this->mId = $dbw->insertId();
03263 
03264                 // Clear instance cache other than user table data, which is already accurate
03265                 $this->clearInstanceCache();
03266 
03267                 $this->saveOptions();
03268                 return Status::newGood();
03269         }
03270 
03276         public function spreadAnyEditBlock() {
03277                 if ( $this->isLoggedIn() && $this->isBlocked() ) {
03278                         return $this->spreadBlock();
03279                 }
03280                 return false;
03281         }
03282 
03288         protected function spreadBlock() {
03289                 wfDebug( __METHOD__ . "()\n" );
03290                 $this->load();
03291                 if ( $this->mId == 0 ) {
03292                         return false;
03293                 }
03294 
03295                 $userblock = Block::newFromTarget( $this->getName() );
03296                 if ( !$userblock ) {
03297                         return false;
03298                 }
03299 
03300                 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
03301         }
03302 
03317         public function getPageRenderingHash() {
03318                 wfDeprecated( __METHOD__, '1.17' );
03319 
03320                 global $wgRenderHashAppend, $wgLang, $wgContLang;
03321                 if( $this->mHash ) {
03322                         return $this->mHash;
03323                 }
03324 
03325                 // stubthreshold is only included below for completeness,
03326                 // since it disables the parser cache, its value will always
03327                 // be 0 when this function is called by parsercache.
03328 
03329                 $confstr = $this->getOption( 'math' );
03330                 $confstr .= '!' . $this->getStubThreshold();
03331                 $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' );
03332                 $confstr .= '!' . $wgLang->getCode();
03333                 $confstr .= '!' . $this->getOption( 'thumbsize' );
03334                 // add in language specific options, if any
03335                 $extra = $wgContLang->getExtraHashOptions();
03336                 $confstr .= $extra;
03337 
03338                 // Since the skin could be overloading link(), it should be
03339                 // included here but in practice, none of our skins do that.
03340 
03341                 $confstr .= $wgRenderHashAppend;
03342 
03343                 // Give a chance for extensions to modify the hash, if they have
03344                 // extra options or other effects on the parser cache.
03345                 wfRunHooks( 'PageRenderingHash', array( &$confstr ) );
03346 
03347                 // Make it a valid memcached key fragment
03348                 $confstr = str_replace( ' ', '_', $confstr );
03349                 $this->mHash = $confstr;
03350                 return $confstr;
03351         }
03352 
03357         public function isBlockedFromCreateAccount() {
03358                 $this->getBlockedStatus();
03359                 if( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
03360                         return $this->mBlock;
03361                 }
03362 
03363                 # bug 13611: if the IP address the user is trying to create an account from is
03364                 # blocked with createaccount disabled, prevent new account creation there even
03365                 # when the user is logged in
03366                 if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
03367                         $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
03368                 }
03369                 return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
03370                         ? $this->mBlockedFromCreateAccount
03371                         : false;
03372         }
03373 
03378         public function isBlockedFromEmailuser() {
03379                 $this->getBlockedStatus();
03380                 return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
03381         }
03382 
03387         function isAllowedToCreateAccount() {
03388                 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
03389         }
03390 
03396         public function getUserPage() {
03397                 return Title::makeTitle( NS_USER, $this->getName() );
03398         }
03399 
03405         public function getTalkPage() {
03406                 $title = $this->getUserPage();
03407                 return $title->getTalkPage();
03408         }
03409 
03415         public function isNewbie() {
03416                 return !$this->isAllowed( 'autoconfirmed' );
03417         }
03418 
03424         public function checkPassword( $password ) {
03425                 global $wgAuth, $wgLegacyEncoding;
03426                 $this->load();
03427 
03428                 // Even though we stop people from creating passwords that
03429                 // are shorter than this, doesn't mean people wont be able
03430                 // to. Certain authentication plugins do NOT want to save
03431                 // domain passwords in a mysql database, so we should
03432                 // check this (in case $wgAuth->strict() is false).
03433                 if( !$this->isValidPassword( $password ) ) {
03434                         return false;
03435                 }
03436 
03437                 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
03438                         return true;
03439                 } elseif( $wgAuth->strict() ) {
03440                         /* Auth plugin doesn't allow local authentication */
03441                         return false;
03442                 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) {
03443                         /* Auth plugin doesn't allow local authentication for this user name */
03444                         return false;
03445                 }
03446                 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) {
03447                         return true;
03448                 } elseif ( $wgLegacyEncoding ) {
03449                         # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
03450                         # Check for this with iconv
03451                         $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
03452                         if ( $cp1252Password != $password &&
03453                                 self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) )
03454                         {
03455                                 return true;
03456                         }
03457                 }
03458                 return false;
03459         }
03460 
03469         public function checkTemporaryPassword( $plaintext ) {
03470                 global $wgNewPasswordExpiry;
03471 
03472                 $this->load();
03473                 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) {
03474                         if ( is_null( $this->mNewpassTime ) ) {
03475                                 return true;
03476                         }
03477                         $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
03478                         return ( time() < $expiry );
03479                 } else {
03480                         return false;
03481                 }
03482         }
03483 
03492         public function editToken( $salt = '', $request = null ) {
03493                 wfDeprecated( __METHOD__, '1.19' );
03494                 return $this->getEditToken( $salt, $request );
03495         }
03496 
03509         public function getEditToken( $salt = '', $request = null ) {
03510                 if ( $request == null ) {
03511                         $request = $this->getRequest();
03512                 }
03513 
03514                 if ( $this->isAnon() ) {
03515                         return EDIT_TOKEN_SUFFIX;
03516                 } else {
03517                         $token = $request->getSessionData( 'wsEditToken' );
03518                         if ( $token === null ) {
03519                                 $token = MWCryptRand::generateHex( 32 );
03520                                 $request->setSessionData( 'wsEditToken', $token );
03521                         }
03522                         if( is_array( $salt ) ) {
03523                                 $salt = implode( '|', $salt );
03524                         }
03525                         return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX;
03526                 }
03527         }
03528 
03535         public static function generateToken() {
03536                 return MWCryptRand::generateHex( 32 );
03537         }
03538 
03550         public function matchEditToken( $val, $salt = '', $request = null ) {
03551                 $sessionToken = $this->getEditToken( $salt, $request );
03552                 if ( $val != $sessionToken ) {
03553                         wfDebug( "User::matchEditToken: broken session data\n" );
03554                 }
03555                 return $val == $sessionToken;
03556         }
03557 
03567         public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
03568                 $sessionToken = $this->getEditToken( $salt, $request );
03569                 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
03570         }
03571 
03579         public function sendConfirmationMail( $type = 'created' ) {
03580                 global $wgLang;
03581                 $expiration = null; // gets passed-by-ref and defined in next line.
03582                 $token = $this->confirmationToken( $expiration );
03583                 $url = $this->confirmationTokenUrl( $token );
03584                 $invalidateURL = $this->invalidationTokenUrl( $token );
03585                 $this->saveSettings();
03586 
03587                 if ( $type == 'created' || $type === false ) {
03588                         $message = 'confirmemail_body';
03589                 } elseif ( $type === true ) {
03590                         $message = 'confirmemail_body_changed';
03591                 } else {
03592                         $message = 'confirmemail_body_' . $type;
03593                 }
03594 
03595                 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
03596                         wfMessage( $message,
03597                                 $this->getRequest()->getIP(),
03598                                 $this->getName(),
03599                                 $url,
03600                                 $wgLang->timeanddate( $expiration, false ),
03601                                 $invalidateURL,
03602                                 $wgLang->date( $expiration, false ),
03603                                 $wgLang->time( $expiration, false ) )->text() );
03604         }
03605 
03616         public function sendMail( $subject, $body, $from = null, $replyto = null ) {
03617                 if( is_null( $from ) ) {
03618                         global $wgPasswordSender, $wgPasswordSenderName;
03619                         $sender = new MailAddress( $wgPasswordSender, $wgPasswordSenderName );
03620                 } else {
03621                         $sender = new MailAddress( $from );
03622                 }
03623 
03624                 $to = new MailAddress( $this );
03625                 return UserMailer::send( $to, $sender, $subject, $body, $replyto );
03626         }
03627 
03638         private function confirmationToken( &$expiration ) {
03639                 global $wgUserEmailConfirmationTokenExpiry;
03640                 $now = time();
03641                 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
03642                 $expiration = wfTimestamp( TS_MW, $expires );
03643                 $this->load();
03644                 $token = MWCryptRand::generateHex( 32 );
03645                 $hash = md5( $token );
03646                 $this->mEmailToken = $hash;
03647                 $this->mEmailTokenExpires = $expiration;
03648                 return $token;
03649         }
03650 
03656         private function confirmationTokenUrl( $token ) {
03657                 return $this->getTokenUrl( 'ConfirmEmail', $token );
03658         }
03659 
03665         private function invalidationTokenUrl( $token ) {
03666                 return $this->getTokenUrl( 'InvalidateEmail', $token );
03667         }
03668 
03683         protected function getTokenUrl( $page, $token ) {
03684                 // Hack to bypass localization of 'Special:'
03685                 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
03686                 return $title->getCanonicalUrl();
03687         }
03688 
03696         public function confirmEmail() {
03697                 $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
03698                 wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
03699                 return true;
03700         }
03701 
03709         function invalidateEmail() {
03710                 $this->load();
03711                 $this->mEmailToken = null;
03712                 $this->mEmailTokenExpires = null;
03713                 $this->setEmailAuthenticationTimestamp( null );
03714                 wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
03715                 return true;
03716         }
03717 
03722         function setEmailAuthenticationTimestamp( $timestamp ) {
03723                 $this->load();
03724                 $this->mEmailAuthenticated = $timestamp;
03725                 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
03726         }
03727 
03733         public function canSendEmail() {
03734                 global $wgEnableEmail, $wgEnableUserEmail;
03735                 if( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
03736                         return false;
03737                 }
03738                 $canSend = $this->isEmailConfirmed();
03739                 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
03740                 return $canSend;
03741         }
03742 
03748         public function canReceiveEmail() {
03749                 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
03750         }
03751 
03762         public function isEmailConfirmed() {
03763                 global $wgEmailAuthentication;
03764                 $this->load();
03765                 $confirmed = true;
03766                 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
03767                         if( $this->isAnon() ) {
03768                                 return false;
03769                         }
03770                         if( !Sanitizer::validateEmail( $this->mEmail ) ) {
03771                                 return false;
03772                         }
03773                         if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
03774                                 return false;
03775                         }
03776                         return true;
03777                 } else {
03778                         return $confirmed;
03779                 }
03780         }
03781 
03786         public function isEmailConfirmationPending() {
03787                 global $wgEmailAuthentication;
03788                 return $wgEmailAuthentication &&
03789                         !$this->isEmailConfirmed() &&
03790                         $this->mEmailToken &&
03791                         $this->mEmailTokenExpires > wfTimestamp();
03792         }
03793 
03801         public function getRegistration() {
03802                 if ( $this->isAnon() ) {
03803                         return false;
03804                 }
03805                 $this->load();
03806                 return $this->mRegistration;
03807         }
03808 
03815         public function getFirstEditTimestamp() {
03816                 if( $this->getId() == 0 ) {
03817                         return false; // anons
03818                 }
03819                 $dbr = wfGetDB( DB_SLAVE );
03820                 $time = $dbr->selectField( 'revision', 'rev_timestamp',
03821                         array( 'rev_user' => $this->getId() ),
03822                         __METHOD__,
03823                         array( 'ORDER BY' => 'rev_timestamp ASC' )
03824                 );
03825                 if( !$time ) {
03826                         return false; // no edits
03827                 }
03828                 return wfTimestamp( TS_MW, $time );
03829         }
03830 
03837         public static function getGroupPermissions( $groups ) {
03838                 global $wgGroupPermissions, $wgRevokePermissions;
03839                 $rights = array();
03840                 // grant every granted permission first
03841                 foreach( $groups as $group ) {
03842                         if( isset( $wgGroupPermissions[$group] ) ) {
03843                                 $rights = array_merge( $rights,
03844                                         // array_filter removes empty items
03845                                         array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
03846                         }
03847                 }
03848                 // now revoke the revoked permissions
03849                 foreach( $groups as $group ) {
03850                         if( isset( $wgRevokePermissions[$group] ) ) {
03851                                 $rights = array_diff( $rights,
03852                                         array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
03853                         }
03854                 }
03855                 return array_unique( $rights );
03856         }
03857 
03864         public static function getGroupsWithPermission( $role ) {
03865                 global $wgGroupPermissions;
03866                 $allowedGroups = array();
03867                 foreach ( array_keys( $wgGroupPermissions ) as $group ) {
03868                         if ( self::groupHasPermission( $group, $role ) ) {
03869                                 $allowedGroups[] = $group;
03870                         }
03871                 }
03872                 return $allowedGroups;
03873         }
03874 
03882         public static function groupHasPermission( $group, $role ) {
03883                 global $wgGroupPermissions, $wgRevokePermissions;
03884                 return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
03885                         && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
03886         }
03887 
03894         public static function getGroupName( $group ) {
03895                 $msg = wfMessage( "group-$group" );
03896                 return $msg->isBlank() ? $group : $msg->text();
03897         }
03898 
03906         public static function getGroupMember( $group, $username = '#' ) {
03907                 $msg = wfMessage( "group-$group-member", $username );
03908                 return $msg->isBlank() ? $group : $msg->text();
03909         }
03910 
03917         public static function getAllGroups() {
03918                 global $wgGroupPermissions, $wgRevokePermissions;
03919                 return array_diff(
03920                         array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
03921                         self::getImplicitGroups()
03922                 );
03923         }
03924 
03929         public static function getAllRights() {
03930                 if ( self::$mAllRights === false ) {
03931                         global $wgAvailableRights;
03932                         if ( count( $wgAvailableRights ) ) {
03933                                 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
03934                         } else {
03935                                 self::$mAllRights = self::$mCoreRights;
03936                         }
03937                         wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
03938                 }
03939                 return self::$mAllRights;
03940         }
03941 
03946         public static function getImplicitGroups() {
03947                 global $wgImplicitGroups;
03948                 $groups = $wgImplicitGroups;
03949                 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );       #deprecated, use $wgImplictGroups instead
03950                 return $groups;
03951         }
03952 
03959         public static function getGroupPage( $group ) {
03960                 $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
03961                 if( $msg->exists() ) {
03962                         $title = Title::newFromText( $msg->text() );
03963                         if( is_object( $title ) )
03964                                 return $title;
03965                 }
03966                 return false;
03967         }
03968 
03977         public static function makeGroupLinkHTML( $group, $text = '' ) {
03978                 if( $text == '' ) {
03979                         $text = self::getGroupName( $group );
03980                 }
03981                 $title = self::getGroupPage( $group );
03982                 if( $title ) {
03983                         return Linker::link( $title, htmlspecialchars( $text ) );
03984                 } else {
03985                         return $text;
03986                 }
03987         }
03988 
03997         public static function makeGroupLinkWiki( $group, $text = '' ) {
03998                 if( $text == '' ) {
03999                         $text = self::getGroupName( $group );
04000                 }
04001                 $title = self::getGroupPage( $group );
04002                 if( $title ) {
04003                         $page = $title->getPrefixedText();
04004                         return "[[$page|$text]]";
04005                 } else {
04006                         return $text;
04007                 }
04008         }
04009 
04019         public static function changeableByGroup( $group ) {
04020                 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
04021 
04022                 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
04023                 if( empty( $wgAddGroups[$group] ) ) {
04024                         // Don't add anything to $groups
04025                 } elseif( $wgAddGroups[$group] === true ) {
04026                         // You get everything
04027                         $groups['add'] = self::getAllGroups();
04028                 } elseif( is_array( $wgAddGroups[$group] ) ) {
04029                         $groups['add'] = $wgAddGroups[$group];
04030                 }
04031 
04032                 // Same thing for remove
04033                 if( empty( $wgRemoveGroups[$group] ) ) {
04034                 } elseif( $wgRemoveGroups[$group] === true ) {
04035                         $groups['remove'] = self::getAllGroups();
04036                 } elseif( is_array( $wgRemoveGroups[$group] ) ) {
04037                         $groups['remove'] = $wgRemoveGroups[$group];
04038                 }
04039 
04040                 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
04041                 if( empty( $wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
04042                         foreach( $wgGroupsAddToSelf as $key => $value ) {
04043                                 if( is_int( $key ) ) {
04044                                         $wgGroupsAddToSelf['user'][] = $value;
04045                                 }
04046                         }
04047                 }
04048 
04049                 if( empty( $wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
04050                         foreach( $wgGroupsRemoveFromSelf as $key => $value ) {
04051                                 if( is_int( $key ) ) {
04052                                         $wgGroupsRemoveFromSelf['user'][] = $value;
04053                                 }
04054                         }
04055                 }
04056 
04057                 // Now figure out what groups the user can add to him/herself
04058                 if( empty( $wgGroupsAddToSelf[$group] ) ) {
04059                 } elseif( $wgGroupsAddToSelf[$group] === true ) {
04060                         // No idea WHY this would be used, but it's there
04061                         $groups['add-self'] = User::getAllGroups();
04062                 } elseif( is_array( $wgGroupsAddToSelf[$group] ) ) {
04063                         $groups['add-self'] = $wgGroupsAddToSelf[$group];
04064                 }
04065 
04066                 if( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
04067                 } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
04068                         $groups['remove-self'] = User::getAllGroups();
04069                 } elseif( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
04070                         $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
04071                 }
04072 
04073                 return $groups;
04074         }
04075 
04083         public function changeableGroups() {
04084                 if( $this->isAllowed( 'userrights' ) ) {
04085                         // This group gives the right to modify everything (reverse-
04086                         // compatibility with old "userrights lets you change
04087                         // everything")
04088                         // Using array_merge to make the groups reindexed
04089                         $all = array_merge( User::getAllGroups() );
04090                         return array(
04091                                 'add' => $all,
04092                                 'remove' => $all,
04093                                 'add-self' => array(),
04094                                 'remove-self' => array()
04095                         );
04096                 }
04097 
04098                 // Okay, it's not so simple, we will have to go through the arrays
04099                 $groups = array(
04100                         'add' => array(),
04101                         'remove' => array(),
04102                         'add-self' => array(),
04103                         'remove-self' => array()
04104                 );
04105                 $addergroups = $this->getEffectiveGroups();
04106 
04107                 foreach( $addergroups as $addergroup ) {
04108                         $groups = array_merge_recursive(
04109                                 $groups, $this->changeableByGroup( $addergroup )
04110                         );
04111                         $groups['add'] = array_unique( $groups['add'] );
04112                         $groups['remove'] = array_unique( $groups['remove'] );
04113                         $groups['add-self'] = array_unique( $groups['add-self'] );
04114                         $groups['remove-self'] = array_unique( $groups['remove-self'] );
04115                 }
04116                 return $groups;
04117         }
04118 
04123         public function incEditCount() {
04124                 if( !$this->isAnon() ) {
04125                         $dbw = wfGetDB( DB_MASTER );
04126                         $dbw->update(
04127                                 'user',
04128                                 array( 'user_editcount=user_editcount+1' ),
04129                                 array( 'user_id' => $this->getId() ),
04130                                 __METHOD__
04131                         );
04132 
04133                         // Lazy initialization check...
04134                         if( $dbw->affectedRows() == 0 ) {
04135                                 // Now here's a goddamn hack...
04136                                 $dbr = wfGetDB( DB_SLAVE );
04137                                 if( $dbr !== $dbw ) {
04138                                         // If we actually have a slave server, the count is
04139                                         // at least one behind because the current transaction
04140                                         // has not been committed and replicated.
04141                                         $this->initEditCount( 1 );
04142                                 } else {
04143                                         // But if DB_SLAVE is selecting the master, then the
04144                                         // count we just read includes the revision that was
04145                                         // just added in the working transaction.
04146                                         $this->initEditCount();
04147                                 }
04148                         }
04149                 }
04150                 // edit count in user cache too
04151                 $this->invalidateCache();
04152         }
04153 
04160         protected function initEditCount( $add = 0 ) {
04161                 // Pull from a slave to be less cruel to servers
04162                 // Accuracy isn't the point anyway here
04163                 $dbr = wfGetDB( DB_SLAVE );
04164                 $count = (int) $dbr->selectField(
04165                         'revision',
04166                         'COUNT(rev_user)',
04167                         array( 'rev_user' => $this->getId() ),
04168                         __METHOD__
04169                 );
04170                 $count = $count + $add;
04171 
04172                 $dbw = wfGetDB( DB_MASTER );
04173                 $dbw->update(
04174                         'user',
04175                         array( 'user_editcount' => $count ),
04176                         array( 'user_id' => $this->getId() ),
04177                         __METHOD__
04178                 );
04179 
04180                 return $count;
04181         }
04182 
04189         public static function getRightDescription( $right ) {
04190                 $key = "right-$right";
04191                 $msg = wfMessage( $key );
04192                 return $msg->isBlank() ? $right : $msg->text();
04193         }
04194 
04202         public static function oldCrypt( $password, $userId ) {
04203                 global $wgPasswordSalt;
04204                 if ( $wgPasswordSalt ) {
04205                         return md5( $userId . '-' . md5( $password ) );
04206                 } else {
04207                         return md5( $password );
04208                 }
04209         }
04210 
04220         public static function crypt( $password, $salt = false ) {
04221                 global $wgPasswordSalt;
04222 
04223                 $hash = '';
04224                 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
04225                         return $hash;
04226                 }
04227 
04228                 if( $wgPasswordSalt ) {
04229                         if ( $salt === false ) {
04230                                 $salt = MWCryptRand::generateHex( 8 );
04231                         }
04232                         return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
04233                 } else {
04234                         return ':A:' . md5( $password );
04235                 }
04236         }
04237 
04248         public static function comparePasswords( $hash, $password, $userId = false ) {
04249                 $type = substr( $hash, 0, 3 );
04250 
04251                 $result = false;
04252                 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
04253                         return $result;
04254                 }
04255 
04256                 if ( $type == ':A:' ) {
04257                         # Unsalted
04258                         return md5( $password ) === substr( $hash, 3 );
04259                 } elseif ( $type == ':B:' ) {
04260                         # Salted
04261                         list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 );
04262                         return md5( $salt.'-'.md5( $password ) ) === $realHash;
04263                 } else {
04264                         # Old-style
04265                         return self::oldCrypt( $password, $userId ) === $hash;
04266                 }
04267         }
04268 
04289         public function addNewUserLogEntry( $action = false, $reason = '' ) {
04290                 global $wgUser, $wgNewUserLog;
04291                 if( empty( $wgNewUserLog ) ) {
04292                         return true; // disabled
04293                 }
04294 
04295                 if ( $action === true ) {
04296                         $action = 'byemail';
04297                 } elseif ( $action === false ) {
04298                         if ( $this->getName() == $wgUser->getName() ) {
04299                                 $action = 'create';
04300                         } else {
04301                                 $action = 'create2';
04302                         }
04303                 }
04304 
04305                 if ( $action === 'create' || $action === 'autocreate' ) {
04306                         $performer = $this;
04307                 } else {
04308                         $performer = $wgUser;
04309                 }
04310 
04311                 $logEntry = new ManualLogEntry( 'newusers', $action );
04312                 $logEntry->setPerformer( $performer );
04313                 $logEntry->setTarget( $this->getUserPage() );
04314                 $logEntry->setComment( $reason );
04315                 $logEntry->setParameters( array(
04316                         '4::userid' => $this->getId(),
04317                 ) );
04318                 $logid = $logEntry->insert();
04319 
04320                 if ( $action !== 'autocreate' ) {
04321                         $logEntry->publish( $logid );
04322                 }
04323 
04324                 return (int)$logid;
04325         }
04326 
04334         public function addNewUserLogEntryAutoCreate() {
04335                 $this->addNewUserLogEntry( 'autocreate' );
04336 
04337                 return true;
04338         }
04339 
04345         protected function loadOptions( $data = null ) {
04346                 global $wgContLang;
04347 
04348                 $this->load();
04349 
04350                 if ( $this->mOptionsLoaded ) {
04351                         return;
04352                 }
04353 
04354                 $this->mOptions = self::getDefaultOptions();
04355 
04356                 if ( !$this->getId() ) {
04357                         // For unlogged-in users, load language/variant options from request.
04358                         // There's no need to do it for logged-in users: they can set preferences,
04359                         // and handling of page content is done by $pageLang->getPreferredVariant() and such,
04360                         // so don't override user's choice (especially when the user chooses site default).
04361                         $variant = $wgContLang->getDefaultVariant();
04362                         $this->mOptions['variant'] = $variant;
04363                         $this->mOptions['language'] = $variant;
04364                         $this->mOptionsLoaded = true;
04365                         return;
04366                 }
04367 
04368                 // Maybe load from the object
04369                 if ( !is_null( $this->mOptionOverrides ) ) {
04370                         wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
04371                         foreach( $this->mOptionOverrides as $key => $value ) {
04372                                 $this->mOptions[$key] = $value;
04373                         }
04374                 } else {
04375                         if( !is_array( $data ) ) {
04376                                 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
04377                                 // Load from database
04378                                 $dbr = wfGetDB( DB_SLAVE );
04379 
04380                                 $res = $dbr->select(
04381                                         'user_properties',
04382                                         array( 'up_property', 'up_value' ),
04383                                         array( 'up_user' => $this->getId() ),
04384                                         __METHOD__
04385                                 );
04386 
04387                                 $this->mOptionOverrides = array();
04388                                 $data = array();
04389                                 foreach ( $res as $row ) {
04390                                         $data[$row->up_property] = $row->up_value;
04391                                 }
04392                         }
04393                         foreach ( $data as $property => $value ) {
04394                                 $this->mOptionOverrides[$property] = $value;
04395                                 $this->mOptions[$property] = $value;
04396                         }
04397                 }
04398 
04399                 $this->mOptionsLoaded = true;
04400 
04401                 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
04402         }
04403 
04407         protected function saveOptions() {
04408                 global $wgAllowPrefChange;
04409 
04410                 $this->loadOptions();
04411 
04412                 // Not using getOptions(), to keep hidden preferences in database
04413                 $saveOptions = $this->mOptions;
04414 
04415                 // Allow hooks to abort, for instance to save to a global profile.
04416                 // Reset options to default state before saving.
04417                 if( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
04418                         return;
04419                 }
04420 
04421                 $extuser = ExternalUser::newFromUser( $this );
04422                 $userId = $this->getId();
04423                 $insert_rows = array();
04424                 foreach( $saveOptions as $key => $value ) {
04425                         # Don't bother storing default values
04426                         $defaultOption = self::getDefaultOption( $key );
04427                         if ( ( is_null( $defaultOption ) &&
04428                                         !( $value === false || is_null( $value ) ) ) ||
04429                                         $value != $defaultOption ) {
04430                                 $insert_rows[] = array(
04431                                                 'up_user' => $userId,
04432                                                 'up_property' => $key,
04433                                                 'up_value' => $value,
04434                                         );
04435                         }
04436                         if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
04437                                 switch ( $wgAllowPrefChange[$key] ) {
04438                                         case 'local':
04439                                         case 'message':
04440                                                 break;
04441                                         case 'semiglobal':
04442                                         case 'global':
04443                                                 $extuser->setPref( $key, $value );
04444                                 }
04445                         }
04446                 }
04447 
04448                 $dbw = wfGetDB( DB_MASTER );
04449                 $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ );
04450                 $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
04451         }
04452 
04477         public static function passwordChangeInputAttribs() {
04478                 global $wgMinimalPasswordLength;
04479 
04480                 if ( $wgMinimalPasswordLength == 0 ) {
04481                         return array();
04482                 }
04483 
04484                 # Note that the pattern requirement will always be satisfied if the
04485                 # input is empty, so we need required in all cases.
04486                 #
04487                 # @todo FIXME: Bug 23769: This needs to not claim the password is required
04488                 # if e-mail confirmation is being used.  Since HTML5 input validation
04489                 # is b0rked anyway in some browsers, just return nothing.  When it's
04490                 # re-enabled, fix this code to not output required for e-mail
04491                 # registration.
04492                 #$ret = array( 'required' );
04493                 $ret = array();
04494 
04495                 # We can't actually do this right now, because Opera 9.6 will print out
04496                 # the entered password visibly in its error message!  When other
04497                 # browsers add support for this attribute, or Opera fixes its support,
04498                 # we can add support with a version check to avoid doing this on Opera
04499                 # versions where it will be a problem.  Reported to Opera as
04500                 # DSK-262266, but they don't have a public bug tracker for us to follow.
04501                 /*
04502                 if ( $wgMinimalPasswordLength > 1 ) {
04503                         $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
04504                         $ret['title'] = wfMessage( 'passwordtooshort' )
04505                                 ->numParams( $wgMinimalPasswordLength )->text();
04506                 }
04507                 */
04508 
04509                 return $ret;
04510         }
04511 
04517         public static function selectFields() {
04518                 return array(
04519                         'user_id',
04520                         'user_name',
04521                         'user_real_name',
04522                         'user_password',
04523                         'user_newpassword',
04524                         'user_newpass_time',
04525                         'user_email',
04526                         'user_touched',
04527                         'user_token',
04528                         'user_email_authenticated',
04529                         'user_email_token',
04530                         'user_email_token_expires',
04531                         'user_registration',
04532                         'user_editcount',
04533                 );
04534         }
04535 }