MediaWiki
REL1_19
|
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 00074 static $mCacheVars = array( 00075 // user table 00076 'mId', 00077 'mName', 00078 'mRealName', 00079 'mPassword', 00080 'mNewpassword', 00081 'mNewpassTime', 00082 'mEmail', 00083 'mTouched', 00084 'mToken', 00085 'mEmailAuthenticated', 00086 'mEmailToken', 00087 'mEmailTokenExpires', 00088 'mRegistration', 00089 'mEditCount', 00090 // user_groups table 00091 'mGroups', 00092 // user_properties table 00093 'mOptionOverrides', 00094 ); 00095 00102 static $mCoreRights = array( 00103 'apihighlimits', 00104 'autoconfirmed', 00105 'autopatrol', 00106 'bigdelete', 00107 'block', 00108 'blockemail', 00109 'bot', 00110 'browsearchive', 00111 'createaccount', 00112 'createpage', 00113 'createtalk', 00114 'delete', 00115 'deletedhistory', 00116 'deletedtext', 00117 'deleterevision', 00118 'edit', 00119 'editinterface', 00120 'editusercssjs', #deprecated 00121 'editusercss', 00122 'edituserjs', 00123 'hideuser', 00124 'import', 00125 'importupload', 00126 'ipblock-exempt', 00127 'markbotedits', 00128 'mergehistory', 00129 'minoredit', 00130 'move', 00131 'movefile', 00132 'move-rootuserpages', 00133 'move-subpages', 00134 'nominornewtalk', 00135 'noratelimit', 00136 'override-export-depth', 00137 'patrol', 00138 'protect', 00139 'proxyunbannable', 00140 'purge', 00141 'read', 00142 'reupload', 00143 'reupload-shared', 00144 'rollback', 00145 'sendemail', 00146 'siteadmin', 00147 'suppressionlog', 00148 'suppressredirect', 00149 'suppressrevision', 00150 'unblockself', 00151 'undelete', 00152 'unwatchedpages', 00153 'upload', 00154 'upload_by_url', 00155 'userrights', 00156 'userrights-interwiki', 00157 'writeapi', 00158 ); 00162 static $mAllRights = false; 00163 00166 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, 00167 $mEmail, $mTouched, $mToken, $mEmailAuthenticated, 00168 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides, 00169 $mCookiePassword, $mEditCount, $mAllowUsertalk; 00171 00176 var $mOptionsLoaded; 00177 00181 private $mLoadedItems = array(); 00183 00193 var $mFrom; 00194 00198 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mRights, 00199 $mBlockreason, $mEffectiveGroups, $mImplicitGroups, $mFormerGroups, $mBlockedGlobally, 00200 $mLocked, $mHideName, $mOptions; 00201 00205 private $mRequest; 00206 00210 var $mBlock; 00211 00215 private $mBlockedFromCreateAccount = false; 00216 00217 static $idCacheByName = array(); 00218 00229 function __construct() { 00230 $this->clearInstanceCache( 'defaults' ); 00231 } 00232 00236 function __toString(){ 00237 return $this->getName(); 00238 } 00239 00243 public function load() { 00244 if ( $this->mLoadedItems === true ) { 00245 return; 00246 } 00247 wfProfileIn( __METHOD__ ); 00248 00249 # Set it now to avoid infinite recursion in accessors 00250 $this->mLoadedItems = true; 00251 00252 switch ( $this->mFrom ) { 00253 case 'defaults': 00254 $this->loadDefaults(); 00255 break; 00256 case 'name': 00257 $this->mId = self::idFromName( $this->mName ); 00258 if ( !$this->mId ) { 00259 # Nonexistent user placeholder object 00260 $this->loadDefaults( $this->mName ); 00261 } else { 00262 $this->loadFromId(); 00263 } 00264 break; 00265 case 'id': 00266 $this->loadFromId(); 00267 break; 00268 case 'session': 00269 $this->loadFromSession(); 00270 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) ); 00271 break; 00272 default: 00273 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" ); 00274 } 00275 wfProfileOut( __METHOD__ ); 00276 } 00277 00282 public function loadFromId() { 00283 global $wgMemc; 00284 if ( $this->mId == 0 ) { 00285 $this->loadDefaults(); 00286 return false; 00287 } 00288 00289 # Try cache 00290 $key = wfMemcKey( 'user', 'id', $this->mId ); 00291 $data = $wgMemc->get( $key ); 00292 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) { 00293 # Object is expired, load from DB 00294 $data = false; 00295 } 00296 00297 if ( !$data ) { 00298 wfDebug( "User: cache miss for user {$this->mId}\n" ); 00299 # Load from DB 00300 if ( !$this->loadFromDatabase() ) { 00301 # Can't load from ID, user is anonymous 00302 return false; 00303 } 00304 $this->saveToCache(); 00305 } else { 00306 wfDebug( "User: got user {$this->mId} from cache\n" ); 00307 # Restore from cache 00308 foreach ( self::$mCacheVars as $name ) { 00309 $this->$name = $data[$name]; 00310 } 00311 } 00312 return true; 00313 } 00314 00318 public function saveToCache() { 00319 $this->load(); 00320 $this->loadGroups(); 00321 $this->loadOptions(); 00322 if ( $this->isAnon() ) { 00323 // Anonymous users are uncached 00324 return; 00325 } 00326 $data = array(); 00327 foreach ( self::$mCacheVars as $name ) { 00328 $data[$name] = $this->$name; 00329 } 00330 $data['mVersion'] = MW_USER_VERSION; 00331 $key = wfMemcKey( 'user', 'id', $this->mId ); 00332 global $wgMemc; 00333 $wgMemc->set( $key, $data ); 00334 } 00335 00338 00355 public static function newFromName( $name, $validate = 'valid' ) { 00356 if ( $validate === true ) { 00357 $validate = 'valid'; 00358 } 00359 $name = self::getCanonicalName( $name, $validate ); 00360 if ( $name === false ) { 00361 return false; 00362 } else { 00363 # Create unloaded user object 00364 $u = new User; 00365 $u->mName = $name; 00366 $u->mFrom = 'name'; 00367 $u->setItemLoaded( 'name' ); 00368 return $u; 00369 } 00370 } 00371 00378 public static function newFromId( $id ) { 00379 $u = new User; 00380 $u->mId = $id; 00381 $u->mFrom = 'id'; 00382 $u->setItemLoaded( 'id' ); 00383 return $u; 00384 } 00385 00396 public static function newFromConfirmationCode( $code ) { 00397 $dbr = wfGetDB( DB_SLAVE ); 00398 $id = $dbr->selectField( 'user', 'user_id', array( 00399 'user_email_token' => md5( $code ), 00400 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ), 00401 ) ); 00402 if( $id !== false ) { 00403 return User::newFromId( $id ); 00404 } else { 00405 return null; 00406 } 00407 } 00408 00417 public static function newFromSession( WebRequest $request = null ) { 00418 $user = new User; 00419 $user->mFrom = 'session'; 00420 $user->mRequest = $request; 00421 return $user; 00422 } 00423 00437 public static function newFromRow( $row ) { 00438 $user = new User; 00439 $user->loadFromRow( $row ); 00440 return $user; 00441 } 00442 00444 00450 public static function whoIs( $id ) { 00451 $dbr = wfGetDB( DB_SLAVE ); 00452 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), __METHOD__ ); 00453 } 00454 00461 public static function whoIsReal( $id ) { 00462 $dbr = wfGetDB( DB_SLAVE ); 00463 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ ); 00464 } 00465 00471 public static function idFromName( $name ) { 00472 $nt = Title::makeTitleSafe( NS_USER, $name ); 00473 if( is_null( $nt ) ) { 00474 # Illegal name 00475 return null; 00476 } 00477 00478 if ( isset( self::$idCacheByName[$name] ) ) { 00479 return self::$idCacheByName[$name]; 00480 } 00481 00482 $dbr = wfGetDB( DB_SLAVE ); 00483 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ ); 00484 00485 if ( $s === false ) { 00486 $result = null; 00487 } else { 00488 $result = $s->user_id; 00489 } 00490 00491 self::$idCacheByName[$name] = $result; 00492 00493 if ( count( self::$idCacheByName ) > 1000 ) { 00494 self::$idCacheByName = array(); 00495 } 00496 00497 return $result; 00498 } 00499 00503 public static function resetIdByNameCache() { 00504 self::$idCacheByName = array(); 00505 } 00506 00523 public static function isIP( $name ) { 00524 return preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/',$name) || IP::isIPv6($name); 00525 } 00526 00538 public static function isValidUserName( $name ) { 00539 global $wgContLang, $wgMaxNameChars; 00540 00541 if ( $name == '' 00542 || User::isIP( $name ) 00543 || strpos( $name, '/' ) !== false 00544 || strlen( $name ) > $wgMaxNameChars 00545 || $name != $wgContLang->ucfirst( $name ) ) { 00546 wfDebugLog( 'username', __METHOD__ . 00547 ": '$name' invalid due to empty, IP, slash, length, or lowercase" ); 00548 return false; 00549 } 00550 00551 00552 // Ensure that the name can't be misresolved as a different title, 00553 // such as with extra namespace keys at the start. 00554 $parsed = Title::newFromText( $name ); 00555 if( is_null( $parsed ) 00556 || $parsed->getNamespace() 00557 || strcmp( $name, $parsed->getPrefixedText() ) ) { 00558 wfDebugLog( 'username', __METHOD__ . 00559 ": '$name' invalid due to ambiguous prefixes" ); 00560 return false; 00561 } 00562 00563 // Check an additional blacklist of troublemaker characters. 00564 // Should these be merged into the title char list? 00565 $unicodeBlacklist = '/[' . 00566 '\x{0080}-\x{009f}' . # iso-8859-1 control chars 00567 '\x{00a0}' . # non-breaking space 00568 '\x{2000}-\x{200f}' . # various whitespace 00569 '\x{2028}-\x{202f}' . # breaks and control chars 00570 '\x{3000}' . # ideographic space 00571 '\x{e000}-\x{f8ff}' . # private use 00572 ']/u'; 00573 if( preg_match( $unicodeBlacklist, $name ) ) { 00574 wfDebugLog( 'username', __METHOD__ . 00575 ": '$name' invalid due to blacklisted characters" ); 00576 return false; 00577 } 00578 00579 return true; 00580 } 00581 00593 public static function isUsableName( $name ) { 00594 global $wgReservedUsernames; 00595 // Must be a valid username, obviously ;) 00596 if ( !self::isValidUserName( $name ) ) { 00597 return false; 00598 } 00599 00600 static $reservedUsernames = false; 00601 if ( !$reservedUsernames ) { 00602 $reservedUsernames = $wgReservedUsernames; 00603 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) ); 00604 } 00605 00606 // Certain names may be reserved for batch processes. 00607 foreach ( $reservedUsernames as $reserved ) { 00608 if ( substr( $reserved, 0, 4 ) == 'msg:' ) { 00609 $reserved = wfMsgForContent( substr( $reserved, 4 ) ); 00610 } 00611 if ( $reserved == $name ) { 00612 return false; 00613 } 00614 } 00615 return true; 00616 } 00617 00630 public static function isCreatableName( $name ) { 00631 global $wgInvalidUsernameCharacters; 00632 00633 // Ensure that the username isn't longer than 235 bytes, so that 00634 // (at least for the builtin skins) user javascript and css files 00635 // will work. (bug 23080) 00636 if( strlen( $name ) > 235 ) { 00637 wfDebugLog( 'username', __METHOD__ . 00638 ": '$name' invalid due to length" ); 00639 return false; 00640 } 00641 00642 // Preg yells if you try to give it an empty string 00643 if( $wgInvalidUsernameCharacters !== '' ) { 00644 if( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) { 00645 wfDebugLog( 'username', __METHOD__ . 00646 ": '$name' invalid due to wgInvalidUsernameCharacters" ); 00647 return false; 00648 } 00649 } 00650 00651 return self::isUsableName( $name ); 00652 } 00653 00660 public function isValidPassword( $password ) { 00661 //simple boolean wrapper for getPasswordValidity 00662 return $this->getPasswordValidity( $password ) === true; 00663 } 00664 00671 public function getPasswordValidity( $password ) { 00672 global $wgMinimalPasswordLength, $wgContLang; 00673 00674 static $blockedLogins = array( 00675 'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589 00676 'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605 00677 ); 00678 00679 $result = false; //init $result to false for the internal checks 00680 00681 if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) 00682 return $result; 00683 00684 if ( $result === false ) { 00685 if( strlen( $password ) < $wgMinimalPasswordLength ) { 00686 return 'passwordtooshort'; 00687 } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) { 00688 return 'password-name-match'; 00689 } elseif ( isset( $blockedLogins[ $this->getName() ] ) && $password == $blockedLogins[ $this->getName() ] ) { 00690 return 'password-login-forbidden'; 00691 } else { 00692 //it seems weird returning true here, but this is because of the 00693 //initialization of $result to false above. If the hook is never run or it 00694 //doesn't modify $result, then we will likely get down into this if with 00695 //a valid password. 00696 return true; 00697 } 00698 } elseif( $result === true ) { 00699 return true; 00700 } else { 00701 return $result; //the isValidPassword hook set a string $result and returned true 00702 } 00703 } 00704 00732 public static function isValidEmailAddr( $addr ) { 00733 wfDeprecated( __METHOD__, '1.18' ); 00734 return Sanitizer::validateEmail( $addr ); 00735 } 00736 00749 public static function getCanonicalName( $name, $validate = 'valid' ) { 00750 # Force usernames to capital 00751 global $wgContLang; 00752 $name = $wgContLang->ucfirst( $name ); 00753 00754 # Reject names containing '#'; these will be cleaned up 00755 # with title normalisation, but then it's too late to 00756 # check elsewhere 00757 if( strpos( $name, '#' ) !== false ) 00758 return false; 00759 00760 # Clean up name according to title rules 00761 $t = ( $validate === 'valid' ) ? 00762 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name ); 00763 # Check for invalid titles 00764 if( is_null( $t ) ) { 00765 return false; 00766 } 00767 00768 # Reject various classes of invalid names 00769 global $wgAuth; 00770 $name = $wgAuth->getCanonicalName( $t->getText() ); 00771 00772 switch ( $validate ) { 00773 case false: 00774 break; 00775 case 'valid': 00776 if ( !User::isValidUserName( $name ) ) { 00777 $name = false; 00778 } 00779 break; 00780 case 'usable': 00781 if ( !User::isUsableName( $name ) ) { 00782 $name = false; 00783 } 00784 break; 00785 case 'creatable': 00786 if ( !User::isCreatableName( $name ) ) { 00787 $name = false; 00788 } 00789 break; 00790 default: 00791 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ ); 00792 } 00793 return $name; 00794 } 00795 00803 public static function edits( $uid ) { 00804 wfProfileIn( __METHOD__ ); 00805 $dbr = wfGetDB( DB_SLAVE ); 00806 // check if the user_editcount field has been initialized 00807 $field = $dbr->selectField( 00808 'user', 'user_editcount', 00809 array( 'user_id' => $uid ), 00810 __METHOD__ 00811 ); 00812 00813 if( $field === null ) { // it has not been initialized. do so. 00814 $dbw = wfGetDB( DB_MASTER ); 00815 $count = $dbr->selectField( 00816 'revision', 'count(*)', 00817 array( 'rev_user' => $uid ), 00818 __METHOD__ 00819 ); 00820 $dbw->update( 00821 'user', 00822 array( 'user_editcount' => $count ), 00823 array( 'user_id' => $uid ), 00824 __METHOD__ 00825 ); 00826 } else { 00827 $count = $field; 00828 } 00829 wfProfileOut( __METHOD__ ); 00830 return $count; 00831 } 00832 00838 public static function randomPassword() { 00839 global $wgMinimalPasswordLength; 00840 // Decide the final password length based on our min password length, stopping at a minimum of 10 chars 00841 $length = max( 10, $wgMinimalPasswordLength ); 00842 // Multiply by 1.25 to get the number of hex characters we need 00843 $length = $length * 1.25; 00844 // Generate random hex chars 00845 $hex = MWCryptRand::generateHex( $length ); 00846 // Convert from base 16 to base 32 to get a proper password like string 00847 return wfBaseConvert( $hex, 16, 32 ); 00848 } 00849 00858 public function loadDefaults( $name = false ) { 00859 wfProfileIn( __METHOD__ ); 00860 00861 $this->mId = 0; 00862 $this->mName = $name; 00863 $this->mRealName = ''; 00864 $this->mPassword = $this->mNewpassword = ''; 00865 $this->mNewpassTime = null; 00866 $this->mEmail = ''; 00867 $this->mOptionOverrides = null; 00868 $this->mOptionsLoaded = false; 00869 00870 $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' ); 00871 if( $loggedOut !== null ) { 00872 $this->mTouched = wfTimestamp( TS_MW, $loggedOut ); 00873 } else { 00874 $this->mTouched = '0'; # Allow any pages to be cached 00875 } 00876 00877 $this->mToken = null; // Don't run cryptographic functions till we need a token 00878 $this->mEmailAuthenticated = null; 00879 $this->mEmailToken = ''; 00880 $this->mEmailTokenExpires = null; 00881 $this->mRegistration = wfTimestamp( TS_MW ); 00882 $this->mGroups = array(); 00883 00884 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) ); 00885 00886 wfProfileOut( __METHOD__ ); 00887 } 00888 00901 public function isItemLoaded( $item, $all = 'all' ) { 00902 return ( $this->mLoadedItems === true && $all === 'all' ) || 00903 ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true ); 00904 } 00905 00911 private function setItemLoaded( $item ) { 00912 if ( is_array( $this->mLoadedItems ) ) { 00913 $this->mLoadedItems[$item] = true; 00914 } 00915 } 00916 00922 private function loadFromSession() { 00923 global $wgExternalAuthType, $wgAutocreatePolicy; 00924 00925 $result = null; 00926 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) ); 00927 if ( $result !== null ) { 00928 return $result; 00929 } 00930 00931 if ( $wgExternalAuthType && $wgAutocreatePolicy == 'view' ) { 00932 $extUser = ExternalUser::newFromCookie(); 00933 if ( $extUser ) { 00934 # TODO: Automatically create the user here (or probably a bit 00935 # lower down, in fact) 00936 } 00937 } 00938 00939 $request = $this->getRequest(); 00940 00941 $cookieId = $request->getCookie( 'UserID' ); 00942 $sessId = $request->getSessionData( 'wsUserID' ); 00943 00944 if ( $cookieId !== null ) { 00945 $sId = intval( $cookieId ); 00946 if( $sessId !== null && $cookieId != $sessId ) { 00947 $this->loadDefaults(); // Possible collision! 00948 wfDebugLog( 'loginSessions', "Session user ID ($sessId) and 00949 cookie user ID ($sId) don't match!" ); 00950 return false; 00951 } 00952 $request->setSessionData( 'wsUserID', $sId ); 00953 } elseif ( $sessId !== null && $sessId != 0 ) { 00954 $sId = $sessId; 00955 } else { 00956 $this->loadDefaults(); 00957 return false; 00958 } 00959 00960 if ( $request->getSessionData( 'wsUserName' ) !== null ) { 00961 $sName = $request->getSessionData( 'wsUserName' ); 00962 } elseif ( $request->getCookie( 'UserName' ) !== null ) { 00963 $sName = $request->getCookie( 'UserName' ); 00964 $request->setSessionData( 'wsUserName', $sName ); 00965 } else { 00966 $this->loadDefaults(); 00967 return false; 00968 } 00969 00970 $proposedUser = User::newFromId( $sId ); 00971 if ( !$proposedUser->isLoggedIn() ) { 00972 # Not a valid ID 00973 $this->loadDefaults(); 00974 return false; 00975 } 00976 00977 global $wgBlockDisablesLogin; 00978 if( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) { 00979 # User blocked and we've disabled blocked user logins 00980 $this->loadDefaults(); 00981 return false; 00982 } 00983 00984 if ( $request->getSessionData( 'wsToken' ) ) { 00985 $passwordCorrect = $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ); 00986 $from = 'session'; 00987 } elseif ( $request->getCookie( 'Token' ) ) { 00988 # Get the token from DB/cache and clean it up to remove garbage padding. 00989 # This deals with historical problems with bugs and the default column value. 00990 $token = rtrim( $proposedUser->getToken( false ) ); // correct token 00991 // Make comparison in constant time (bug 61346) 00992 $passwordCorrect = strlen( $token ) && $this->compareSecrets( $token, $request->getCookie( 'Token' ) ); 00993 $from = 'cookie'; 00994 } else { 00995 # No session or persistent login cookie 00996 $this->loadDefaults(); 00997 return false; 00998 } 00999 01000 if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) { 01001 $this->loadFromUserObject( $proposedUser ); 01002 $request->setSessionData( 'wsToken', $this->mToken ); 01003 wfDebug( "User: logged in from $from\n" ); 01004 return true; 01005 } else { 01006 # Invalid credentials 01007 wfDebug( "User: can't log in from $from, invalid credentials\n" ); 01008 $this->loadDefaults(); 01009 return false; 01010 } 01011 } 01012 01019 protected function compareSecrets( $answer, $test ) { 01020 if ( strlen( $answer ) !== strlen( $test ) ) { 01021 $passwordCorrect = false; 01022 } else { 01023 $result = 0; 01024 for ( $i = 0; $i < strlen( $answer ); $i++ ) { 01025 $result |= ord( $answer{$i} ) ^ ord( $test{$i} ); 01026 } 01027 $passwordCorrect = ( $result == 0 ); 01028 } 01029 return $passwordCorrect; 01030 } 01031 01038 public function loadFromDatabase() { 01039 # Paranoia 01040 $this->mId = intval( $this->mId ); 01041 01043 if( !$this->mId ) { 01044 $this->loadDefaults(); 01045 return false; 01046 } 01047 01048 $dbr = wfGetDB( DB_MASTER ); 01049 $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ ); 01050 01051 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) ); 01052 01053 if ( $s !== false ) { 01054 # Initialise user table data 01055 $this->loadFromRow( $s ); 01056 $this->mGroups = null; // deferred 01057 $this->getEditCount(); // revalidation for nulls 01058 return true; 01059 } else { 01060 # Invalid user_id 01061 $this->mId = 0; 01062 $this->loadDefaults(); 01063 return false; 01064 } 01065 } 01066 01072 public function loadFromRow( $row ) { 01073 $all = true; 01074 01075 $this->mGroups = null; // deferred 01076 01077 if ( isset( $row->user_name ) ) { 01078 $this->mName = $row->user_name; 01079 $this->mFrom = 'name'; 01080 $this->setItemLoaded( 'name' ); 01081 } else { 01082 $all = false; 01083 } 01084 01085 if ( isset( $row->user_real_name ) ) { 01086 $this->mRealName = $row->user_real_name; 01087 $this->setItemLoaded( 'realname' ); 01088 } else { 01089 $all = false; 01090 } 01091 01092 if ( isset( $row->user_id ) ) { 01093 $this->mId = intval( $row->user_id ); 01094 $this->mFrom = 'id'; 01095 $this->setItemLoaded( 'id' ); 01096 } else { 01097 $all = false; 01098 } 01099 01100 if ( isset( $row->user_editcount ) ) { 01101 $this->mEditCount = $row->user_editcount; 01102 } else { 01103 $all = false; 01104 } 01105 01106 if ( isset( $row->user_password ) ) { 01107 $this->mPassword = $row->user_password; 01108 $this->mNewpassword = $row->user_newpassword; 01109 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time ); 01110 $this->mEmail = $row->user_email; 01111 if ( isset( $row->user_options ) ) { 01112 $this->decodeOptions( $row->user_options ); 01113 } 01114 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched ); 01115 $this->mToken = $row->user_token; 01116 if ( $this->mToken == '' ) { 01117 $this->mToken = null; 01118 } 01119 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); 01120 $this->mEmailToken = $row->user_email_token; 01121 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); 01122 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration ); 01123 } else { 01124 $all = false; 01125 } 01126 01127 if ( $all ) { 01128 $this->mLoadedItems = true; 01129 } 01130 } 01131 01137 protected function loadFromUserObject( $user ) { 01138 $user->load(); 01139 $user->loadGroups(); 01140 $user->loadOptions(); 01141 foreach ( self::$mCacheVars as $var ) { 01142 $this->$var = $user->$var; 01143 } 01144 } 01145 01149 private function loadGroups() { 01150 if ( is_null( $this->mGroups ) ) { 01151 $dbr = wfGetDB( DB_MASTER ); 01152 $res = $dbr->select( 'user_groups', 01153 array( 'ug_group' ), 01154 array( 'ug_user' => $this->mId ), 01155 __METHOD__ ); 01156 $this->mGroups = array(); 01157 foreach ( $res as $row ) { 01158 $this->mGroups[] = $row->ug_group; 01159 } 01160 } 01161 } 01162 01177 public function addAutopromoteOnceGroups( $event ) { 01178 global $wgAutopromoteOnceLogInRC; 01179 01180 $toPromote = array(); 01181 if ( $this->getId() ) { 01182 $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event ); 01183 if ( count( $toPromote ) ) { 01184 $oldGroups = $this->getGroups(); // previous groups 01185 foreach ( $toPromote as $group ) { 01186 $this->addGroup( $group ); 01187 } 01188 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups 01189 01190 $log = new LogPage( 'rights', $wgAutopromoteOnceLogInRC /* in RC? */ ); 01191 $log->addEntry( 'autopromote', 01192 $this->getUserPage(), 01193 '', // no comment 01194 // These group names are "list to texted"-ed in class LogPage. 01195 array( implode( ', ', $oldGroups ), implode( ', ', $newGroups ) ) 01196 ); 01197 } 01198 } 01199 return $toPromote; 01200 } 01201 01208 public function clearInstanceCache( $reloadFrom = false ) { 01209 $this->mNewtalk = -1; 01210 $this->mDatePreference = null; 01211 $this->mBlockedby = -1; # Unset 01212 $this->mHash = false; 01213 $this->mRights = null; 01214 $this->mEffectiveGroups = null; 01215 $this->mImplicitGroups = null; 01216 $this->mOptions = null; 01217 01218 if ( $reloadFrom ) { 01219 $this->mLoadedItems = array(); 01220 $this->mFrom = $reloadFrom; 01221 } 01222 } 01223 01230 public static function getDefaultOptions() { 01231 global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin; 01232 01233 $defOpt = $wgDefaultUserOptions; 01234 # default language setting 01235 $variant = $wgContLang->getDefaultVariant(); 01236 $defOpt['variant'] = $variant; 01237 $defOpt['language'] = $variant; 01238 foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) { 01239 $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] ); 01240 } 01241 $defOpt['skin'] = $wgDefaultSkin; 01242 01243 // FIXME: Ideally we'd cache the results of this function so the hook is only run once, 01244 // but that breaks the parser tests because they rely on being able to change $wgContLang 01245 // mid-request and see that change reflected in the return value of this function. 01246 // Which is insane and would never happen during normal MW operation, but is also not 01247 // likely to get fixed unless and until we context-ify everything. 01248 // See also https://www.mediawiki.org/wiki/Special:Code/MediaWiki/101488#c25275 01249 wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) ); 01250 01251 return $defOpt; 01252 } 01253 01260 public static function getDefaultOption( $opt ) { 01261 $defOpts = self::getDefaultOptions(); 01262 if( isset( $defOpts[$opt] ) ) { 01263 return $defOpts[$opt]; 01264 } else { 01265 return null; 01266 } 01267 } 01268 01269 01277 private function getBlockedStatus( $bFromSlave = true ) { 01278 global $wgProxyWhitelist, $wgUser; 01279 01280 if ( -1 != $this->mBlockedby ) { 01281 return; 01282 } 01283 01284 wfProfileIn( __METHOD__ ); 01285 wfDebug( __METHOD__.": checking...\n" ); 01286 01287 // Initialize data... 01288 // Otherwise something ends up stomping on $this->mBlockedby when 01289 // things get lazy-loaded later, causing false positive block hits 01290 // due to -1 !== 0. Probably session-related... Nothing should be 01291 // overwriting mBlockedby, surely? 01292 $this->load(); 01293 01294 # We only need to worry about passing the IP address to the Block generator if the 01295 # user is not immune to autoblocks/hardblocks, and they are the current user so we 01296 # know which IP address they're actually coming from 01297 if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) { 01298 $ip = $this->getRequest()->getIP(); 01299 } else { 01300 $ip = null; 01301 } 01302 01303 # User/IP blocking 01304 $block = Block::newFromTarget( $this->getName(), $ip, !$bFromSlave ); 01305 01306 # Proxy blocking 01307 if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' ) 01308 && !in_array( $ip, $wgProxyWhitelist ) ) 01309 { 01310 # Local list 01311 if ( self::isLocallyBlockedProxy( $ip ) ) { 01312 $block = new Block; 01313 $block->setBlocker( wfMsg( 'proxyblocker' ) ); 01314 $block->mReason = wfMsg( 'proxyblockreason' ); 01315 $block->setTarget( $ip ); 01316 } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) { 01317 $block = new Block; 01318 $block->setBlocker( wfMsg( 'sorbs' ) ); 01319 $block->mReason = wfMsg( 'sorbsreason' ); 01320 $block->setTarget( $ip ); 01321 } 01322 } 01323 01324 if ( $block instanceof Block ) { 01325 wfDebug( __METHOD__ . ": Found block.\n" ); 01326 $this->mBlock = $block; 01327 $this->mBlockedby = $block->getByName(); 01328 $this->mBlockreason = $block->mReason; 01329 $this->mHideName = $block->mHideName; 01330 $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' ); 01331 } else { 01332 $this->mBlockedby = ''; 01333 $this->mHideName = 0; 01334 $this->mAllowUsertalk = false; 01335 } 01336 01337 # Extensions 01338 wfRunHooks( 'GetBlockedStatus', array( &$this ) ); 01339 01340 wfProfileOut( __METHOD__ ); 01341 } 01342 01350 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) { 01351 global $wgEnableSorbs, $wgEnableDnsBlacklist, 01352 $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist; 01353 01354 if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs ) 01355 return false; 01356 01357 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) 01358 return false; 01359 01360 $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl ); 01361 return $this->inDnsBlacklist( $ip, $urls ); 01362 } 01363 01371 public function inDnsBlacklist( $ip, $bases ) { 01372 wfProfileIn( __METHOD__ ); 01373 01374 $found = false; 01375 // @todo FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170) 01376 if( IP::isIPv4( $ip ) ) { 01377 # Reverse IP, bug 21255 01378 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) ); 01379 01380 foreach( (array)$bases as $base ) { 01381 # Make hostname 01382 # If we have an access key, use that too (ProjectHoneypot, etc.) 01383 if( is_array( $base ) ) { 01384 if( count( $base ) >= 2 ) { 01385 # Access key is 1, base URL is 0 01386 $host = "{$base[1]}.$ipReversed.{$base[0]}"; 01387 } else { 01388 $host = "$ipReversed.{$base[0]}"; 01389 } 01390 } else { 01391 $host = "$ipReversed.$base"; 01392 } 01393 01394 # Send query 01395 $ipList = gethostbynamel( $host ); 01396 01397 if( $ipList ) { 01398 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" ); 01399 $found = true; 01400 break; 01401 } else { 01402 wfDebug( "Requested $host, not found in $base.\n" ); 01403 } 01404 } 01405 } 01406 01407 wfProfileOut( __METHOD__ ); 01408 return $found; 01409 } 01410 01418 public static function isLocallyBlockedProxy( $ip ) { 01419 global $wgProxyList; 01420 01421 if ( !$wgProxyList ) { 01422 return false; 01423 } 01424 wfProfileIn( __METHOD__ ); 01425 01426 if ( !is_array( $wgProxyList ) ) { 01427 # Load from the specified file 01428 $wgProxyList = array_map( 'trim', file( $wgProxyList ) ); 01429 } 01430 01431 if ( !is_array( $wgProxyList ) ) { 01432 $ret = false; 01433 } elseif ( array_search( $ip, $wgProxyList ) !== false ) { 01434 $ret = true; 01435 } elseif ( array_key_exists( $ip, $wgProxyList ) ) { 01436 # Old-style flipped proxy list 01437 $ret = true; 01438 } else { 01439 $ret = false; 01440 } 01441 wfProfileOut( __METHOD__ ); 01442 return $ret; 01443 } 01444 01450 public function isPingLimitable() { 01451 global $wgRateLimitsExcludedIPs; 01452 if( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) { 01453 // No other good way currently to disable rate limits 01454 // for specific IPs. :P 01455 // But this is a crappy hack and should die. 01456 return false; 01457 } 01458 return !$this->isAllowed('noratelimit'); 01459 } 01460 01471 public function pingLimiter( $action = 'edit' ) { 01472 # Call the 'PingLimiter' hook 01473 $result = false; 01474 if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) { 01475 return $result; 01476 } 01477 01478 global $wgRateLimits; 01479 if( !isset( $wgRateLimits[$action] ) ) { 01480 return false; 01481 } 01482 01483 # Some groups shouldn't trigger the ping limiter, ever 01484 if( !$this->isPingLimitable() ) 01485 return false; 01486 01487 global $wgMemc, $wgRateLimitLog; 01488 wfProfileIn( __METHOD__ ); 01489 01490 $limits = $wgRateLimits[$action]; 01491 $keys = array(); 01492 $id = $this->getId(); 01493 $ip = $this->getRequest()->getIP(); 01494 $userLimit = false; 01495 01496 if( isset( $limits['anon'] ) && $id == 0 ) { 01497 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon']; 01498 } 01499 01500 if( isset( $limits['user'] ) && $id != 0 ) { 01501 $userLimit = $limits['user']; 01502 } 01503 if( $this->isNewbie() ) { 01504 if( isset( $limits['newbie'] ) && $id != 0 ) { 01505 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie']; 01506 } 01507 if( isset( $limits['ip'] ) ) { 01508 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip']; 01509 } 01510 $matches = array(); 01511 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) { 01512 $subnet = $matches[1]; 01513 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet']; 01514 } 01515 } 01516 // Check for group-specific permissions 01517 // If more than one group applies, use the group with the highest limit 01518 foreach ( $this->getGroups() as $group ) { 01519 if ( isset( $limits[$group] ) ) { 01520 if ( $userLimit === false || $limits[$group] > $userLimit ) { 01521 $userLimit = $limits[$group]; 01522 } 01523 } 01524 } 01525 // Set the user limit key 01526 if ( $userLimit !== false ) { 01527 wfDebug( __METHOD__ . ": effective user limit: $userLimit\n" ); 01528 $keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit; 01529 } 01530 01531 $triggered = false; 01532 foreach( $keys as $key => $limit ) { 01533 list( $max, $period ) = $limit; 01534 $summary = "(limit $max in {$period}s)"; 01535 $count = $wgMemc->get( $key ); 01536 // Already pinged? 01537 if( $count ) { 01538 if( $count > $max ) { 01539 wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" ); 01540 if( $wgRateLimitLog ) { 01541 wfSuppressWarnings(); 01542 file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND ); 01543 wfRestoreWarnings(); 01544 } 01545 $triggered = true; 01546 } else { 01547 wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" ); 01548 } 01549 } else { 01550 wfDebug( __METHOD__ . ": adding record for $key $summary\n" ); 01551 $wgMemc->add( $key, 0, intval( $period ) ); // first ping 01552 } 01553 $wgMemc->incr( $key ); 01554 } 01555 01556 wfProfileOut( __METHOD__ ); 01557 return $triggered; 01558 } 01559 01566 public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site 01567 return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' ); 01568 } 01569 01576 public function getBlock( $bFromSlave = true ){ 01577 $this->getBlockedStatus( $bFromSlave ); 01578 return $this->mBlock instanceof Block ? $this->mBlock : null; 01579 } 01580 01588 function isBlockedFrom( $title, $bFromSlave = false ) { 01589 global $wgBlockAllowsUTEdit; 01590 wfProfileIn( __METHOD__ ); 01591 01592 $blocked = $this->isBlocked( $bFromSlave ); 01593 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false ); 01594 # If a user's name is suppressed, they cannot make edits anywhere 01595 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() && 01596 $title->getNamespace() == NS_USER_TALK ) { 01597 $blocked = false; 01598 wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" ); 01599 } 01600 01601 wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) ); 01602 01603 wfProfileOut( __METHOD__ ); 01604 return $blocked; 01605 } 01606 01611 public function blockedBy() { 01612 $this->getBlockedStatus(); 01613 return $this->mBlockedby; 01614 } 01615 01620 public function blockedFor() { 01621 $this->getBlockedStatus(); 01622 return $this->mBlockreason; 01623 } 01624 01629 public function getBlockId() { 01630 $this->getBlockedStatus(); 01631 return ( $this->mBlock ? $this->mBlock->getId() : false ); 01632 } 01633 01642 public function isBlockedGlobally( $ip = '' ) { 01643 if( $this->mBlockedGlobally !== null ) { 01644 return $this->mBlockedGlobally; 01645 } 01646 // User is already an IP? 01647 if( IP::isIPAddress( $this->getName() ) ) { 01648 $ip = $this->getName(); 01649 } elseif( !$ip ) { 01650 $ip = $this->getRequest()->getIP(); 01651 } 01652 $blocked = false; 01653 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) ); 01654 $this->mBlockedGlobally = (bool)$blocked; 01655 return $this->mBlockedGlobally; 01656 } 01657 01663 public function isLocked() { 01664 if( $this->mLocked !== null ) { 01665 return $this->mLocked; 01666 } 01667 global $wgAuth; 01668 $authUser = $wgAuth->getUserInstance( $this ); 01669 $this->mLocked = (bool)$authUser->isLocked(); 01670 return $this->mLocked; 01671 } 01672 01678 public function isHidden() { 01679 if( $this->mHideName !== null ) { 01680 return $this->mHideName; 01681 } 01682 $this->getBlockedStatus(); 01683 if( !$this->mHideName ) { 01684 global $wgAuth; 01685 $authUser = $wgAuth->getUserInstance( $this ); 01686 $this->mHideName = (bool)$authUser->isHidden(); 01687 } 01688 return $this->mHideName; 01689 } 01690 01695 public function getId() { 01696 if( $this->mId === null && $this->mName !== null 01697 && User::isIP( $this->mName ) ) { 01698 // Special case, we know the user is anonymous 01699 return 0; 01700 } elseif( !$this->isItemLoaded( 'id' ) ) { 01701 // Don't load if this was initialized from an ID 01702 $this->load(); 01703 } 01704 return $this->mId; 01705 } 01706 01711 public function setId( $v ) { 01712 $this->mId = $v; 01713 $this->clearInstanceCache( 'id' ); 01714 } 01715 01720 public function getName() { 01721 if ( $this->isItemLoaded( 'name', 'only' ) ) { 01722 # Special case optimisation 01723 return $this->mName; 01724 } else { 01725 $this->load(); 01726 if ( $this->mName === false ) { 01727 # Clean up IPs 01728 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() ); 01729 } 01730 return $this->mName; 01731 } 01732 } 01733 01747 public function setName( $str ) { 01748 $this->load(); 01749 $this->mName = $str; 01750 } 01751 01756 public function getTitleKey() { 01757 return str_replace( ' ', '_', $this->getName() ); 01758 } 01759 01764 public function getNewtalk() { 01765 $this->load(); 01766 01767 # Load the newtalk status if it is unloaded (mNewtalk=-1) 01768 if( $this->mNewtalk === -1 ) { 01769 $this->mNewtalk = false; # reset talk page status 01770 01771 # Check memcached separately for anons, who have no 01772 # entire User object stored in there. 01773 if( !$this->mId ) { 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 } else { 01786 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId ); 01787 } 01788 } 01789 01790 return (bool)$this->mNewtalk; 01791 } 01792 01797 public function getNewMessageLinks() { 01798 $talks = array(); 01799 if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) 01800 return $talks; 01801 01802 if( !$this->getNewtalk() ) 01803 return array(); 01804 $up = $this->getUserPage(); 01805 $utp = $up->getTalkPage(); 01806 return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL() ) ); 01807 } 01808 01818 protected function checkNewtalk( $field, $id, $fromMaster = false ) { 01819 if ( $fromMaster ) { 01820 $db = wfGetDB( DB_MASTER ); 01821 } else { 01822 $db = wfGetDB( DB_SLAVE ); 01823 } 01824 $ok = $db->selectField( 'user_newtalk', $field, 01825 array( $field => $id ), __METHOD__ ); 01826 return $ok !== false; 01827 } 01828 01835 protected function updateNewtalk( $field, $id ) { 01836 $dbw = wfGetDB( DB_MASTER ); 01837 $dbw->insert( 'user_newtalk', 01838 array( $field => $id ), 01839 __METHOD__, 01840 'IGNORE' ); 01841 if ( $dbw->affectedRows() ) { 01842 wfDebug( __METHOD__ . ": set on ($field, $id)\n" ); 01843 return true; 01844 } else { 01845 wfDebug( __METHOD__ . " already set ($field, $id)\n" ); 01846 return false; 01847 } 01848 } 01849 01856 protected function deleteNewtalk( $field, $id ) { 01857 $dbw = wfGetDB( DB_MASTER ); 01858 $dbw->delete( 'user_newtalk', 01859 array( $field => $id ), 01860 __METHOD__ ); 01861 if ( $dbw->affectedRows() ) { 01862 wfDebug( __METHOD__ . ": killed on ($field, $id)\n" ); 01863 return true; 01864 } else { 01865 wfDebug( __METHOD__ . ": already gone ($field, $id)\n" ); 01866 return false; 01867 } 01868 } 01869 01874 public function setNewtalk( $val ) { 01875 if( wfReadOnly() ) { 01876 return; 01877 } 01878 01879 $this->load(); 01880 $this->mNewtalk = $val; 01881 01882 if( $this->isAnon() ) { 01883 $field = 'user_ip'; 01884 $id = $this->getName(); 01885 } else { 01886 $field = 'user_id'; 01887 $id = $this->getId(); 01888 } 01889 global $wgMemc; 01890 01891 if( $val ) { 01892 $changed = $this->updateNewtalk( $field, $id ); 01893 } else { 01894 $changed = $this->deleteNewtalk( $field, $id ); 01895 } 01896 01897 if( $this->isAnon() ) { 01898 // Anons have a separate memcached space, since 01899 // user records aren't kept for them. 01900 $key = wfMemcKey( 'newtalk', 'ip', $id ); 01901 $wgMemc->set( $key, $val ? 1 : 0, 1800 ); 01902 } 01903 if ( $changed ) { 01904 $this->invalidateCache(); 01905 } 01906 } 01907 01913 private static function newTouchedTimestamp() { 01914 global $wgClockSkewFudge; 01915 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge ); 01916 } 01917 01925 private function clearSharedCache() { 01926 $this->load(); 01927 if( $this->mId ) { 01928 global $wgMemc; 01929 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) ); 01930 } 01931 } 01932 01938 public function invalidateCache() { 01939 if( wfReadOnly() ) { 01940 return; 01941 } 01942 $this->load(); 01943 if( $this->mId ) { 01944 $this->mTouched = self::newTouchedTimestamp(); 01945 01946 $dbw = wfGetDB( DB_MASTER ); 01947 $dbw->update( 'user', 01948 array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ), 01949 array( 'user_id' => $this->mId ), 01950 __METHOD__ ); 01951 01952 $this->clearSharedCache(); 01953 } 01954 } 01955 01962 public function validateCache( $timestamp ) { 01963 $this->load(); 01964 return ( $timestamp >= $this->mTouched ); 01965 } 01966 01971 public function getTouched() { 01972 $this->load(); 01973 return $this->mTouched; 01974 } 01975 01992 public function setPassword( $str ) { 01993 global $wgAuth; 01994 01995 if( $str !== null ) { 01996 if( !$wgAuth->allowPasswordChange() ) { 01997 throw new PasswordError( wfMsg( 'password-change-forbidden' ) ); 01998 } 01999 02000 if( !$this->isValidPassword( $str ) ) { 02001 global $wgMinimalPasswordLength; 02002 $valid = $this->getPasswordValidity( $str ); 02003 if ( is_array( $valid ) ) { 02004 $message = array_shift( $valid ); 02005 $params = $valid; 02006 } else { 02007 $message = $valid; 02008 $params = array( $wgMinimalPasswordLength ); 02009 } 02010 throw new PasswordError( wfMsgExt( $message, array( 'parsemag' ), $params ) ); 02011 } 02012 } 02013 02014 if( !$wgAuth->setPassword( $this, $str ) ) { 02015 throw new PasswordError( wfMsg( 'externaldberror' ) ); 02016 } 02017 02018 $this->setInternalPassword( $str ); 02019 02020 return true; 02021 } 02022 02028 public function setInternalPassword( $str ) { 02029 $this->load(); 02030 $this->setToken(); 02031 02032 if( $str === null ) { 02033 // Save an invalid hash... 02034 $this->mPassword = ''; 02035 } else { 02036 $this->mPassword = self::crypt( $str ); 02037 } 02038 $this->mNewpassword = ''; 02039 $this->mNewpassTime = null; 02040 } 02041 02047 public function getToken( $forceCreation = true ) { 02048 $this->load(); 02049 if ( !$this->mToken && $forceCreation ) { 02050 $this->setToken(); 02051 } 02052 return $this->mToken; 02053 } 02054 02061 public function setToken( $token = false ) { 02062 global $wgSecretKey, $wgProxyKey; 02063 $this->load(); 02064 if ( !$token ) { 02065 $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH ); 02066 } else { 02067 $this->mToken = $token; 02068 } 02069 } 02070 02076 private function setCookiePassword( $str ) { 02077 $this->load(); 02078 $this->mCookiePassword = md5( $str ); 02079 } 02080 02087 public function setNewpassword( $str, $throttle = true ) { 02088 $this->load(); 02089 $this->mNewpassword = self::crypt( $str ); 02090 if ( $throttle ) { 02091 $this->mNewpassTime = wfTimestampNow(); 02092 } 02093 } 02094 02100 public function isPasswordReminderThrottled() { 02101 global $wgPasswordReminderResendTime; 02102 $this->load(); 02103 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) { 02104 return false; 02105 } 02106 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600; 02107 return time() < $expiry; 02108 } 02109 02114 public function getEmail() { 02115 $this->load(); 02116 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) ); 02117 return $this->mEmail; 02118 } 02119 02124 public function getEmailAuthenticationTimestamp() { 02125 $this->load(); 02126 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) ); 02127 return $this->mEmailAuthenticated; 02128 } 02129 02134 public function setEmail( $str ) { 02135 $this->load(); 02136 if( $str == $this->mEmail ) { 02137 return; 02138 } 02139 $this->mEmail = $str; 02140 $this->invalidateEmail(); 02141 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) ); 02142 } 02143 02148 public function getRealName() { 02149 if ( !$this->isItemLoaded( 'realname' ) ) { 02150 $this->load(); 02151 } 02152 02153 return $this->mRealName; 02154 } 02155 02160 public function setRealName( $str ) { 02161 $this->load(); 02162 $this->mRealName = $str; 02163 } 02164 02175 public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) { 02176 global $wgHiddenPrefs; 02177 $this->loadOptions(); 02178 02179 if ( is_null( $this->mOptions ) ) { 02180 if($defaultOverride != '') { 02181 return $defaultOverride; 02182 } 02183 $this->mOptions = User::getDefaultOptions(); 02184 } 02185 02186 # We want 'disabled' preferences to always behave as the default value for 02187 # users, even if they have set the option explicitly in their settings (ie they 02188 # set it, and then it was disabled removing their ability to change it). But 02189 # we don't want to erase the preferences in the database in case the preference 02190 # is re-enabled again. So don't touch $mOptions, just override the returned value 02191 if( in_array( $oname, $wgHiddenPrefs ) && !$ignoreHidden ){ 02192 return self::getDefaultOption( $oname ); 02193 } 02194 02195 if ( array_key_exists( $oname, $this->mOptions ) ) { 02196 return $this->mOptions[$oname]; 02197 } else { 02198 return $defaultOverride; 02199 } 02200 } 02201 02207 public function getOptions() { 02208 global $wgHiddenPrefs; 02209 $this->loadOptions(); 02210 $options = $this->mOptions; 02211 02212 # We want 'disabled' preferences to always behave as the default value for 02213 # users, even if they have set the option explicitly in their settings (ie they 02214 # set it, and then it was disabled removing their ability to change it). But 02215 # we don't want to erase the preferences in the database in case the preference 02216 # is re-enabled again. So don't touch $mOptions, just override the returned value 02217 foreach( $wgHiddenPrefs as $pref ){ 02218 $default = self::getDefaultOption( $pref ); 02219 if( $default !== null ){ 02220 $options[$pref] = $default; 02221 } 02222 } 02223 02224 return $options; 02225 } 02226 02234 public function getBoolOption( $oname ) { 02235 return (bool)$this->getOption( $oname ); 02236 } 02237 02246 public function getIntOption( $oname, $defaultOverride=0 ) { 02247 $val = $this->getOption( $oname ); 02248 if( $val == '' ) { 02249 $val = $defaultOverride; 02250 } 02251 return intval( $val ); 02252 } 02253 02260 public function setOption( $oname, $val ) { 02261 $this->load(); 02262 $this->loadOptions(); 02263 02264 // Explicitly NULL values should refer to defaults 02265 global $wgDefaultUserOptions; 02266 if( is_null( $val ) && isset( $wgDefaultUserOptions[$oname] ) ) { 02267 $val = $wgDefaultUserOptions[$oname]; 02268 } 02269 02270 $this->mOptions[$oname] = $val; 02271 } 02272 02276 public function resetOptions() { 02277 $this->mOptions = self::getDefaultOptions(); 02278 } 02279 02284 public function getDatePreference() { 02285 // Important migration for old data rows 02286 if ( is_null( $this->mDatePreference ) ) { 02287 global $wgLang; 02288 $value = $this->getOption( 'date' ); 02289 $map = $wgLang->getDatePreferenceMigrationMap(); 02290 if ( isset( $map[$value] ) ) { 02291 $value = $map[$value]; 02292 } 02293 $this->mDatePreference = $value; 02294 } 02295 return $this->mDatePreference; 02296 } 02297 02303 public function getStubThreshold() { 02304 global $wgMaxArticleSize; # Maximum article size, in Kb 02305 $threshold = intval( $this->getOption( 'stubthreshold' ) ); 02306 if ( $threshold > $wgMaxArticleSize * 1024 ) { 02307 # If they have set an impossible value, disable the preference 02308 # so we can use the parser cache again. 02309 $threshold = 0; 02310 } 02311 return $threshold; 02312 } 02313 02318 public function getRights() { 02319 if ( is_null( $this->mRights ) ) { 02320 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); 02321 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) ); 02322 // Force reindexation of rights when a hook has unset one of them 02323 $this->mRights = array_values( $this->mRights ); 02324 } 02325 return $this->mRights; 02326 } 02327 02333 public function getGroups() { 02334 $this->load(); 02335 $this->loadGroups(); 02336 return $this->mGroups; 02337 } 02338 02346 public function getEffectiveGroups( $recache = false ) { 02347 if ( $recache || is_null( $this->mEffectiveGroups ) ) { 02348 wfProfileIn( __METHOD__ ); 02349 $this->mEffectiveGroups = array_unique( array_merge( 02350 $this->getGroups(), // explicit groups 02351 $this->getAutomaticGroups( $recache ) // implicit groups 02352 ) ); 02353 # Hook for additional groups 02354 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) ); 02355 wfProfileOut( __METHOD__ ); 02356 } 02357 return $this->mEffectiveGroups; 02358 } 02359 02367 public function getAutomaticGroups( $recache = false ) { 02368 if ( $recache || is_null( $this->mImplicitGroups ) ) { 02369 wfProfileIn( __METHOD__ ); 02370 $this->mImplicitGroups = array( '*' ); 02371 if ( $this->getId() ) { 02372 $this->mImplicitGroups[] = 'user'; 02373 02374 $this->mImplicitGroups = array_unique( array_merge( 02375 $this->mImplicitGroups, 02376 Autopromote::getAutopromoteGroups( $this ) 02377 ) ); 02378 } 02379 if ( $recache ) { 02380 # Assure data consistency with rights/groups, 02381 # as getEffectiveGroups() depends on this function 02382 $this->mEffectiveGroups = null; 02383 } 02384 wfProfileOut( __METHOD__ ); 02385 } 02386 return $this->mImplicitGroups; 02387 } 02388 02398 public function getFormerGroups() { 02399 if( is_null( $this->mFormerGroups ) ) { 02400 $dbr = wfGetDB( DB_MASTER ); 02401 $res = $dbr->select( 'user_former_groups', 02402 array( 'ufg_group' ), 02403 array( 'ufg_user' => $this->mId ), 02404 __METHOD__ ); 02405 $this->mFormerGroups = array(); 02406 foreach( $res as $row ) { 02407 $this->mFormerGroups[] = $row->ufg_group; 02408 } 02409 } 02410 return $this->mFormerGroups; 02411 } 02412 02417 public function getEditCount() { 02418 if( $this->getId() ) { 02419 if ( !isset( $this->mEditCount ) ) { 02420 /* Populate the count, if it has not been populated yet */ 02421 $this->mEditCount = User::edits( $this->mId ); 02422 } 02423 return $this->mEditCount; 02424 } else { 02425 /* nil */ 02426 return null; 02427 } 02428 } 02429 02435 public function addGroup( $group ) { 02436 if( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) { 02437 $dbw = wfGetDB( DB_MASTER ); 02438 if( $this->getId() ) { 02439 $dbw->insert( 'user_groups', 02440 array( 02441 'ug_user' => $this->getID(), 02442 'ug_group' => $group, 02443 ), 02444 __METHOD__, 02445 array( 'IGNORE' ) ); 02446 } 02447 } 02448 $this->loadGroups(); 02449 $this->mGroups[] = $group; 02450 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) ); 02451 02452 $this->invalidateCache(); 02453 } 02454 02460 public function removeGroup( $group ) { 02461 $this->load(); 02462 if( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) { 02463 $dbw = wfGetDB( DB_MASTER ); 02464 $dbw->delete( 'user_groups', 02465 array( 02466 'ug_user' => $this->getID(), 02467 'ug_group' => $group, 02468 ), __METHOD__ ); 02469 // Remember that the user was in this group 02470 $dbw->insert( 'user_former_groups', 02471 array( 02472 'ufg_user' => $this->getID(), 02473 'ufg_group' => $group, 02474 ), 02475 __METHOD__, 02476 array( 'IGNORE' ) ); 02477 } 02478 $this->loadGroups(); 02479 $this->mGroups = array_diff( $this->mGroups, array( $group ) ); 02480 $this->mRights = User::getGroupPermissions( $this->getEffectiveGroups( true ) ); 02481 02482 $this->invalidateCache(); 02483 } 02484 02489 public function isLoggedIn() { 02490 return $this->getID() != 0; 02491 } 02492 02497 public function isAnon() { 02498 return !$this->isLoggedIn(); 02499 } 02500 02509 public function isAllowedAny( /*...*/ ){ 02510 $permissions = func_get_args(); 02511 foreach( $permissions as $permission ){ 02512 if( $this->isAllowed( $permission ) ){ 02513 return true; 02514 } 02515 } 02516 return false; 02517 } 02518 02524 public function isAllowedAll( /*...*/ ){ 02525 $permissions = func_get_args(); 02526 foreach( $permissions as $permission ){ 02527 if( !$this->isAllowed( $permission ) ){ 02528 return false; 02529 } 02530 } 02531 return true; 02532 } 02533 02539 public function isAllowed( $action = '' ) { 02540 if ( $action === '' ) { 02541 return true; // In the spirit of DWIM 02542 } 02543 # Patrolling may not be enabled 02544 if( $action === 'patrol' || $action === 'autopatrol' ) { 02545 global $wgUseRCPatrol, $wgUseNPPatrol; 02546 if( !$wgUseRCPatrol && !$wgUseNPPatrol ) 02547 return false; 02548 } 02549 # Use strict parameter to avoid matching numeric 0 accidentally inserted 02550 # by misconfiguration: 0 == 'foo' 02551 return in_array( $action, $this->getRights(), true ); 02552 } 02553 02558 public function useRCPatrol() { 02559 global $wgUseRCPatrol; 02560 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' ); 02561 } 02562 02567 public function useNPPatrol() { 02568 global $wgUseRCPatrol, $wgUseNPPatrol; 02569 return( ( $wgUseRCPatrol || $wgUseNPPatrol ) && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) ); 02570 } 02571 02577 public function getRequest() { 02578 if ( $this->mRequest ) { 02579 return $this->mRequest; 02580 } else { 02581 global $wgRequest; 02582 return $wgRequest; 02583 } 02584 } 02585 02592 public function getSkin() { 02593 wfDeprecated( __METHOD__, '1.18' ); 02594 return RequestContext::getMain()->getSkin(); 02595 } 02596 02602 public function isWatched( $title ) { 02603 $wl = WatchedItem::fromUserTitle( $this, $title ); 02604 return $wl->isWatched(); 02605 } 02606 02611 public function addWatch( $title ) { 02612 $wl = WatchedItem::fromUserTitle( $this, $title ); 02613 $wl->addWatch(); 02614 $this->invalidateCache(); 02615 } 02616 02621 public function removeWatch( $title ) { 02622 $wl = WatchedItem::fromUserTitle( $this, $title ); 02623 $wl->removeWatch(); 02624 $this->invalidateCache(); 02625 } 02626 02633 public function clearNotification( &$title ) { 02634 global $wgUseEnotif, $wgShowUpdatedMarker; 02635 02636 # Do nothing if the database is locked to writes 02637 if( wfReadOnly() ) { 02638 return; 02639 } 02640 02641 if( $title->getNamespace() == NS_USER_TALK && 02642 $title->getText() == $this->getName() ) { 02643 if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) ) 02644 return; 02645 $this->setNewtalk( false ); 02646 } 02647 02648 if( !$wgUseEnotif && !$wgShowUpdatedMarker ) { 02649 return; 02650 } 02651 02652 if( $this->isAnon() ) { 02653 // Nothing else to do... 02654 return; 02655 } 02656 02657 // Only update the timestamp if the page is being watched. 02658 // The query to find out if it is watched is cached both in memcached and per-invocation, 02659 // and when it does have to be executed, it can be on a slave 02660 // If this is the user's newtalk page, we always update the timestamp 02661 if( $title->getNamespace() == NS_USER_TALK && 02662 $title->getText() == $this->getName() ) 02663 { 02664 $watched = true; 02665 } else { 02666 $watched = $this->isWatched( $title ); 02667 } 02668 02669 // If the page is watched by the user (or may be watched), update the timestamp on any 02670 // any matching rows 02671 if ( $watched ) { 02672 $dbw = wfGetDB( DB_MASTER ); 02673 $dbw->update( 'watchlist', 02674 array( /* SET */ 02675 'wl_notificationtimestamp' => null 02676 ), array( /* WHERE */ 02677 'wl_title' => $title->getDBkey(), 02678 'wl_namespace' => $title->getNamespace(), 02679 'wl_user' => $this->getID() 02680 ), __METHOD__ 02681 ); 02682 } 02683 } 02684 02690 public function clearAllNotifications() { 02691 global $wgUseEnotif, $wgShowUpdatedMarker; 02692 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) { 02693 $this->setNewtalk( false ); 02694 return; 02695 } 02696 $id = $this->getId(); 02697 if( $id != 0 ) { 02698 $dbw = wfGetDB( DB_MASTER ); 02699 $dbw->update( 'watchlist', 02700 array( /* SET */ 02701 'wl_notificationtimestamp' => null 02702 ), array( /* WHERE */ 02703 'wl_user' => $id 02704 ), __METHOD__ 02705 ); 02706 # We also need to clear here the "you have new message" notification for the own user_talk page 02707 # This is cleared one page view later in Article::viewUpdates(); 02708 } 02709 } 02710 02717 private function decodeOptions( $str ) { 02718 wfDeprecated( __METHOD__, '1.19' ); 02719 if( !$str ) 02720 return; 02721 02722 $this->mOptionsLoaded = true; 02723 $this->mOptionOverrides = array(); 02724 02725 // If an option is not set in $str, use the default value 02726 $this->mOptions = self::getDefaultOptions(); 02727 02728 $a = explode( "\n", $str ); 02729 foreach ( $a as $s ) { 02730 $m = array(); 02731 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) { 02732 $this->mOptions[$m[1]] = $m[2]; 02733 $this->mOptionOverrides[$m[1]] = $m[2]; 02734 } 02735 } 02736 } 02737 02746 protected function setCookie( $name, $value, $exp = 0 ) { 02747 $this->getRequest()->response()->setcookie( $name, $value, $exp ); 02748 } 02749 02754 protected function clearCookie( $name ) { 02755 $this->setCookie( $name, '', time() - 86400 ); 02756 } 02757 02764 public function setCookies( $request = null ) { 02765 if ( $request === null ) { 02766 $request = $this->getRequest(); 02767 } 02768 02769 $this->load(); 02770 if ( 0 == $this->mId ) return; 02771 if ( !$this->mToken ) { 02772 // When token is empty or NULL generate a new one and then save it to the database 02773 // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey 02774 // Simply by setting every cell in the user_token column to NULL and letting them be 02775 // regenerated as users log back into the wiki. 02776 $this->setToken(); 02777 $this->saveSettings(); 02778 } 02779 $session = array( 02780 'wsUserID' => $this->mId, 02781 'wsToken' => $this->mToken, 02782 'wsUserName' => $this->getName() 02783 ); 02784 $cookies = array( 02785 'UserID' => $this->mId, 02786 'UserName' => $this->getName(), 02787 ); 02788 if ( 1 == $this->getOption( 'rememberpassword' ) ) { 02789 $cookies['Token'] = $this->mToken; 02790 } else { 02791 $cookies['Token'] = false; 02792 } 02793 02794 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) ); 02795 02796 foreach ( $session as $name => $value ) { 02797 $request->setSessionData( $name, $value ); 02798 } 02799 foreach ( $cookies as $name => $value ) { 02800 if ( $value === false ) { 02801 $this->clearCookie( $name ); 02802 } else { 02803 $this->setCookie( $name, $value ); 02804 } 02805 } 02806 } 02807 02811 public function logout() { 02812 if( wfRunHooks( 'UserLogout', array( &$this ) ) ) { 02813 $this->doLogout(); 02814 } 02815 } 02816 02821 public function doLogout() { 02822 $this->clearInstanceCache( 'defaults' ); 02823 02824 $this->getRequest()->setSessionData( 'wsUserID', 0 ); 02825 02826 $this->clearCookie( 'UserID' ); 02827 $this->clearCookie( 'Token' ); 02828 02829 # Remember when user logged out, to prevent seeing cached pages 02830 $this->setCookie( 'LoggedOut', wfTimestampNow(), time() + 86400 ); 02831 } 02832 02837 public function saveSettings() { 02838 global $wgAuth; 02839 02840 $this->load(); 02841 if ( wfReadOnly() ) { return; } 02842 if ( 0 == $this->mId ) { return; } 02843 02844 $this->mTouched = self::newTouchedTimestamp(); 02845 if ( !$wgAuth->allowSetLocalPassword() ) { 02846 $this->mPassword = ''; 02847 } 02848 02849 $dbw = wfGetDB( DB_MASTER ); 02850 $dbw->update( 'user', 02851 array( /* SET */ 02852 'user_name' => $this->mName, 02853 'user_password' => $this->mPassword, 02854 'user_newpassword' => $this->mNewpassword, 02855 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 02856 'user_real_name' => $this->mRealName, 02857 'user_email' => $this->mEmail, 02858 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 02859 'user_touched' => $dbw->timestamp( $this->mTouched ), 02860 'user_token' => strval( $this->mToken ), 02861 'user_email_token' => $this->mEmailToken, 02862 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), 02863 ), array( /* WHERE */ 02864 'user_id' => $this->mId 02865 ), __METHOD__ 02866 ); 02867 02868 $this->saveOptions(); 02869 02870 wfRunHooks( 'UserSaveSettings', array( $this ) ); 02871 $this->clearSharedCache(); 02872 $this->getUserPage()->invalidateCache(); 02873 } 02874 02879 public function idForName() { 02880 $s = trim( $this->getName() ); 02881 if ( $s === '' ) return 0; 02882 02883 $dbr = wfGetDB( DB_SLAVE ); 02884 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ ); 02885 if ( $id === false ) { 02886 $id = 0; 02887 } 02888 return $id; 02889 } 02890 02907 public static function createNew( $name, $params = array() ) { 02908 $user = new User; 02909 $user->load(); 02910 if ( isset( $params['options'] ) ) { 02911 $user->mOptions = $params['options'] + (array)$user->mOptions; 02912 unset( $params['options'] ); 02913 } 02914 $dbw = wfGetDB( DB_MASTER ); 02915 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); 02916 02917 $fields = array( 02918 'user_id' => $seqVal, 02919 'user_name' => $name, 02920 'user_password' => $user->mPassword, 02921 'user_newpassword' => $user->mNewpassword, 02922 'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ), 02923 'user_email' => $user->mEmail, 02924 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), 02925 'user_real_name' => $user->mRealName, 02926 'user_token' => strval( $user->mToken ), 02927 'user_registration' => $dbw->timestamp( $user->mRegistration ), 02928 'user_editcount' => 0, 02929 ); 02930 foreach ( $params as $name => $value ) { 02931 $fields["user_$name"] = $value; 02932 } 02933 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) ); 02934 if ( $dbw->affectedRows() ) { 02935 $newUser = User::newFromId( $dbw->insertId() ); 02936 } else { 02937 $newUser = null; 02938 } 02939 return $newUser; 02940 } 02941 02945 public function addToDatabase() { 02946 $this->load(); 02947 $dbw = wfGetDB( DB_MASTER ); 02948 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); 02949 $dbw->insert( 'user', 02950 array( 02951 'user_id' => $seqVal, 02952 'user_name' => $this->mName, 02953 'user_password' => $this->mPassword, 02954 'user_newpassword' => $this->mNewpassword, 02955 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 02956 'user_email' => $this->mEmail, 02957 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 02958 'user_real_name' => $this->mRealName, 02959 'user_token' => strval( $this->mToken ), 02960 'user_registration' => $dbw->timestamp( $this->mRegistration ), 02961 'user_editcount' => 0, 02962 ), __METHOD__ 02963 ); 02964 $this->mId = $dbw->insertId(); 02965 02966 // Clear instance cache other than user table data, which is already accurate 02967 $this->clearInstanceCache(); 02968 02969 $this->saveOptions(); 02970 } 02971 02977 public function spreadAnyEditBlock() { 02978 if ( $this->isLoggedIn() && $this->isBlocked() ) { 02979 return $this->spreadBlock(); 02980 } 02981 return false; 02982 } 02983 02989 protected function spreadBlock() { 02990 wfDebug( __METHOD__ . "()\n" ); 02991 $this->load(); 02992 if ( $this->mId == 0 ) { 02993 return false; 02994 } 02995 02996 $userblock = Block::newFromTarget( $this->getName() ); 02997 if ( !$userblock ) { 02998 return false; 02999 } 03000 03001 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() ); 03002 } 03003 03018 public function getPageRenderingHash() { 03019 wfDeprecated( __METHOD__, '1.17' ); 03020 03021 global $wgUseDynamicDates, $wgRenderHashAppend, $wgLang, $wgContLang; 03022 if( $this->mHash ){ 03023 return $this->mHash; 03024 } 03025 03026 // stubthreshold is only included below for completeness, 03027 // since it disables the parser cache, its value will always 03028 // be 0 when this function is called by parsercache. 03029 03030 $confstr = $this->getOption( 'math' ); 03031 $confstr .= '!' . $this->getStubThreshold(); 03032 if ( $wgUseDynamicDates ) { # This is wrong (bug 24714) 03033 $confstr .= '!' . $this->getDatePreference(); 03034 } 03035 $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' ); 03036 $confstr .= '!' . $wgLang->getCode(); 03037 $confstr .= '!' . $this->getOption( 'thumbsize' ); 03038 // add in language specific options, if any 03039 $extra = $wgContLang->getExtraHashOptions(); 03040 $confstr .= $extra; 03041 03042 // Since the skin could be overloading link(), it should be 03043 // included here but in practice, none of our skins do that. 03044 03045 $confstr .= $wgRenderHashAppend; 03046 03047 // Give a chance for extensions to modify the hash, if they have 03048 // extra options or other effects on the parser cache. 03049 wfRunHooks( 'PageRenderingHash', array( &$confstr ) ); 03050 03051 // Make it a valid memcached key fragment 03052 $confstr = str_replace( ' ', '_', $confstr ); 03053 $this->mHash = $confstr; 03054 return $confstr; 03055 } 03056 03061 public function isBlockedFromCreateAccount() { 03062 $this->getBlockedStatus(); 03063 if( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ){ 03064 return $this->mBlock; 03065 } 03066 03067 # bug 13611: if the IP address the user is trying to create an account from is 03068 # blocked with createaccount disabled, prevent new account creation there even 03069 # when the user is logged in 03070 if( $this->mBlockedFromCreateAccount === false ){ 03071 $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() ); 03072 } 03073 return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' ) 03074 ? $this->mBlockedFromCreateAccount 03075 : false; 03076 } 03077 03082 public function isBlockedFromEmailuser() { 03083 $this->getBlockedStatus(); 03084 return $this->mBlock && $this->mBlock->prevents( 'sendemail' ); 03085 } 03086 03091 function isAllowedToCreateAccount() { 03092 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); 03093 } 03094 03100 public function getUserPage() { 03101 return Title::makeTitle( NS_USER, $this->getName() ); 03102 } 03103 03109 public function getTalkPage() { 03110 $title = $this->getUserPage(); 03111 return $title->getTalkPage(); 03112 } 03113 03119 public function isNewbie() { 03120 return !$this->isAllowed( 'autoconfirmed' ); 03121 } 03122 03128 public function checkPassword( $password ) { 03129 global $wgAuth, $wgLegacyEncoding; 03130 $this->load(); 03131 03132 // Even though we stop people from creating passwords that 03133 // are shorter than this, doesn't mean people wont be able 03134 // to. Certain authentication plugins do NOT want to save 03135 // domain passwords in a mysql database, so we should 03136 // check this (in case $wgAuth->strict() is false). 03137 if( !$this->isValidPassword( $password ) ) { 03138 return false; 03139 } 03140 03141 if( $wgAuth->authenticate( $this->getName(), $password ) ) { 03142 return true; 03143 } elseif( $wgAuth->strict() ) { 03144 /* Auth plugin doesn't allow local authentication */ 03145 return false; 03146 } elseif( $wgAuth->strictUserAuth( $this->getName() ) ) { 03147 /* Auth plugin doesn't allow local authentication for this user name */ 03148 return false; 03149 } 03150 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) { 03151 return true; 03152 } elseif ( $wgLegacyEncoding ) { 03153 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted 03154 # Check for this with iconv 03155 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ); 03156 if ( $cp1252Password != $password && 03157 self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) 03158 { 03159 return true; 03160 } 03161 } 03162 return false; 03163 } 03164 03173 public function checkTemporaryPassword( $plaintext ) { 03174 global $wgNewPasswordExpiry; 03175 03176 $this->load(); 03177 if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) { 03178 if ( is_null( $this->mNewpassTime ) ) { 03179 return true; 03180 } 03181 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry; 03182 return ( time() < $expiry ); 03183 } else { 03184 return false; 03185 } 03186 } 03187 03196 public function editToken( $salt = '', $request = null ) { 03197 wfDeprecated( __METHOD__, '1.19' ); 03198 return $this->getEditToken( $salt, $request ); 03199 } 03200 03213 public function getEditToken( $salt = '', $request = null ) { 03214 if ( $request == null ) { 03215 $request = $this->getRequest(); 03216 } 03217 03218 if ( $this->isAnon() ) { 03219 return EDIT_TOKEN_SUFFIX; 03220 } else { 03221 $token = $request->getSessionData( 'wsEditToken' ); 03222 if ( $token === null ) { 03223 $token = MWCryptRand::generateHex( 32 ); 03224 $request->setSessionData( 'wsEditToken', $token ); 03225 } 03226 if( is_array( $salt ) ) { 03227 $salt = implode( '|', $salt ); 03228 } 03229 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX; 03230 } 03231 } 03232 03239 public static function generateToken( $salt = '' ) { 03240 return MWCryptRand::generateHex( 32 ); 03241 } 03242 03254 public function matchEditToken( $val, $salt = '', $request = null ) { 03255 $sessionToken = $this->getEditToken( $salt, $request ); 03256 if ( $val != $sessionToken ) { 03257 wfDebug( "User::matchEditToken: broken session data\n" ); 03258 } 03259 return $val == $sessionToken; 03260 } 03261 03271 public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) { 03272 $sessionToken = $this->getEditToken( $salt, $request ); 03273 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 ); 03274 } 03275 03283 public function sendConfirmationMail( $type = 'created' ) { 03284 global $wgLang; 03285 $expiration = null; // gets passed-by-ref and defined in next line. 03286 $token = $this->confirmationToken( $expiration ); 03287 $url = $this->confirmationTokenUrl( $token ); 03288 $invalidateURL = $this->invalidationTokenUrl( $token ); 03289 $this->saveSettings(); 03290 03291 if ( $type == 'created' || $type === false ) { 03292 $message = 'confirmemail_body'; 03293 } elseif ( $type === true ) { 03294 $message = 'confirmemail_body_changed'; 03295 } else { 03296 $message = 'confirmemail_body_' . $type; 03297 } 03298 03299 return $this->sendMail( wfMsg( 'confirmemail_subject' ), 03300 wfMsg( $message, 03301 $this->getRequest()->getIP(), 03302 $this->getName(), 03303 $url, 03304 $wgLang->timeanddate( $expiration, false ), 03305 $invalidateURL, 03306 $wgLang->date( $expiration, false ), 03307 $wgLang->time( $expiration, false ) ) ); 03308 } 03309 03320 public function sendMail( $subject, $body, $from = null, $replyto = null ) { 03321 if( is_null( $from ) ) { 03322 global $wgPasswordSender, $wgPasswordSenderName; 03323 $sender = new MailAddress( $wgPasswordSender, $wgPasswordSenderName ); 03324 } else { 03325 $sender = new MailAddress( $from ); 03326 } 03327 03328 $to = new MailAddress( $this ); 03329 return UserMailer::send( $to, $sender, $subject, $body, $replyto ); 03330 } 03331 03342 private function confirmationToken( &$expiration ) { 03343 global $wgUserEmailConfirmationTokenExpiry; 03344 $now = time(); 03345 $expires = $now + $wgUserEmailConfirmationTokenExpiry; 03346 $expiration = wfTimestamp( TS_MW, $expires ); 03347 $this->load(); 03348 $token = MWCryptRand::generateHex( 32 ); 03349 $hash = md5( $token ); 03350 $this->mEmailToken = $hash; 03351 $this->mEmailTokenExpires = $expiration; 03352 return $token; 03353 } 03354 03360 private function confirmationTokenUrl( $token ) { 03361 return $this->getTokenUrl( 'ConfirmEmail', $token ); 03362 } 03363 03369 private function invalidationTokenUrl( $token ) { 03370 return $this->getTokenUrl( 'Invalidateemail', $token ); 03371 } 03372 03387 protected function getTokenUrl( $page, $token ) { 03388 // Hack to bypass localization of 'Special:' 03389 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" ); 03390 return $title->getCanonicalUrl(); 03391 } 03392 03400 public function confirmEmail() { 03401 $this->setEmailAuthenticationTimestamp( wfTimestampNow() ); 03402 wfRunHooks( 'ConfirmEmailComplete', array( $this ) ); 03403 return true; 03404 } 03405 03413 function invalidateEmail() { 03414 $this->load(); 03415 $this->mEmailToken = null; 03416 $this->mEmailTokenExpires = null; 03417 $this->setEmailAuthenticationTimestamp( null ); 03418 wfRunHooks( 'InvalidateEmailComplete', array( $this ) ); 03419 return true; 03420 } 03421 03426 function setEmailAuthenticationTimestamp( $timestamp ) { 03427 $this->load(); 03428 $this->mEmailAuthenticated = $timestamp; 03429 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) ); 03430 } 03431 03437 public function canSendEmail() { 03438 global $wgEnableEmail, $wgEnableUserEmail; 03439 if( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) { 03440 return false; 03441 } 03442 $canSend = $this->isEmailConfirmed(); 03443 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) ); 03444 return $canSend; 03445 } 03446 03452 public function canReceiveEmail() { 03453 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' ); 03454 } 03455 03466 public function isEmailConfirmed() { 03467 global $wgEmailAuthentication; 03468 $this->load(); 03469 $confirmed = true; 03470 if( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) { 03471 if( $this->isAnon() ) { 03472 return false; 03473 } 03474 if( !Sanitizer::validateEmail( $this->mEmail ) ) { 03475 return false; 03476 } 03477 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) { 03478 return false; 03479 } 03480 return true; 03481 } else { 03482 return $confirmed; 03483 } 03484 } 03485 03490 public function isEmailConfirmationPending() { 03491 global $wgEmailAuthentication; 03492 return $wgEmailAuthentication && 03493 !$this->isEmailConfirmed() && 03494 $this->mEmailToken && 03495 $this->mEmailTokenExpires > wfTimestamp(); 03496 } 03497 03504 public function getRegistration() { 03505 if ( $this->isAnon() ) { 03506 return false; 03507 } 03508 $this->load(); 03509 return $this->mRegistration; 03510 } 03511 03518 public function getFirstEditTimestamp() { 03519 if( $this->getId() == 0 ) { 03520 return false; // anons 03521 } 03522 $dbr = wfGetDB( DB_SLAVE ); 03523 $time = $dbr->selectField( 'revision', 'rev_timestamp', 03524 array( 'rev_user' => $this->getId() ), 03525 __METHOD__, 03526 array( 'ORDER BY' => 'rev_timestamp ASC' ) 03527 ); 03528 if( !$time ) { 03529 return false; // no edits 03530 } 03531 return wfTimestamp( TS_MW, $time ); 03532 } 03533 03540 public static function getGroupPermissions( $groups ) { 03541 global $wgGroupPermissions, $wgRevokePermissions; 03542 $rights = array(); 03543 // grant every granted permission first 03544 foreach( $groups as $group ) { 03545 if( isset( $wgGroupPermissions[$group] ) ) { 03546 $rights = array_merge( $rights, 03547 // array_filter removes empty items 03548 array_keys( array_filter( $wgGroupPermissions[$group] ) ) ); 03549 } 03550 } 03551 // now revoke the revoked permissions 03552 foreach( $groups as $group ) { 03553 if( isset( $wgRevokePermissions[$group] ) ) { 03554 $rights = array_diff( $rights, 03555 array_keys( array_filter( $wgRevokePermissions[$group] ) ) ); 03556 } 03557 } 03558 return array_unique( $rights ); 03559 } 03560 03567 public static function getGroupsWithPermission( $role ) { 03568 global $wgGroupPermissions; 03569 $allowedGroups = array(); 03570 foreach ( $wgGroupPermissions as $group => $rights ) { 03571 if ( isset( $rights[$role] ) && $rights[$role] ) { 03572 $allowedGroups[] = $group; 03573 } 03574 } 03575 return $allowedGroups; 03576 } 03577 03584 public static function getGroupName( $group ) { 03585 $msg = wfMessage( "group-$group" ); 03586 return $msg->isBlank() ? $group : $msg->text(); 03587 } 03588 03596 public static function getGroupMember( $group, $username = '#' ) { 03597 $msg = wfMessage( "group-$group-member", $username ); 03598 return $msg->isBlank() ? $group : $msg->text(); 03599 } 03600 03607 public static function getAllGroups() { 03608 global $wgGroupPermissions, $wgRevokePermissions; 03609 return array_diff( 03610 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ), 03611 self::getImplicitGroups() 03612 ); 03613 } 03614 03619 public static function getAllRights() { 03620 if ( self::$mAllRights === false ) { 03621 global $wgAvailableRights; 03622 if ( count( $wgAvailableRights ) ) { 03623 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) ); 03624 } else { 03625 self::$mAllRights = self::$mCoreRights; 03626 } 03627 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) ); 03628 } 03629 return self::$mAllRights; 03630 } 03631 03636 public static function getImplicitGroups() { 03637 global $wgImplicitGroups; 03638 $groups = $wgImplicitGroups; 03639 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead 03640 return $groups; 03641 } 03642 03649 public static function getGroupPage( $group ) { 03650 $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage(); 03651 if( $msg->exists() ) { 03652 $title = Title::newFromText( $msg->text() ); 03653 if( is_object( $title ) ) 03654 return $title; 03655 } 03656 return false; 03657 } 03658 03667 public static function makeGroupLinkHTML( $group, $text = '' ) { 03668 if( $text == '' ) { 03669 $text = self::getGroupName( $group ); 03670 } 03671 $title = self::getGroupPage( $group ); 03672 if( $title ) { 03673 return Linker::link( $title, htmlspecialchars( $text ) ); 03674 } else { 03675 return $text; 03676 } 03677 } 03678 03687 public static function makeGroupLinkWiki( $group, $text = '' ) { 03688 if( $text == '' ) { 03689 $text = self::getGroupName( $group ); 03690 } 03691 $title = self::getGroupPage( $group ); 03692 if( $title ) { 03693 $page = $title->getPrefixedText(); 03694 return "[[$page|$text]]"; 03695 } else { 03696 return $text; 03697 } 03698 } 03699 03709 public static function changeableByGroup( $group ) { 03710 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; 03711 03712 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); 03713 if( empty( $wgAddGroups[$group] ) ) { 03714 // Don't add anything to $groups 03715 } elseif( $wgAddGroups[$group] === true ) { 03716 // You get everything 03717 $groups['add'] = self::getAllGroups(); 03718 } elseif( is_array( $wgAddGroups[$group] ) ) { 03719 $groups['add'] = $wgAddGroups[$group]; 03720 } 03721 03722 // Same thing for remove 03723 if( empty( $wgRemoveGroups[$group] ) ) { 03724 } elseif( $wgRemoveGroups[$group] === true ) { 03725 $groups['remove'] = self::getAllGroups(); 03726 } elseif( is_array( $wgRemoveGroups[$group] ) ) { 03727 $groups['remove'] = $wgRemoveGroups[$group]; 03728 } 03729 03730 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility 03731 if( empty( $wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) { 03732 foreach( $wgGroupsAddToSelf as $key => $value ) { 03733 if( is_int( $key ) ) { 03734 $wgGroupsAddToSelf['user'][] = $value; 03735 } 03736 } 03737 } 03738 03739 if( empty( $wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) { 03740 foreach( $wgGroupsRemoveFromSelf as $key => $value ) { 03741 if( is_int( $key ) ) { 03742 $wgGroupsRemoveFromSelf['user'][] = $value; 03743 } 03744 } 03745 } 03746 03747 // Now figure out what groups the user can add to him/herself 03748 if( empty( $wgGroupsAddToSelf[$group] ) ) { 03749 } elseif( $wgGroupsAddToSelf[$group] === true ) { 03750 // No idea WHY this would be used, but it's there 03751 $groups['add-self'] = User::getAllGroups(); 03752 } elseif( is_array( $wgGroupsAddToSelf[$group] ) ) { 03753 $groups['add-self'] = $wgGroupsAddToSelf[$group]; 03754 } 03755 03756 if( empty( $wgGroupsRemoveFromSelf[$group] ) ) { 03757 } elseif( $wgGroupsRemoveFromSelf[$group] === true ) { 03758 $groups['remove-self'] = User::getAllGroups(); 03759 } elseif( is_array( $wgGroupsRemoveFromSelf[$group] ) ) { 03760 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; 03761 } 03762 03763 return $groups; 03764 } 03765 03773 public function changeableGroups() { 03774 if( $this->isAllowed( 'userrights' ) ) { 03775 // This group gives the right to modify everything (reverse- 03776 // compatibility with old "userrights lets you change 03777 // everything") 03778 // Using array_merge to make the groups reindexed 03779 $all = array_merge( User::getAllGroups() ); 03780 return array( 03781 'add' => $all, 03782 'remove' => $all, 03783 'add-self' => array(), 03784 'remove-self' => array() 03785 ); 03786 } 03787 03788 // Okay, it's not so simple, we will have to go through the arrays 03789 $groups = array( 03790 'add' => array(), 03791 'remove' => array(), 03792 'add-self' => array(), 03793 'remove-self' => array() 03794 ); 03795 $addergroups = $this->getEffectiveGroups(); 03796 03797 foreach( $addergroups as $addergroup ) { 03798 $groups = array_merge_recursive( 03799 $groups, $this->changeableByGroup( $addergroup ) 03800 ); 03801 $groups['add'] = array_unique( $groups['add'] ); 03802 $groups['remove'] = array_unique( $groups['remove'] ); 03803 $groups['add-self'] = array_unique( $groups['add-self'] ); 03804 $groups['remove-self'] = array_unique( $groups['remove-self'] ); 03805 } 03806 return $groups; 03807 } 03808 03813 public function incEditCount() { 03814 if( !$this->isAnon() ) { 03815 $dbw = wfGetDB( DB_MASTER ); 03816 $dbw->update( 'user', 03817 array( 'user_editcount=user_editcount+1' ), 03818 array( 'user_id' => $this->getId() ), 03819 __METHOD__ ); 03820 03821 // Lazy initialization check... 03822 if( $dbw->affectedRows() == 0 ) { 03823 // Pull from a slave to be less cruel to servers 03824 // Accuracy isn't the point anyway here 03825 $dbr = wfGetDB( DB_SLAVE ); 03826 $count = $dbr->selectField( 'revision', 03827 'COUNT(rev_user)', 03828 array( 'rev_user' => $this->getId() ), 03829 __METHOD__ ); 03830 03831 // Now here's a goddamn hack... 03832 if( $dbr !== $dbw ) { 03833 // If we actually have a slave server, the count is 03834 // at least one behind because the current transaction 03835 // has not been committed and replicated. 03836 $count++; 03837 } else { 03838 // But if DB_SLAVE is selecting the master, then the 03839 // count we just read includes the revision that was 03840 // just added in the working transaction. 03841 } 03842 03843 $dbw->update( 'user', 03844 array( 'user_editcount' => $count ), 03845 array( 'user_id' => $this->getId() ), 03846 __METHOD__ ); 03847 } 03848 } 03849 // edit count in user cache too 03850 $this->invalidateCache(); 03851 } 03852 03859 public static function getRightDescription( $right ) { 03860 $key = "right-$right"; 03861 $msg = wfMessage( $key ); 03862 return $msg->isBlank() ? $right : $msg->text(); 03863 } 03864 03872 public static function oldCrypt( $password, $userId ) { 03873 global $wgPasswordSalt; 03874 if ( $wgPasswordSalt ) { 03875 return md5( $userId . '-' . md5( $password ) ); 03876 } else { 03877 return md5( $password ); 03878 } 03879 } 03880 03890 public static function crypt( $password, $salt = false ) { 03891 global $wgPasswordSalt; 03892 03893 $hash = ''; 03894 if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) { 03895 return $hash; 03896 } 03897 03898 if( $wgPasswordSalt ) { 03899 if ( $salt === false ) { 03900 $salt = MWCryptRand::generateHex( 8 ); 03901 } 03902 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); 03903 } else { 03904 return ':A:' . md5( $password ); 03905 } 03906 } 03907 03918 public static function comparePasswords( $hash, $password, $userId = false ) { 03919 $type = substr( $hash, 0, 3 ); 03920 03921 $result = false; 03922 if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) { 03923 return $result; 03924 } 03925 03926 if ( $type == ':A:' ) { 03927 # Unsalted 03928 return md5( $password ) === substr( $hash, 3 ); 03929 } elseif ( $type == ':B:' ) { 03930 # Salted 03931 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 ); 03932 return md5( $salt.'-'.md5( $password ) ) === $realHash; 03933 } else { 03934 # Old-style 03935 return self::oldCrypt( $password, $userId ) === $hash; 03936 } 03937 } 03938 03947 public function addNewUserLogEntry( $byEmail = false, $reason = '' ) { 03948 global $wgUser, $wgContLang, $wgNewUserLog; 03949 if( empty( $wgNewUserLog ) ) { 03950 return true; // disabled 03951 } 03952 03953 if( $this->getName() == $wgUser->getName() ) { 03954 $action = 'create'; 03955 } else { 03956 $action = 'create2'; 03957 if ( $byEmail ) { 03958 if ( $reason === '' ) { 03959 $reason = wfMsgForContent( 'newuserlog-byemail' ); 03960 } else { 03961 $reason = $wgContLang->commaList( array( 03962 $reason, wfMsgForContent( 'newuserlog-byemail' ) ) ); 03963 } 03964 } 03965 } 03966 $log = new LogPage( 'newusers' ); 03967 return (int)$log->addEntry( 03968 $action, 03969 $this->getUserPage(), 03970 $reason, 03971 array( $this->getId() ) 03972 ); 03973 } 03974 03981 public function addNewUserLogEntryAutoCreate() { 03982 global $wgNewUserLog; 03983 if( !$wgNewUserLog ) { 03984 return true; // disabled 03985 } 03986 $log = new LogPage( 'newusers', false ); 03987 $log->addEntry( 'autocreate', $this->getUserPage(), '', array( $this->getId() ) ); 03988 return true; 03989 } 03990 03994 protected function loadOptions() { 03995 $this->load(); 03996 if ( $this->mOptionsLoaded || !$this->getId() ) 03997 return; 03998 03999 $this->mOptions = self::getDefaultOptions(); 04000 04001 // Maybe load from the object 04002 if ( !is_null( $this->mOptionOverrides ) ) { 04003 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" ); 04004 foreach( $this->mOptionOverrides as $key => $value ) { 04005 $this->mOptions[$key] = $value; 04006 } 04007 } else { 04008 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" ); 04009 // Load from database 04010 $dbr = wfGetDB( DB_SLAVE ); 04011 04012 $res = $dbr->select( 04013 'user_properties', 04014 '*', 04015 array( 'up_user' => $this->getId() ), 04016 __METHOD__ 04017 ); 04018 04019 $this->mOptionOverrides = array(); 04020 foreach ( $res as $row ) { 04021 $this->mOptionOverrides[$row->up_property] = $row->up_value; 04022 $this->mOptions[$row->up_property] = $row->up_value; 04023 } 04024 } 04025 04026 $this->mOptionsLoaded = true; 04027 04028 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) ); 04029 } 04030 04034 protected function saveOptions() { 04035 global $wgAllowPrefChange; 04036 04037 $extuser = ExternalUser::newFromUser( $this ); 04038 04039 $this->loadOptions(); 04040 $dbw = wfGetDB( DB_MASTER ); 04041 04042 $insert_rows = array(); 04043 04044 $saveOptions = $this->mOptions; 04045 04046 // Allow hooks to abort, for instance to save to a global profile. 04047 // Reset options to default state before saving. 04048 if( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) { 04049 return; 04050 } 04051 04052 foreach( $saveOptions as $key => $value ) { 04053 # Don't bother storing default values 04054 if ( ( is_null( self::getDefaultOption( $key ) ) && 04055 !( $value === false || is_null($value) ) ) || 04056 $value != self::getDefaultOption( $key ) ) { 04057 $insert_rows[] = array( 04058 'up_user' => $this->getId(), 04059 'up_property' => $key, 04060 'up_value' => $value, 04061 ); 04062 } 04063 if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) { 04064 switch ( $wgAllowPrefChange[$key] ) { 04065 case 'local': 04066 case 'message': 04067 break; 04068 case 'semiglobal': 04069 case 'global': 04070 $extuser->setPref( $key, $value ); 04071 } 04072 } 04073 } 04074 04075 $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ ); 04076 $dbw->insert( 'user_properties', $insert_rows, __METHOD__ ); 04077 } 04078 04103 public static function passwordChangeInputAttribs() { 04104 global $wgMinimalPasswordLength; 04105 04106 if ( $wgMinimalPasswordLength == 0 ) { 04107 return array(); 04108 } 04109 04110 # Note that the pattern requirement will always be satisfied if the 04111 # input is empty, so we need required in all cases. 04112 # 04113 # @todo FIXME: Bug 23769: This needs to not claim the password is required 04114 # if e-mail confirmation is being used. Since HTML5 input validation 04115 # is b0rked anyway in some browsers, just return nothing. When it's 04116 # re-enabled, fix this code to not output required for e-mail 04117 # registration. 04118 #$ret = array( 'required' ); 04119 $ret = array(); 04120 04121 # We can't actually do this right now, because Opera 9.6 will print out 04122 # the entered password visibly in its error message! When other 04123 # browsers add support for this attribute, or Opera fixes its support, 04124 # we can add support with a version check to avoid doing this on Opera 04125 # versions where it will be a problem. Reported to Opera as 04126 # DSK-262266, but they don't have a public bug tracker for us to follow. 04127 /* 04128 if ( $wgMinimalPasswordLength > 1 ) { 04129 $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}'; 04130 $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag', 04131 $wgMinimalPasswordLength ); 04132 } 04133 */ 04134 04135 return $ret; 04136 } 04137 }