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