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