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