MediaWiki
REL1_23
|
00001 <?php 00027 define( 'USER_TOKEN_LENGTH', 32 ); 00028 00033 define( 'MW_USER_VERSION', 9 ); 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 'mPasswordExpires', 00094 'mRegistration', 00095 'mEditCount', 00096 // user_groups table 00097 'mGroups', 00098 // user_properties table 00099 'mOptionOverrides', 00100 ); 00101 00108 static $mCoreRights = array( 00109 'apihighlimits', 00110 'autoconfirmed', 00111 'autopatrol', 00112 'bigdelete', 00113 'block', 00114 'blockemail', 00115 'bot', 00116 'browsearchive', 00117 'createaccount', 00118 'createpage', 00119 'createtalk', 00120 'delete', 00121 'deletedhistory', 00122 'deletedtext', 00123 'deletelogentry', 00124 'deleterevision', 00125 'edit', 00126 'editinterface', 00127 'editprotected', 00128 'editmyoptions', 00129 'editmyprivateinfo', 00130 'editmyusercss', 00131 'editmyuserjs', 00132 'editmywatchlist', 00133 'editsemiprotected', 00134 'editusercssjs', #deprecated 00135 'editusercss', 00136 'edituserjs', 00137 'hideuser', 00138 'import', 00139 'importupload', 00140 'ipblock-exempt', 00141 'markbotedits', 00142 'mergehistory', 00143 'minoredit', 00144 'move', 00145 'movefile', 00146 'move-rootuserpages', 00147 'move-subpages', 00148 'nominornewtalk', 00149 'noratelimit', 00150 'override-export-depth', 00151 'passwordreset', 00152 'patrol', 00153 'patrolmarks', 00154 'protect', 00155 'proxyunbannable', 00156 'purge', 00157 'read', 00158 'reupload', 00159 'reupload-own', 00160 'reupload-shared', 00161 'rollback', 00162 'sendemail', 00163 'siteadmin', 00164 'suppressionlog', 00165 'suppressredirect', 00166 'suppressrevision', 00167 'unblockself', 00168 'undelete', 00169 'unwatchedpages', 00170 'upload', 00171 'upload_by_url', 00172 'userrights', 00173 'userrights-interwiki', 00174 'viewmyprivateinfo', 00175 'viewmywatchlist', 00176 'writeapi', 00177 ); 00181 static $mAllRights = false; 00182 00185 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, 00186 $mEmail, $mTouched, $mToken, $mEmailAuthenticated, 00187 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount, 00188 $mGroups, $mOptionOverrides; 00189 00190 protected $mPasswordExpires; 00192 00197 var $mOptionsLoaded; 00198 00202 private $mLoadedItems = array(); 00204 00214 var $mFrom; 00215 00219 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mRights, 00220 $mBlockreason, $mEffectiveGroups, $mImplicitGroups, $mFormerGroups, $mBlockedGlobally, 00221 $mLocked, $mHideName, $mOptions; 00222 00226 private $mRequest; 00227 00231 var $mBlock; 00232 00236 var $mAllowUsertalk; 00237 00241 private $mBlockedFromCreateAccount = false; 00242 00246 private $mWatchedItems = array(); 00247 00248 static $idCacheByName = array(); 00249 00260 public function __construct() { 00261 $this->clearInstanceCache( 'defaults' ); 00262 } 00263 00267 public function __toString() { 00268 return $this->getName(); 00269 } 00270 00274 public function load() { 00275 if ( $this->mLoadedItems === true ) { 00276 return; 00277 } 00278 wfProfileIn( __METHOD__ ); 00279 00280 // Set it now to avoid infinite recursion in accessors 00281 $this->mLoadedItems = true; 00282 00283 switch ( $this->mFrom ) { 00284 case 'defaults': 00285 $this->loadDefaults(); 00286 break; 00287 case 'name': 00288 $this->mId = self::idFromName( $this->mName ); 00289 if ( !$this->mId ) { 00290 // Nonexistent user placeholder object 00291 $this->loadDefaults( $this->mName ); 00292 } else { 00293 $this->loadFromId(); 00294 } 00295 break; 00296 case 'id': 00297 $this->loadFromId(); 00298 break; 00299 case 'session': 00300 if ( !$this->loadFromSession() ) { 00301 // Loading from session failed. Load defaults. 00302 $this->loadDefaults(); 00303 } 00304 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) ); 00305 break; 00306 default: 00307 wfProfileOut( __METHOD__ ); 00308 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" ); 00309 } 00310 wfProfileOut( __METHOD__ ); 00311 } 00312 00317 public function loadFromId() { 00318 global $wgMemc; 00319 if ( $this->mId == 0 ) { 00320 $this->loadDefaults(); 00321 return false; 00322 } 00323 00324 // Try cache 00325 $key = wfMemcKey( 'user', 'id', $this->mId ); 00326 $data = $wgMemc->get( $key ); 00327 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) { 00328 // Object is expired, load from DB 00329 $data = false; 00330 } 00331 00332 if ( !$data ) { 00333 wfDebug( "User: cache miss for user {$this->mId}\n" ); 00334 // Load from DB 00335 if ( !$this->loadFromDatabase() ) { 00336 // Can't load from ID, user is anonymous 00337 return false; 00338 } 00339 $this->saveToCache(); 00340 } else { 00341 wfDebug( "User: got user {$this->mId} from cache\n" ); 00342 // Restore from cache 00343 foreach ( self::$mCacheVars as $name ) { 00344 $this->$name = $data[$name]; 00345 } 00346 } 00347 00348 $this->mLoadedItems = true; 00349 00350 return true; 00351 } 00352 00356 public function saveToCache() { 00357 $this->load(); 00358 $this->loadGroups(); 00359 $this->loadOptions(); 00360 if ( $this->isAnon() ) { 00361 // Anonymous users are uncached 00362 return; 00363 } 00364 $data = array(); 00365 foreach ( self::$mCacheVars as $name ) { 00366 $data[$name] = $this->$name; 00367 } 00368 $data['mVersion'] = MW_USER_VERSION; 00369 $key = wfMemcKey( 'user', 'id', $this->mId ); 00370 global $wgMemc; 00371 $wgMemc->set( $key, $data ); 00372 } 00373 00376 00393 public static function newFromName( $name, $validate = 'valid' ) { 00394 if ( $validate === true ) { 00395 $validate = 'valid'; 00396 } 00397 $name = self::getCanonicalName( $name, $validate ); 00398 if ( $name === false ) { 00399 return false; 00400 } else { 00401 // Create unloaded user object 00402 $u = new User; 00403 $u->mName = $name; 00404 $u->mFrom = 'name'; 00405 $u->setItemLoaded( 'name' ); 00406 return $u; 00407 } 00408 } 00409 00416 public static function newFromId( $id ) { 00417 $u = new User; 00418 $u->mId = $id; 00419 $u->mFrom = 'id'; 00420 $u->setItemLoaded( 'id' ); 00421 return $u; 00422 } 00423 00434 public static function newFromConfirmationCode( $code ) { 00435 $dbr = wfGetDB( DB_SLAVE ); 00436 $id = $dbr->selectField( 'user', 'user_id', array( 00437 'user_email_token' => md5( $code ), 00438 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ), 00439 ) ); 00440 if ( $id !== false ) { 00441 return User::newFromId( $id ); 00442 } else { 00443 return null; 00444 } 00445 } 00446 00454 public static function newFromSession( WebRequest $request = null ) { 00455 $user = new User; 00456 $user->mFrom = 'session'; 00457 $user->mRequest = $request; 00458 return $user; 00459 } 00460 00475 public static function newFromRow( $row, $data = null ) { 00476 $user = new User; 00477 $user->loadFromRow( $row, $data ); 00478 return $user; 00479 } 00480 00482 00488 public static function whoIs( $id ) { 00489 return UserCache::singleton()->getProp( $id, 'name' ); 00490 } 00491 00498 public static function whoIsReal( $id ) { 00499 return UserCache::singleton()->getProp( $id, 'real_name' ); 00500 } 00501 00507 public static function idFromName( $name ) { 00508 $nt = Title::makeTitleSafe( NS_USER, $name ); 00509 if ( is_null( $nt ) ) { 00510 // Illegal name 00511 return null; 00512 } 00513 00514 if ( isset( self::$idCacheByName[$name] ) ) { 00515 return self::$idCacheByName[$name]; 00516 } 00517 00518 $dbr = wfGetDB( DB_SLAVE ); 00519 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ ); 00520 00521 if ( $s === false ) { 00522 $result = null; 00523 } else { 00524 $result = $s->user_id; 00525 } 00526 00527 self::$idCacheByName[$name] = $result; 00528 00529 if ( count( self::$idCacheByName ) > 1000 ) { 00530 self::$idCacheByName = array(); 00531 } 00532 00533 return $result; 00534 } 00535 00539 public static function resetIdByNameCache() { 00540 self::$idCacheByName = array(); 00541 } 00542 00559 public static function isIP( $name ) { 00560 return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name ) || IP::isIPv6( $name ); 00561 } 00562 00574 public static function isValidUserName( $name ) { 00575 global $wgContLang, $wgMaxNameChars; 00576 00577 if ( $name == '' 00578 || User::isIP( $name ) 00579 || strpos( $name, '/' ) !== false 00580 || strlen( $name ) > $wgMaxNameChars 00581 || $name != $wgContLang->ucfirst( $name ) ) { 00582 wfDebugLog( 'username', __METHOD__ . 00583 ": '$name' invalid due to empty, IP, slash, length, or lowercase" ); 00584 return false; 00585 } 00586 00587 // Ensure that the name can't be misresolved as a different title, 00588 // such as with extra namespace keys at the start. 00589 $parsed = Title::newFromText( $name ); 00590 if ( is_null( $parsed ) 00591 || $parsed->getNamespace() 00592 || strcmp( $name, $parsed->getPrefixedText() ) ) { 00593 wfDebugLog( 'username', __METHOD__ . 00594 ": '$name' invalid due to ambiguous prefixes" ); 00595 return false; 00596 } 00597 00598 // Check an additional blacklist of troublemaker characters. 00599 // Should these be merged into the title char list? 00600 $unicodeBlacklist = '/[' . 00601 '\x{0080}-\x{009f}' . # iso-8859-1 control chars 00602 '\x{00a0}' . # non-breaking space 00603 '\x{2000}-\x{200f}' . # various whitespace 00604 '\x{2028}-\x{202f}' . # breaks and control chars 00605 '\x{3000}' . # ideographic space 00606 '\x{e000}-\x{f8ff}' . # private use 00607 ']/u'; 00608 if ( preg_match( $unicodeBlacklist, $name ) ) { 00609 wfDebugLog( 'username', __METHOD__ . 00610 ": '$name' invalid due to blacklisted characters" ); 00611 return false; 00612 } 00613 00614 return true; 00615 } 00616 00628 public static function isUsableName( $name ) { 00629 global $wgReservedUsernames; 00630 // Must be a valid username, obviously ;) 00631 if ( !self::isValidUserName( $name ) ) { 00632 return false; 00633 } 00634 00635 static $reservedUsernames = false; 00636 if ( !$reservedUsernames ) { 00637 $reservedUsernames = $wgReservedUsernames; 00638 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) ); 00639 } 00640 00641 // Certain names may be reserved for batch processes. 00642 foreach ( $reservedUsernames as $reserved ) { 00643 if ( substr( $reserved, 0, 4 ) == 'msg:' ) { 00644 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text(); 00645 } 00646 if ( $reserved == $name ) { 00647 return false; 00648 } 00649 } 00650 return true; 00651 } 00652 00665 public static function isCreatableName( $name ) { 00666 global $wgInvalidUsernameCharacters; 00667 00668 // Ensure that the username isn't longer than 235 bytes, so that 00669 // (at least for the builtin skins) user javascript and css files 00670 // will work. (bug 23080) 00671 if ( strlen( $name ) > 235 ) { 00672 wfDebugLog( 'username', __METHOD__ . 00673 ": '$name' invalid due to length" ); 00674 return false; 00675 } 00676 00677 // Preg yells if you try to give it an empty string 00678 if ( $wgInvalidUsernameCharacters !== '' ) { 00679 if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) { 00680 wfDebugLog( 'username', __METHOD__ . 00681 ": '$name' invalid due to wgInvalidUsernameCharacters" ); 00682 return false; 00683 } 00684 } 00685 00686 return self::isUsableName( $name ); 00687 } 00688 00695 public function isValidPassword( $password ) { 00696 //simple boolean wrapper for getPasswordValidity 00697 return $this->getPasswordValidity( $password ) === true; 00698 } 00699 00700 00707 public function getPasswordValidity( $password ) { 00708 $result = $this->checkPasswordValidity( $password ); 00709 if ( $result->isGood() ) { 00710 return true; 00711 } else { 00712 $messages = array(); 00713 foreach ( $result->getErrorsByType( 'error' ) as $error ) { 00714 $messages[] = $error['message']; 00715 } 00716 foreach ( $result->getErrorsByType( 'warning' ) as $warning ) { 00717 $messages[] = $warning['message']; 00718 } 00719 if ( count( $messages ) === 1 ) { 00720 return $messages[0]; 00721 } 00722 return $messages; 00723 } 00724 } 00725 00734 public function checkPasswordValidity( $password ) { 00735 global $wgMinimalPasswordLength, $wgContLang; 00736 00737 static $blockedLogins = array( 00738 'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589 00739 'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605 00740 ); 00741 00742 $status = Status::newGood(); 00743 00744 $result = false; //init $result to false for the internal checks 00745 00746 if ( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) { 00747 $status->error( $result ); 00748 return $status; 00749 } 00750 00751 if ( $result === false ) { 00752 if ( strlen( $password ) < $wgMinimalPasswordLength ) { 00753 $status->error( 'passwordtooshort', $wgMinimalPasswordLength ); 00754 return $status; 00755 } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) { 00756 $status->error( 'password-name-match' ); 00757 return $status; 00758 } elseif ( isset( $blockedLogins[$this->getName()] ) && $password == $blockedLogins[$this->getName()] ) { 00759 $status->error( 'password-login-forbidden' ); 00760 return $status; 00761 } else { 00762 //it seems weird returning a Good status here, but this is because of the 00763 //initialization of $result to false above. If the hook is never run or it 00764 //doesn't modify $result, then we will likely get down into this if with 00765 //a valid password. 00766 return $status; 00767 } 00768 } elseif ( $result === true ) { 00769 return $status; 00770 } else { 00771 $status->error( $result ); 00772 return $status; //the isValidPassword hook set a string $result and returned true 00773 } 00774 } 00775 00781 public function expirePassword( $ts = 0 ) { 00782 $this->load(); 00783 $timestamp = wfTimestamp( TS_MW, $ts ); 00784 $this->mPasswordExpires = $timestamp; 00785 $this->saveSettings(); 00786 } 00787 00793 public function resetPasswordExpiration( $load = true ) { 00794 global $wgPasswordExpirationDays; 00795 if ( $load ) { 00796 $this->load(); 00797 } 00798 $newExpire = null; 00799 if ( $wgPasswordExpirationDays ) { 00800 $newExpire = wfTimestamp( 00801 TS_MW, 00802 time() + ( $wgPasswordExpirationDays * 24 * 3600 ) 00803 ); 00804 } 00805 // Give extensions a chance to force an expiration 00806 wfRunHooks( 'ResetPasswordExpiration', array( $this, &$newExpire ) ); 00807 $this->mPasswordExpires = $newExpire; 00808 } 00809 00819 public function getPasswordExpired() { 00820 global $wgPasswordExpireGrace; 00821 $expired = false; 00822 $now = wfTimestamp(); 00823 $expiration = $this->getPasswordExpireDate(); 00824 $expUnix = wfTimestamp( TS_UNIX, $expiration ); 00825 if ( $expiration !== null && $expUnix < $now ) { 00826 $expired = ( $expUnix + $wgPasswordExpireGrace < $now ) ? 'hard' : 'soft'; 00827 } 00828 return $expired; 00829 } 00830 00838 public function getPasswordExpireDate() { 00839 $this->load(); 00840 return $this->mPasswordExpires; 00841 } 00842 00870 public static function isValidEmailAddr( $addr ) { 00871 wfDeprecated( __METHOD__, '1.18' ); 00872 return Sanitizer::validateEmail( $addr ); 00873 } 00874 00888 public static function getCanonicalName( $name, $validate = 'valid' ) { 00889 // Force usernames to capital 00890 global $wgContLang; 00891 $name = $wgContLang->ucfirst( $name ); 00892 00893 # Reject names containing '#'; these will be cleaned up 00894 # with title normalisation, but then it's too late to 00895 # check elsewhere 00896 if ( strpos( $name, '#' ) !== false ) { 00897 return false; 00898 } 00899 00900 // Clean up name according to title rules 00901 $t = ( $validate === 'valid' ) ? 00902 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name ); 00903 // Check for invalid titles 00904 if ( is_null( $t ) ) { 00905 return false; 00906 } 00907 00908 // Reject various classes of invalid names 00909 global $wgAuth; 00910 $name = $wgAuth->getCanonicalName( $t->getText() ); 00911 00912 switch ( $validate ) { 00913 case false: 00914 break; 00915 case 'valid': 00916 if ( !User::isValidUserName( $name ) ) { 00917 $name = false; 00918 } 00919 break; 00920 case 'usable': 00921 if ( !User::isUsableName( $name ) ) { 00922 $name = false; 00923 } 00924 break; 00925 case 'creatable': 00926 if ( !User::isCreatableName( $name ) ) { 00927 $name = false; 00928 } 00929 break; 00930 default: 00931 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ ); 00932 } 00933 return $name; 00934 } 00935 00944 public static function edits( $uid ) { 00945 wfDeprecated( __METHOD__, '1.21' ); 00946 $user = self::newFromId( $uid ); 00947 return $user->getEditCount(); 00948 } 00949 00955 public static function randomPassword() { 00956 global $wgMinimalPasswordLength; 00957 // Decide the final password length based on our min password length, stopping at a minimum of 10 chars 00958 $length = max( 10, $wgMinimalPasswordLength ); 00959 // Multiply by 1.25 to get the number of hex characters we need 00960 $length = $length * 1.25; 00961 // Generate random hex chars 00962 $hex = MWCryptRand::generateHex( $length ); 00963 // Convert from base 16 to base 32 to get a proper password like string 00964 return wfBaseConvert( $hex, 16, 32 ); 00965 } 00966 00975 public function loadDefaults( $name = false ) { 00976 wfProfileIn( __METHOD__ ); 00977 00978 $this->mId = 0; 00979 $this->mName = $name; 00980 $this->mRealName = ''; 00981 $this->mPassword = $this->mNewpassword = ''; 00982 $this->mNewpassTime = null; 00983 $this->mEmail = ''; 00984 $this->mOptionOverrides = null; 00985 $this->mOptionsLoaded = false; 00986 00987 $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' ); 00988 if ( $loggedOut !== null ) { 00989 $this->mTouched = wfTimestamp( TS_MW, $loggedOut ); 00990 } else { 00991 $this->mTouched = '1'; # Allow any pages to be cached 00992 } 00993 00994 $this->mToken = null; // Don't run cryptographic functions till we need a token 00995 $this->mEmailAuthenticated = null; 00996 $this->mEmailToken = ''; 00997 $this->mEmailTokenExpires = null; 00998 $this->mPasswordExpires = null; 00999 $this->resetPasswordExpiration( false ); 01000 $this->mRegistration = wfTimestamp( TS_MW ); 01001 $this->mGroups = array(); 01002 01003 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) ); 01004 01005 wfProfileOut( __METHOD__ ); 01006 } 01007 01020 public function isItemLoaded( $item, $all = 'all' ) { 01021 return ( $this->mLoadedItems === true && $all === 'all' ) || 01022 ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true ); 01023 } 01024 01030 protected function setItemLoaded( $item ) { 01031 if ( is_array( $this->mLoadedItems ) ) { 01032 $this->mLoadedItems[$item] = true; 01033 } 01034 } 01035 01040 private function loadFromSession() { 01041 $result = null; 01042 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) ); 01043 if ( $result !== null ) { 01044 return $result; 01045 } 01046 01047 $request = $this->getRequest(); 01048 01049 $cookieId = $request->getCookie( 'UserID' ); 01050 $sessId = $request->getSessionData( 'wsUserID' ); 01051 01052 if ( $cookieId !== null ) { 01053 $sId = intval( $cookieId ); 01054 if ( $sessId !== null && $cookieId != $sessId ) { 01055 wfDebugLog( 'loginSessions', "Session user ID ($sessId) and 01056 cookie user ID ($sId) don't match!" ); 01057 return false; 01058 } 01059 $request->setSessionData( 'wsUserID', $sId ); 01060 } elseif ( $sessId !== null && $sessId != 0 ) { 01061 $sId = $sessId; 01062 } else { 01063 return false; 01064 } 01065 01066 if ( $request->getSessionData( 'wsUserName' ) !== null ) { 01067 $sName = $request->getSessionData( 'wsUserName' ); 01068 } elseif ( $request->getCookie( 'UserName' ) !== null ) { 01069 $sName = $request->getCookie( 'UserName' ); 01070 $request->setSessionData( 'wsUserName', $sName ); 01071 } else { 01072 return false; 01073 } 01074 01075 $proposedUser = User::newFromId( $sId ); 01076 if ( !$proposedUser->isLoggedIn() ) { 01077 // Not a valid ID 01078 return false; 01079 } 01080 01081 global $wgBlockDisablesLogin; 01082 if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) { 01083 // User blocked and we've disabled blocked user logins 01084 return false; 01085 } 01086 01087 if ( $request->getSessionData( 'wsToken' ) ) { 01088 $passwordCorrect = ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) ); 01089 $from = 'session'; 01090 } elseif ( $request->getCookie( 'Token' ) ) { 01091 # Get the token from DB/cache and clean it up to remove garbage padding. 01092 # This deals with historical problems with bugs and the default column value. 01093 $token = rtrim( $proposedUser->getToken( false ) ); // correct token 01094 // Make comparison in constant time (bug 61346) 01095 $passwordCorrect = strlen( $token ) && $this->compareSecrets( $token, $request->getCookie( 'Token' ) ); 01096 $from = 'cookie'; 01097 } else { 01098 // No session or persistent login cookie 01099 return false; 01100 } 01101 01102 if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) { 01103 $this->loadFromUserObject( $proposedUser ); 01104 $request->setSessionData( 'wsToken', $this->mToken ); 01105 wfDebug( "User: logged in from $from\n" ); 01106 return true; 01107 } else { 01108 // Invalid credentials 01109 wfDebug( "User: can't log in from $from, invalid credentials\n" ); 01110 return false; 01111 } 01112 } 01113 01120 protected function compareSecrets( $answer, $test ) { 01121 if ( strlen( $answer ) !== strlen( $test ) ) { 01122 $passwordCorrect = false; 01123 } else { 01124 $result = 0; 01125 for ( $i = 0; $i < strlen( $answer ); $i++ ) { 01126 $result |= ord( $answer[$i] ) ^ ord( $test[$i] ); 01127 } 01128 $passwordCorrect = ( $result == 0 ); 01129 } 01130 return $passwordCorrect; 01131 } 01132 01139 public function loadFromDatabase() { 01140 // Paranoia 01141 $this->mId = intval( $this->mId ); 01142 01143 // Anonymous user 01144 if ( !$this->mId ) { 01145 $this->loadDefaults(); 01146 return false; 01147 } 01148 01149 $dbr = wfGetDB( DB_MASTER ); 01150 $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ ); 01151 01152 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) ); 01153 01154 if ( $s !== false ) { 01155 // Initialise user table data 01156 $this->loadFromRow( $s ); 01157 $this->mGroups = null; // deferred 01158 $this->getEditCount(); // revalidation for nulls 01159 return true; 01160 } else { 01161 // Invalid user_id 01162 $this->mId = 0; 01163 $this->loadDefaults(); 01164 return false; 01165 } 01166 } 01167 01177 public function loadFromRow( $row, $data = null ) { 01178 $all = true; 01179 01180 $this->mGroups = null; // deferred 01181 01182 if ( isset( $row->user_name ) ) { 01183 $this->mName = $row->user_name; 01184 $this->mFrom = 'name'; 01185 $this->setItemLoaded( 'name' ); 01186 } else { 01187 $all = false; 01188 } 01189 01190 if ( isset( $row->user_real_name ) ) { 01191 $this->mRealName = $row->user_real_name; 01192 $this->setItemLoaded( 'realname' ); 01193 } else { 01194 $all = false; 01195 } 01196 01197 if ( isset( $row->user_id ) ) { 01198 $this->mId = intval( $row->user_id ); 01199 $this->mFrom = 'id'; 01200 $this->setItemLoaded( 'id' ); 01201 } else { 01202 $all = false; 01203 } 01204 01205 if ( isset( $row->user_editcount ) ) { 01206 $this->mEditCount = $row->user_editcount; 01207 } else { 01208 $all = false; 01209 } 01210 01211 if ( isset( $row->user_password ) ) { 01212 $this->mPassword = $row->user_password; 01213 $this->mNewpassword = $row->user_newpassword; 01214 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time ); 01215 $this->mEmail = $row->user_email; 01216 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched ); 01217 $this->mToken = $row->user_token; 01218 if ( $this->mToken == '' ) { 01219 $this->mToken = null; 01220 } 01221 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); 01222 $this->mEmailToken = $row->user_email_token; 01223 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); 01224 $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires ); 01225 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration ); 01226 } else { 01227 $all = false; 01228 } 01229 01230 if ( $all ) { 01231 $this->mLoadedItems = true; 01232 } 01233 01234 if ( is_array( $data ) ) { 01235 if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) { 01236 $this->mGroups = $data['user_groups']; 01237 } 01238 if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) { 01239 $this->loadOptions( $data['user_properties'] ); 01240 } 01241 } 01242 } 01243 01249 protected function loadFromUserObject( $user ) { 01250 $user->load(); 01251 $user->loadGroups(); 01252 $user->loadOptions(); 01253 foreach ( self::$mCacheVars as $var ) { 01254 $this->$var = $user->$var; 01255 } 01256 } 01257 01261 private function loadGroups() { 01262 if ( is_null( $this->mGroups ) ) { 01263 $dbr = wfGetDB( DB_MASTER ); 01264 $res = $dbr->select( 'user_groups', 01265 array( 'ug_group' ), 01266 array( 'ug_user' => $this->mId ), 01267 __METHOD__ ); 01268 $this->mGroups = array(); 01269 foreach ( $res as $row ) { 01270 $this->mGroups[] = $row->ug_group; 01271 } 01272 } 01273 } 01274 01289 public function addAutopromoteOnceGroups( $event ) { 01290 global $wgAutopromoteOnceLogInRC, $wgAuth; 01291 01292 $toPromote = array(); 01293 if ( $this->getId() ) { 01294 $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event ); 01295 if ( count( $toPromote ) ) { 01296 $oldGroups = $this->getGroups(); // previous groups 01297 01298 foreach ( $toPromote as $group ) { 01299 $this->addGroup( $group ); 01300 } 01301 // update groups in external authentication database 01302 $wgAuth->updateExternalDBGroups( $this, $toPromote ); 01303 01304 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups 01305 01306 $logEntry = new ManualLogEntry( 'rights', 'autopromote' ); 01307 $logEntry->setPerformer( $this ); 01308 $logEntry->setTarget( $this->getUserPage() ); 01309 $logEntry->setParameters( array( 01310 '4::oldgroups' => $oldGroups, 01311 '5::newgroups' => $newGroups, 01312 ) ); 01313 $logid = $logEntry->insert(); 01314 if ( $wgAutopromoteOnceLogInRC ) { 01315 $logEntry->publish( $logid ); 01316 } 01317 } 01318 } 01319 return $toPromote; 01320 } 01321 01330 public function clearInstanceCache( $reloadFrom = false ) { 01331 $this->mNewtalk = -1; 01332 $this->mDatePreference = null; 01333 $this->mBlockedby = -1; # Unset 01334 $this->mHash = false; 01335 $this->mRights = null; 01336 $this->mEffectiveGroups = null; 01337 $this->mImplicitGroups = null; 01338 $this->mGroups = null; 01339 $this->mOptions = null; 01340 $this->mOptionsLoaded = false; 01341 $this->mEditCount = null; 01342 01343 if ( $reloadFrom ) { 01344 $this->mLoadedItems = array(); 01345 $this->mFrom = $reloadFrom; 01346 } 01347 } 01348 01355 public static function getDefaultOptions() { 01356 global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin; 01357 01358 static $defOpt = null; 01359 if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) { 01360 // Disabling this for the unit tests, as they rely on being able to change $wgContLang 01361 // mid-request and see that change reflected in the return value of this function. 01362 // Which is insane and would never happen during normal MW operation 01363 return $defOpt; 01364 } 01365 01366 $defOpt = $wgDefaultUserOptions; 01367 // Default language setting 01368 $defOpt['language'] = $wgContLang->getCode(); 01369 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) { 01370 $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode; 01371 } 01372 foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) { 01373 $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] ); 01374 } 01375 $defOpt['skin'] = $wgDefaultSkin; 01376 01377 wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) ); 01378 01379 return $defOpt; 01380 } 01381 01388 public static function getDefaultOption( $opt ) { 01389 $defOpts = self::getDefaultOptions(); 01390 if ( isset( $defOpts[$opt] ) ) { 01391 return $defOpts[$opt]; 01392 } else { 01393 return null; 01394 } 01395 } 01396 01404 private function getBlockedStatus( $bFromSlave = true ) { 01405 global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff; 01406 01407 if ( -1 != $this->mBlockedby ) { 01408 return; 01409 } 01410 01411 wfProfileIn( __METHOD__ ); 01412 wfDebug( __METHOD__ . ": checking...\n" ); 01413 01414 // Initialize data... 01415 // Otherwise something ends up stomping on $this->mBlockedby when 01416 // things get lazy-loaded later, causing false positive block hits 01417 // due to -1 !== 0. Probably session-related... Nothing should be 01418 // overwriting mBlockedby, surely? 01419 $this->load(); 01420 01421 # We only need to worry about passing the IP address to the Block generator if the 01422 # user is not immune to autoblocks/hardblocks, and they are the current user so we 01423 # know which IP address they're actually coming from 01424 if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) { 01425 $ip = $this->getRequest()->getIP(); 01426 } else { 01427 $ip = null; 01428 } 01429 01430 // User/IP blocking 01431 $block = Block::newFromTarget( $this, $ip, !$bFromSlave ); 01432 01433 // Proxy blocking 01434 if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' ) 01435 && !in_array( $ip, $wgProxyWhitelist ) 01436 ) { 01437 // Local list 01438 if ( self::isLocallyBlockedProxy( $ip ) ) { 01439 $block = new Block; 01440 $block->setBlocker( wfMessage( 'proxyblocker' )->text() ); 01441 $block->mReason = wfMessage( 'proxyblockreason' )->text(); 01442 $block->setTarget( $ip ); 01443 } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) { 01444 $block = new Block; 01445 $block->setBlocker( wfMessage( 'sorbs' )->text() ); 01446 $block->mReason = wfMessage( 'sorbsreason' )->text(); 01447 $block->setTarget( $ip ); 01448 } 01449 } 01450 01451 // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled 01452 if ( !$block instanceof Block 01453 && $wgApplyIpBlocksToXff 01454 && $ip !== null 01455 && !$this->isAllowed( 'proxyunbannable' ) 01456 && !in_array( $ip, $wgProxyWhitelist ) 01457 ) { 01458 $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' ); 01459 $xff = array_map( 'trim', explode( ',', $xff ) ); 01460 $xff = array_diff( $xff, array( $ip ) ); 01461 $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave ); 01462 $block = Block::chooseBlock( $xffblocks, $xff ); 01463 if ( $block instanceof Block ) { 01464 # Mangle the reason to alert the user that the block 01465 # originated from matching the X-Forwarded-For header. 01466 $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text(); 01467 } 01468 } 01469 01470 if ( $block instanceof Block ) { 01471 wfDebug( __METHOD__ . ": Found block.\n" ); 01472 $this->mBlock = $block; 01473 $this->mBlockedby = $block->getByName(); 01474 $this->mBlockreason = $block->mReason; 01475 $this->mHideName = $block->mHideName; 01476 $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' ); 01477 } else { 01478 $this->mBlockedby = ''; 01479 $this->mHideName = 0; 01480 $this->mAllowUsertalk = false; 01481 } 01482 01483 // Extensions 01484 wfRunHooks( 'GetBlockedStatus', array( &$this ) ); 01485 01486 wfProfileOut( __METHOD__ ); 01487 } 01488 01496 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) { 01497 global $wgEnableSorbs, $wgEnableDnsBlacklist, 01498 $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist; 01499 01500 if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs ) { 01501 return false; 01502 } 01503 01504 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) { 01505 return false; 01506 } 01507 01508 $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl ); 01509 return $this->inDnsBlacklist( $ip, $urls ); 01510 } 01511 01519 public function inDnsBlacklist( $ip, $bases ) { 01520 wfProfileIn( __METHOD__ ); 01521 01522 $found = false; 01523 // @todo FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170) 01524 if ( IP::isIPv4( $ip ) ) { 01525 // Reverse IP, bug 21255 01526 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) ); 01527 01528 foreach ( (array)$bases as $base ) { 01529 // Make hostname 01530 // If we have an access key, use that too (ProjectHoneypot, etc.) 01531 if ( is_array( $base ) ) { 01532 if ( count( $base ) >= 2 ) { 01533 // Access key is 1, base URL is 0 01534 $host = "{$base[1]}.$ipReversed.{$base[0]}"; 01535 } else { 01536 $host = "$ipReversed.{$base[0]}"; 01537 } 01538 } else { 01539 $host = "$ipReversed.$base"; 01540 } 01541 01542 // Send query 01543 $ipList = gethostbynamel( $host ); 01544 01545 if ( $ipList ) { 01546 wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!" ); 01547 $found = true; 01548 break; 01549 } else { 01550 wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base." ); 01551 } 01552 } 01553 } 01554 01555 wfProfileOut( __METHOD__ ); 01556 return $found; 01557 } 01558 01566 public static function isLocallyBlockedProxy( $ip ) { 01567 global $wgProxyList; 01568 01569 if ( !$wgProxyList ) { 01570 return false; 01571 } 01572 wfProfileIn( __METHOD__ ); 01573 01574 if ( !is_array( $wgProxyList ) ) { 01575 // Load from the specified file 01576 $wgProxyList = array_map( 'trim', file( $wgProxyList ) ); 01577 } 01578 01579 if ( !is_array( $wgProxyList ) ) { 01580 $ret = false; 01581 } elseif ( array_search( $ip, $wgProxyList ) !== false ) { 01582 $ret = true; 01583 } elseif ( array_key_exists( $ip, $wgProxyList ) ) { 01584 // Old-style flipped proxy list 01585 $ret = true; 01586 } else { 01587 $ret = false; 01588 } 01589 wfProfileOut( __METHOD__ ); 01590 return $ret; 01591 } 01592 01598 public function isPingLimitable() { 01599 global $wgRateLimitsExcludedIPs; 01600 if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) { 01601 // No other good way currently to disable rate limits 01602 // for specific IPs. :P 01603 // But this is a crappy hack and should die. 01604 return false; 01605 } 01606 return !$this->isAllowed( 'noratelimit' ); 01607 } 01608 01620 public function pingLimiter( $action = 'edit', $incrBy = 1 ) { 01621 // Call the 'PingLimiter' hook 01622 $result = false; 01623 if ( !wfRunHooks( 'PingLimiter', array( &$this, $action, &$result, $incrBy ) ) ) { 01624 return $result; 01625 } 01626 01627 global $wgRateLimits; 01628 if ( !isset( $wgRateLimits[$action] ) ) { 01629 return false; 01630 } 01631 01632 // Some groups shouldn't trigger the ping limiter, ever 01633 if ( !$this->isPingLimitable() ) { 01634 return false; 01635 } 01636 01637 global $wgMemc; 01638 wfProfileIn( __METHOD__ ); 01639 01640 $limits = $wgRateLimits[$action]; 01641 $keys = array(); 01642 $id = $this->getId(); 01643 $userLimit = false; 01644 01645 if ( isset( $limits['anon'] ) && $id == 0 ) { 01646 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon']; 01647 } 01648 01649 if ( isset( $limits['user'] ) && $id != 0 ) { 01650 $userLimit = $limits['user']; 01651 } 01652 if ( $this->isNewbie() ) { 01653 if ( isset( $limits['newbie'] ) && $id != 0 ) { 01654 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie']; 01655 } 01656 if ( isset( $limits['ip'] ) ) { 01657 $ip = $this->getRequest()->getIP(); 01658 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip']; 01659 } 01660 if ( isset( $limits['subnet'] ) ) { 01661 $ip = $this->getRequest()->getIP(); 01662 $matches = array(); 01663 $subnet = false; 01664 if ( IP::isIPv6( $ip ) ) { 01665 $parts = IP::parseRange( "$ip/64" ); 01666 $subnet = $parts[0]; 01667 } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) { 01668 // IPv4 01669 $subnet = $matches[1]; 01670 } 01671 if ( $subnet !== false ) { 01672 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet']; 01673 } 01674 } 01675 } 01676 // Check for group-specific permissions 01677 // If more than one group applies, use the group with the highest limit 01678 foreach ( $this->getGroups() as $group ) { 01679 if ( isset( $limits[$group] ) ) { 01680 if ( $userLimit === false || $limits[$group] > $userLimit ) { 01681 $userLimit = $limits[$group]; 01682 } 01683 } 01684 } 01685 // Set the user limit key 01686 if ( $userLimit !== false ) { 01687 list( $max, $period ) = $userLimit; 01688 wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" ); 01689 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit; 01690 } 01691 01692 $triggered = false; 01693 foreach ( $keys as $key => $limit ) { 01694 list( $max, $period ) = $limit; 01695 $summary = "(limit $max in {$period}s)"; 01696 $count = $wgMemc->get( $key ); 01697 // Already pinged? 01698 if ( $count ) { 01699 if ( $count >= $max ) { 01700 wfDebugLog( 'ratelimit', $this->getName() . " tripped! $key at $count $summary" ); 01701 $triggered = true; 01702 } else { 01703 wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" ); 01704 } 01705 } else { 01706 wfDebug( __METHOD__ . ": adding record for $key $summary\n" ); 01707 if ( $incrBy > 0 ) { 01708 $wgMemc->add( $key, 0, intval( $period ) ); // first ping 01709 } 01710 } 01711 if ( $incrBy > 0 ) { 01712 $wgMemc->incr( $key, $incrBy ); 01713 } 01714 } 01715 01716 wfProfileOut( __METHOD__ ); 01717 return $triggered; 01718 } 01719 01726 public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site 01727 return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' ); 01728 } 01729 01736 public function getBlock( $bFromSlave = true ) { 01737 $this->getBlockedStatus( $bFromSlave ); 01738 return $this->mBlock instanceof Block ? $this->mBlock : null; 01739 } 01740 01748 public function isBlockedFrom( $title, $bFromSlave = false ) { 01749 global $wgBlockAllowsUTEdit; 01750 wfProfileIn( __METHOD__ ); 01751 01752 $blocked = $this->isBlocked( $bFromSlave ); 01753 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false ); 01754 // If a user's name is suppressed, they cannot make edits anywhere 01755 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() 01756 && $title->getNamespace() == NS_USER_TALK ) { 01757 $blocked = false; 01758 wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" ); 01759 } 01760 01761 wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) ); 01762 01763 wfProfileOut( __METHOD__ ); 01764 return $blocked; 01765 } 01766 01771 public function blockedBy() { 01772 $this->getBlockedStatus(); 01773 return $this->mBlockedby; 01774 } 01775 01780 public function blockedFor() { 01781 $this->getBlockedStatus(); 01782 return $this->mBlockreason; 01783 } 01784 01789 public function getBlockId() { 01790 $this->getBlockedStatus(); 01791 return ( $this->mBlock ? $this->mBlock->getId() : false ); 01792 } 01793 01802 public function isBlockedGlobally( $ip = '' ) { 01803 if ( $this->mBlockedGlobally !== null ) { 01804 return $this->mBlockedGlobally; 01805 } 01806 // User is already an IP? 01807 if ( IP::isIPAddress( $this->getName() ) ) { 01808 $ip = $this->getName(); 01809 } elseif ( !$ip ) { 01810 $ip = $this->getRequest()->getIP(); 01811 } 01812 $blocked = false; 01813 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) ); 01814 $this->mBlockedGlobally = (bool)$blocked; 01815 return $this->mBlockedGlobally; 01816 } 01817 01823 public function isLocked() { 01824 if ( $this->mLocked !== null ) { 01825 return $this->mLocked; 01826 } 01827 global $wgAuth; 01828 StubObject::unstub( $wgAuth ); 01829 $authUser = $wgAuth->getUserInstance( $this ); 01830 $this->mLocked = (bool)$authUser->isLocked(); 01831 return $this->mLocked; 01832 } 01833 01839 public function isHidden() { 01840 if ( $this->mHideName !== null ) { 01841 return $this->mHideName; 01842 } 01843 $this->getBlockedStatus(); 01844 if ( !$this->mHideName ) { 01845 global $wgAuth; 01846 StubObject::unstub( $wgAuth ); 01847 $authUser = $wgAuth->getUserInstance( $this ); 01848 $this->mHideName = (bool)$authUser->isHidden(); 01849 } 01850 return $this->mHideName; 01851 } 01852 01857 public function getId() { 01858 if ( $this->mId === null && $this->mName !== null && User::isIP( $this->mName ) ) { 01859 // Special case, we know the user is anonymous 01860 return 0; 01861 } elseif ( !$this->isItemLoaded( 'id' ) ) { 01862 // Don't load if this was initialized from an ID 01863 $this->load(); 01864 } 01865 return $this->mId; 01866 } 01867 01872 public function setId( $v ) { 01873 $this->mId = $v; 01874 $this->clearInstanceCache( 'id' ); 01875 } 01876 01881 public function getName() { 01882 if ( $this->isItemLoaded( 'name', 'only' ) ) { 01883 // Special case optimisation 01884 return $this->mName; 01885 } else { 01886 $this->load(); 01887 if ( $this->mName === false ) { 01888 // Clean up IPs 01889 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() ); 01890 } 01891 return $this->mName; 01892 } 01893 } 01894 01908 public function setName( $str ) { 01909 $this->load(); 01910 $this->mName = $str; 01911 } 01912 01917 public function getTitleKey() { 01918 return str_replace( ' ', '_', $this->getName() ); 01919 } 01920 01925 public function getNewtalk() { 01926 $this->load(); 01927 01928 // Load the newtalk status if it is unloaded (mNewtalk=-1) 01929 if ( $this->mNewtalk === -1 ) { 01930 $this->mNewtalk = false; # reset talk page status 01931 01932 // Check memcached separately for anons, who have no 01933 // entire User object stored in there. 01934 if ( !$this->mId ) { 01935 global $wgDisableAnonTalk; 01936 if ( $wgDisableAnonTalk ) { 01937 // Anon newtalk disabled by configuration. 01938 $this->mNewtalk = false; 01939 } else { 01940 global $wgMemc; 01941 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); 01942 $newtalk = $wgMemc->get( $key ); 01943 if ( strval( $newtalk ) !== '' ) { 01944 $this->mNewtalk = (bool)$newtalk; 01945 } else { 01946 // Since we are caching this, make sure it is up to date by getting it 01947 // from the master 01948 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true ); 01949 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 ); 01950 } 01951 } 01952 } else { 01953 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId ); 01954 } 01955 } 01956 01957 return (bool)$this->mNewtalk; 01958 } 01959 01973 public function getNewMessageLinks() { 01974 $talks = array(); 01975 if ( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) { 01976 return $talks; 01977 } elseif ( !$this->getNewtalk() ) { 01978 return array(); 01979 } 01980 $utp = $this->getTalkPage(); 01981 $dbr = wfGetDB( DB_SLAVE ); 01982 // Get the "last viewed rev" timestamp from the oldest message notification 01983 $timestamp = $dbr->selectField( 'user_newtalk', 01984 'MIN(user_last_timestamp)', 01985 $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ), 01986 __METHOD__ ); 01987 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null; 01988 return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) ); 01989 } 01990 01996 public function getNewMessageRevisionId() { 01997 $newMessageRevisionId = null; 01998 $newMessageLinks = $this->getNewMessageLinks(); 01999 if ( $newMessageLinks ) { 02000 // Note: getNewMessageLinks() never returns more than a single link 02001 // and it is always for the same wiki, but we double-check here in 02002 // case that changes some time in the future. 02003 if ( count( $newMessageLinks ) === 1 02004 && $newMessageLinks[0]['wiki'] === wfWikiID() 02005 && $newMessageLinks[0]['rev'] 02006 ) { 02007 $newMessageRevision = $newMessageLinks[0]['rev']; 02008 $newMessageRevisionId = $newMessageRevision->getId(); 02009 } 02010 } 02011 return $newMessageRevisionId; 02012 } 02013 02023 protected function checkNewtalk( $field, $id, $fromMaster = false ) { 02024 if ( $fromMaster ) { 02025 $db = wfGetDB( DB_MASTER ); 02026 } else { 02027 $db = wfGetDB( DB_SLAVE ); 02028 } 02029 $ok = $db->selectField( 'user_newtalk', $field, 02030 array( $field => $id ), __METHOD__ ); 02031 return $ok !== false; 02032 } 02033 02041 protected function updateNewtalk( $field, $id, $curRev = null ) { 02042 // Get timestamp of the talk page revision prior to the current one 02043 $prevRev = $curRev ? $curRev->getPrevious() : false; 02044 $ts = $prevRev ? $prevRev->getTimestamp() : null; 02045 // Mark the user as having new messages since this revision 02046 $dbw = wfGetDB( DB_MASTER ); 02047 $dbw->insert( 'user_newtalk', 02048 array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ), 02049 __METHOD__, 02050 'IGNORE' ); 02051 if ( $dbw->affectedRows() ) { 02052 wfDebug( __METHOD__ . ": set on ($field, $id)\n" ); 02053 return true; 02054 } else { 02055 wfDebug( __METHOD__ . " already set ($field, $id)\n" ); 02056 return false; 02057 } 02058 } 02059 02066 protected function deleteNewtalk( $field, $id ) { 02067 $dbw = wfGetDB( DB_MASTER ); 02068 $dbw->delete( 'user_newtalk', 02069 array( $field => $id ), 02070 __METHOD__ ); 02071 if ( $dbw->affectedRows() ) { 02072 wfDebug( __METHOD__ . ": killed on ($field, $id)\n" ); 02073 return true; 02074 } else { 02075 wfDebug( __METHOD__ . ": already gone ($field, $id)\n" ); 02076 return false; 02077 } 02078 } 02079 02085 public function setNewtalk( $val, $curRev = null ) { 02086 if ( wfReadOnly() ) { 02087 return; 02088 } 02089 02090 $this->load(); 02091 $this->mNewtalk = $val; 02092 02093 if ( $this->isAnon() ) { 02094 $field = 'user_ip'; 02095 $id = $this->getName(); 02096 } else { 02097 $field = 'user_id'; 02098 $id = $this->getId(); 02099 } 02100 global $wgMemc; 02101 02102 if ( $val ) { 02103 $changed = $this->updateNewtalk( $field, $id, $curRev ); 02104 } else { 02105 $changed = $this->deleteNewtalk( $field, $id ); 02106 } 02107 02108 if ( $this->isAnon() ) { 02109 // Anons have a separate memcached space, since 02110 // user records aren't kept for them. 02111 $key = wfMemcKey( 'newtalk', 'ip', $id ); 02112 $wgMemc->set( $key, $val ? 1 : 0, 1800 ); 02113 } 02114 if ( $changed ) { 02115 $this->invalidateCache(); 02116 } 02117 } 02118 02124 private static function newTouchedTimestamp() { 02125 global $wgClockSkewFudge; 02126 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge ); 02127 } 02128 02136 private function clearSharedCache() { 02137 $this->load(); 02138 if ( $this->mId ) { 02139 global $wgMemc; 02140 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) ); 02141 } 02142 } 02143 02149 public function invalidateCache() { 02150 if ( wfReadOnly() ) { 02151 return; 02152 } 02153 $this->load(); 02154 if ( $this->mId ) { 02155 $this->mTouched = self::newTouchedTimestamp(); 02156 02157 $dbw = wfGetDB( DB_MASTER ); 02158 $userid = $this->mId; 02159 $touched = $this->mTouched; 02160 $method = __METHOD__; 02161 $dbw->onTransactionIdle( function() use ( $dbw, $userid, $touched, $method ) { 02162 // Prevent contention slams by checking user_touched first 02163 $encTouched = $dbw->addQuotes( $dbw->timestamp( $touched ) ); 02164 $needsPurge = $dbw->selectField( 'user', '1', 02165 array( 'user_id' => $userid, 'user_touched < ' . $encTouched ) ); 02166 if ( $needsPurge ) { 02167 $dbw->update( 'user', 02168 array( 'user_touched' => $dbw->timestamp( $touched ) ), 02169 array( 'user_id' => $userid, 'user_touched < ' . $encTouched ), 02170 $method 02171 ); 02172 } 02173 } ); 02174 $this->clearSharedCache(); 02175 } 02176 } 02177 02183 public function validateCache( $timestamp ) { 02184 $this->load(); 02185 return ( $timestamp >= $this->mTouched ); 02186 } 02187 02192 public function getTouched() { 02193 $this->load(); 02194 return $this->mTouched; 02195 } 02196 02213 public function setPassword( $str ) { 02214 global $wgAuth; 02215 02216 if ( $str !== null ) { 02217 if ( !$wgAuth->allowPasswordChange() ) { 02218 throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() ); 02219 } 02220 02221 if ( !$this->isValidPassword( $str ) ) { 02222 global $wgMinimalPasswordLength; 02223 $valid = $this->getPasswordValidity( $str ); 02224 if ( is_array( $valid ) ) { 02225 $message = array_shift( $valid ); 02226 $params = $valid; 02227 } else { 02228 $message = $valid; 02229 $params = array( $wgMinimalPasswordLength ); 02230 } 02231 throw new PasswordError( wfMessage( $message, $params )->text() ); 02232 } 02233 } 02234 02235 if ( !$wgAuth->setPassword( $this, $str ) ) { 02236 throw new PasswordError( wfMessage( 'externaldberror' )->text() ); 02237 } 02238 02239 $this->setInternalPassword( $str ); 02240 02241 return true; 02242 } 02243 02251 public function setInternalPassword( $str ) { 02252 $this->load(); 02253 $this->setToken(); 02254 02255 if ( $str === null ) { 02256 // Save an invalid hash... 02257 $this->mPassword = ''; 02258 } else { 02259 $this->mPassword = self::crypt( $str ); 02260 } 02261 $this->mNewpassword = ''; 02262 $this->mNewpassTime = null; 02263 } 02264 02270 public function getToken( $forceCreation = true ) { 02271 $this->load(); 02272 if ( !$this->mToken && $forceCreation ) { 02273 $this->setToken(); 02274 } 02275 return $this->mToken; 02276 } 02277 02284 public function setToken( $token = false ) { 02285 $this->load(); 02286 if ( !$token ) { 02287 $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH ); 02288 } else { 02289 $this->mToken = $token; 02290 } 02291 } 02292 02300 public function setNewpassword( $str, $throttle = true ) { 02301 $this->load(); 02302 02303 if ( $str === null ) { 02304 $this->mNewpassword = ''; 02305 $this->mNewpassTime = null; 02306 } else { 02307 $this->mNewpassword = self::crypt( $str ); 02308 if ( $throttle ) { 02309 $this->mNewpassTime = wfTimestampNow(); 02310 } 02311 } 02312 } 02313 02319 public function isPasswordReminderThrottled() { 02320 global $wgPasswordReminderResendTime; 02321 $this->load(); 02322 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) { 02323 return false; 02324 } 02325 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600; 02326 return time() < $expiry; 02327 } 02328 02333 public function getEmail() { 02334 $this->load(); 02335 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) ); 02336 return $this->mEmail; 02337 } 02338 02343 public function getEmailAuthenticationTimestamp() { 02344 $this->load(); 02345 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) ); 02346 return $this->mEmailAuthenticated; 02347 } 02348 02353 public function setEmail( $str ) { 02354 $this->load(); 02355 if ( $str == $this->mEmail ) { 02356 return; 02357 } 02358 $this->mEmail = $str; 02359 $this->invalidateEmail(); 02360 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) ); 02361 } 02362 02370 public function setEmailWithConfirmation( $str ) { 02371 global $wgEnableEmail, $wgEmailAuthentication; 02372 02373 if ( !$wgEnableEmail ) { 02374 return Status::newFatal( 'emaildisabled' ); 02375 } 02376 02377 $oldaddr = $this->getEmail(); 02378 if ( $str === $oldaddr ) { 02379 return Status::newGood( true ); 02380 } 02381 02382 $this->setEmail( $str ); 02383 02384 if ( $str !== '' && $wgEmailAuthentication ) { 02385 // Send a confirmation request to the new address if needed 02386 $type = $oldaddr != '' ? 'changed' : 'set'; 02387 $result = $this->sendConfirmationMail( $type ); 02388 if ( $result->isGood() ) { 02389 // Say the the caller that a confirmation mail has been sent 02390 $result->value = 'eauth'; 02391 } 02392 } else { 02393 $result = Status::newGood( true ); 02394 } 02395 02396 return $result; 02397 } 02398 02403 public function getRealName() { 02404 if ( !$this->isItemLoaded( 'realname' ) ) { 02405 $this->load(); 02406 } 02407 02408 return $this->mRealName; 02409 } 02410 02415 public function setRealName( $str ) { 02416 $this->load(); 02417 $this->mRealName = $str; 02418 } 02419 02430 public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) { 02431 global $wgHiddenPrefs; 02432 $this->loadOptions(); 02433 02434 # We want 'disabled' preferences to always behave as the default value for 02435 # users, even if they have set the option explicitly in their settings (ie they 02436 # set it, and then it was disabled removing their ability to change it). But 02437 # we don't want to erase the preferences in the database in case the preference 02438 # is re-enabled again. So don't touch $mOptions, just override the returned value 02439 if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) { 02440 return self::getDefaultOption( $oname ); 02441 } 02442 02443 if ( array_key_exists( $oname, $this->mOptions ) ) { 02444 return $this->mOptions[$oname]; 02445 } else { 02446 return $defaultOverride; 02447 } 02448 } 02449 02455 public function getOptions() { 02456 global $wgHiddenPrefs; 02457 $this->loadOptions(); 02458 $options = $this->mOptions; 02459 02460 # We want 'disabled' preferences to always behave as the default value for 02461 # users, even if they have set the option explicitly in their settings (ie they 02462 # set it, and then it was disabled removing their ability to change it). But 02463 # we don't want to erase the preferences in the database in case the preference 02464 # is re-enabled again. So don't touch $mOptions, just override the returned value 02465 foreach ( $wgHiddenPrefs as $pref ) { 02466 $default = self::getDefaultOption( $pref ); 02467 if ( $default !== null ) { 02468 $options[$pref] = $default; 02469 } 02470 } 02471 02472 return $options; 02473 } 02474 02482 public function getBoolOption( $oname ) { 02483 return (bool)$this->getOption( $oname ); 02484 } 02485 02494 public function getIntOption( $oname, $defaultOverride = 0 ) { 02495 $val = $this->getOption( $oname ); 02496 if ( $val == '' ) { 02497 $val = $defaultOverride; 02498 } 02499 return intval( $val ); 02500 } 02501 02508 public function setOption( $oname, $val ) { 02509 $this->loadOptions(); 02510 02511 // Explicitly NULL values should refer to defaults 02512 if ( is_null( $val ) ) { 02513 $val = self::getDefaultOption( $oname ); 02514 } 02515 02516 $this->mOptions[$oname] = $val; 02517 } 02518 02528 public function getTokenFromOption( $oname ) { 02529 global $wgHiddenPrefs; 02530 if ( in_array( $oname, $wgHiddenPrefs ) ) { 02531 return false; 02532 } 02533 02534 $token = $this->getOption( $oname ); 02535 if ( !$token ) { 02536 $token = $this->resetTokenFromOption( $oname ); 02537 $this->saveSettings(); 02538 } 02539 return $token; 02540 } 02541 02551 public function resetTokenFromOption( $oname ) { 02552 global $wgHiddenPrefs; 02553 if ( in_array( $oname, $wgHiddenPrefs ) ) { 02554 return false; 02555 } 02556 02557 $token = MWCryptRand::generateHex( 40 ); 02558 $this->setOption( $oname, $token ); 02559 return $token; 02560 } 02561 02585 public static function listOptionKinds() { 02586 return array( 02587 'registered', 02588 'registered-multiselect', 02589 'registered-checkmatrix', 02590 'userjs', 02591 'special', 02592 'unused' 02593 ); 02594 } 02595 02607 public function getOptionKinds( IContextSource $context, $options = null ) { 02608 $this->loadOptions(); 02609 if ( $options === null ) { 02610 $options = $this->mOptions; 02611 } 02612 02613 $prefs = Preferences::getPreferences( $this, $context ); 02614 $mapping = array(); 02615 02616 // Pull out the "special" options, so they don't get converted as 02617 // multiselect or checkmatrix. 02618 $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true ); 02619 foreach ( $specialOptions as $name => $value ) { 02620 unset( $prefs[$name] ); 02621 } 02622 02623 // Multiselect and checkmatrix options are stored in the database with 02624 // one key per option, each having a boolean value. Extract those keys. 02625 $multiselectOptions = array(); 02626 foreach ( $prefs as $name => $info ) { 02627 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) || 02628 ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) { 02629 $opts = HTMLFormField::flattenOptions( $info['options'] ); 02630 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name; 02631 02632 foreach ( $opts as $value ) { 02633 $multiselectOptions["$prefix$value"] = true; 02634 } 02635 02636 unset( $prefs[$name] ); 02637 } 02638 } 02639 $checkmatrixOptions = array(); 02640 foreach ( $prefs as $name => $info ) { 02641 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) || 02642 ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) { 02643 $columns = HTMLFormField::flattenOptions( $info['columns'] ); 02644 $rows = HTMLFormField::flattenOptions( $info['rows'] ); 02645 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name; 02646 02647 foreach ( $columns as $column ) { 02648 foreach ( $rows as $row ) { 02649 $checkmatrixOptions["$prefix-$column-$row"] = true; 02650 } 02651 } 02652 02653 unset( $prefs[$name] ); 02654 } 02655 } 02656 02657 // $value is ignored 02658 foreach ( $options as $key => $value ) { 02659 if ( isset( $prefs[$key] ) ) { 02660 $mapping[$key] = 'registered'; 02661 } elseif ( isset( $multiselectOptions[$key] ) ) { 02662 $mapping[$key] = 'registered-multiselect'; 02663 } elseif ( isset( $checkmatrixOptions[$key] ) ) { 02664 $mapping[$key] = 'registered-checkmatrix'; 02665 } elseif ( isset( $specialOptions[$key] ) ) { 02666 $mapping[$key] = 'special'; 02667 } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) { 02668 $mapping[$key] = 'userjs'; 02669 } else { 02670 $mapping[$key] = 'unused'; 02671 } 02672 } 02673 02674 return $mapping; 02675 } 02676 02691 public function resetOptions( 02692 $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ), 02693 IContextSource $context = null 02694 ) { 02695 $this->load(); 02696 $defaultOptions = self::getDefaultOptions(); 02697 02698 if ( !is_array( $resetKinds ) ) { 02699 $resetKinds = array( $resetKinds ); 02700 } 02701 02702 if ( in_array( 'all', $resetKinds ) ) { 02703 $newOptions = $defaultOptions; 02704 } else { 02705 if ( $context === null ) { 02706 $context = RequestContext::getMain(); 02707 } 02708 02709 $optionKinds = $this->getOptionKinds( $context ); 02710 $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() ); 02711 $newOptions = array(); 02712 02713 // Use default values for the options that should be deleted, and 02714 // copy old values for the ones that shouldn't. 02715 foreach ( $this->mOptions as $key => $value ) { 02716 if ( in_array( $optionKinds[$key], $resetKinds ) ) { 02717 if ( array_key_exists( $key, $defaultOptions ) ) { 02718 $newOptions[$key] = $defaultOptions[$key]; 02719 } 02720 } else { 02721 $newOptions[$key] = $value; 02722 } 02723 } 02724 } 02725 02726 $this->mOptions = $newOptions; 02727 $this->mOptionsLoaded = true; 02728 } 02729 02734 public function getDatePreference() { 02735 // Important migration for old data rows 02736 if ( is_null( $this->mDatePreference ) ) { 02737 global $wgLang; 02738 $value = $this->getOption( 'date' ); 02739 $map = $wgLang->getDatePreferenceMigrationMap(); 02740 if ( isset( $map[$value] ) ) { 02741 $value = $map[$value]; 02742 } 02743 $this->mDatePreference = $value; 02744 } 02745 return $this->mDatePreference; 02746 } 02747 02754 public function requiresHTTPS() { 02755 global $wgSecureLogin; 02756 if ( !$wgSecureLogin ) { 02757 return false; 02758 } else { 02759 $https = $this->getBoolOption( 'prefershttps' ); 02760 wfRunHooks( 'UserRequiresHTTPS', array( $this, &$https ) ); 02761 if ( $https ) { 02762 $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() ); 02763 } 02764 return $https; 02765 } 02766 } 02767 02773 public function getStubThreshold() { 02774 global $wgMaxArticleSize; # Maximum article size, in Kb 02775 $threshold = $this->getIntOption( 'stubthreshold' ); 02776 if ( $threshold > $wgMaxArticleSize * 1024 ) { 02777 // If they have set an impossible value, disable the preference 02778 // so we can use the parser cache again. 02779 $threshold = 0; 02780 } 02781 return $threshold; 02782 } 02783 02788 public function getRights() { 02789 if ( is_null( $this->mRights ) ) { 02790 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); 02791 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) ); 02792 // Force reindexation of rights when a hook has unset one of them 02793 $this->mRights = array_values( array_unique( $this->mRights ) ); 02794 } 02795 return $this->mRights; 02796 } 02797 02803 public function getGroups() { 02804 $this->load(); 02805 $this->loadGroups(); 02806 return $this->mGroups; 02807 } 02808 02816 public function getEffectiveGroups( $recache = false ) { 02817 if ( $recache || is_null( $this->mEffectiveGroups ) ) { 02818 wfProfileIn( __METHOD__ ); 02819 $this->mEffectiveGroups = array_unique( array_merge( 02820 $this->getGroups(), // explicit groups 02821 $this->getAutomaticGroups( $recache ) // implicit groups 02822 ) ); 02823 // Hook for additional groups 02824 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) ); 02825 // Force reindexation of groups when a hook has unset one of them 02826 $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) ); 02827 wfProfileOut( __METHOD__ ); 02828 } 02829 return $this->mEffectiveGroups; 02830 } 02831 02839 public function getAutomaticGroups( $recache = false ) { 02840 if ( $recache || is_null( $this->mImplicitGroups ) ) { 02841 wfProfileIn( __METHOD__ ); 02842 $this->mImplicitGroups = array( '*' ); 02843 if ( $this->getId() ) { 02844 $this->mImplicitGroups[] = 'user'; 02845 02846 $this->mImplicitGroups = array_unique( array_merge( 02847 $this->mImplicitGroups, 02848 Autopromote::getAutopromoteGroups( $this ) 02849 ) ); 02850 } 02851 if ( $recache ) { 02852 // Assure data consistency with rights/groups, 02853 // as getEffectiveGroups() depends on this function 02854 $this->mEffectiveGroups = null; 02855 } 02856 wfProfileOut( __METHOD__ ); 02857 } 02858 return $this->mImplicitGroups; 02859 } 02860 02870 public function getFormerGroups() { 02871 if ( is_null( $this->mFormerGroups ) ) { 02872 $dbr = wfGetDB( DB_MASTER ); 02873 $res = $dbr->select( 'user_former_groups', 02874 array( 'ufg_group' ), 02875 array( 'ufg_user' => $this->mId ), 02876 __METHOD__ ); 02877 $this->mFormerGroups = array(); 02878 foreach ( $res as $row ) { 02879 $this->mFormerGroups[] = $row->ufg_group; 02880 } 02881 } 02882 return $this->mFormerGroups; 02883 } 02884 02889 public function getEditCount() { 02890 if ( !$this->getId() ) { 02891 return null; 02892 } 02893 02894 if ( !isset( $this->mEditCount ) ) { 02895 /* Populate the count, if it has not been populated yet */ 02896 wfProfileIn( __METHOD__ ); 02897 $dbr = wfGetDB( DB_SLAVE ); 02898 // check if the user_editcount field has been initialized 02899 $count = $dbr->selectField( 02900 'user', 'user_editcount', 02901 array( 'user_id' => $this->mId ), 02902 __METHOD__ 02903 ); 02904 02905 if ( $count === null ) { 02906 // it has not been initialized. do so. 02907 $count = $this->initEditCount(); 02908 } 02909 $this->mEditCount = $count; 02910 wfProfileOut( __METHOD__ ); 02911 } 02912 return (int)$this->mEditCount; 02913 } 02914 02920 public function addGroup( $group ) { 02921 if ( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) { 02922 $dbw = wfGetDB( DB_MASTER ); 02923 if ( $this->getId() ) { 02924 $dbw->insert( 'user_groups', 02925 array( 02926 'ug_user' => $this->getID(), 02927 'ug_group' => $group, 02928 ), 02929 __METHOD__, 02930 array( 'IGNORE' ) ); 02931 } 02932 } 02933 $this->loadGroups(); 02934 $this->mGroups[] = $group; 02935 // In case loadGroups was not called before, we now have the right twice. 02936 // Get rid of the duplicate. 02937 $this->mGroups = array_unique( $this->mGroups ); 02938 02939 // Refresh the groups caches, and clear the rights cache so it will be 02940 // refreshed on the next call to $this->getRights(). 02941 $this->getEffectiveGroups( true ); 02942 $this->mRights = null; 02943 02944 $this->invalidateCache(); 02945 } 02946 02952 public function removeGroup( $group ) { 02953 $this->load(); 02954 if ( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) { 02955 $dbw = wfGetDB( DB_MASTER ); 02956 $dbw->delete( 'user_groups', 02957 array( 02958 'ug_user' => $this->getID(), 02959 'ug_group' => $group, 02960 ), __METHOD__ ); 02961 // Remember that the user was in this group 02962 $dbw->insert( 'user_former_groups', 02963 array( 02964 'ufg_user' => $this->getID(), 02965 'ufg_group' => $group, 02966 ), 02967 __METHOD__, 02968 array( 'IGNORE' ) ); 02969 } 02970 $this->loadGroups(); 02971 $this->mGroups = array_diff( $this->mGroups, array( $group ) ); 02972 02973 // Refresh the groups caches, and clear the rights cache so it will be 02974 // refreshed on the next call to $this->getRights(). 02975 $this->getEffectiveGroups( true ); 02976 $this->mRights = null; 02977 02978 $this->invalidateCache(); 02979 } 02980 02985 public function isLoggedIn() { 02986 return $this->getID() != 0; 02987 } 02988 02993 public function isAnon() { 02994 return !$this->isLoggedIn(); 02995 } 02996 03005 public function isAllowedAny( /*...*/ ) { 03006 $permissions = func_get_args(); 03007 foreach ( $permissions as $permission ) { 03008 if ( $this->isAllowed( $permission ) ) { 03009 return true; 03010 } 03011 } 03012 return false; 03013 } 03014 03020 public function isAllowedAll( /*...*/ ) { 03021 $permissions = func_get_args(); 03022 foreach ( $permissions as $permission ) { 03023 if ( !$this->isAllowed( $permission ) ) { 03024 return false; 03025 } 03026 } 03027 return true; 03028 } 03029 03035 public function isAllowed( $action = '' ) { 03036 if ( $action === '' ) { 03037 return true; // In the spirit of DWIM 03038 } 03039 // Patrolling may not be enabled 03040 if ( $action === 'patrol' || $action === 'autopatrol' ) { 03041 global $wgUseRCPatrol, $wgUseNPPatrol; 03042 if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) { 03043 return false; 03044 } 03045 } 03046 // Use strict parameter to avoid matching numeric 0 accidentally inserted 03047 // by misconfiguration: 0 == 'foo' 03048 return in_array( $action, $this->getRights(), true ); 03049 } 03050 03055 public function useRCPatrol() { 03056 global $wgUseRCPatrol; 03057 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' ); 03058 } 03059 03064 public function useNPPatrol() { 03065 global $wgUseRCPatrol, $wgUseNPPatrol; 03066 return ( 03067 ( $wgUseRCPatrol || $wgUseNPPatrol ) 03068 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) 03069 ); 03070 } 03071 03077 public function getRequest() { 03078 if ( $this->mRequest ) { 03079 return $this->mRequest; 03080 } else { 03081 global $wgRequest; 03082 return $wgRequest; 03083 } 03084 } 03085 03092 public function getSkin() { 03093 wfDeprecated( __METHOD__, '1.18' ); 03094 return RequestContext::getMain()->getSkin(); 03095 } 03096 03106 public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) { 03107 $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey(); 03108 03109 if ( isset( $this->mWatchedItems[$key] ) ) { 03110 return $this->mWatchedItems[$key]; 03111 } 03112 03113 if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) { 03114 $this->mWatchedItems = array(); 03115 } 03116 03117 $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights ); 03118 return $this->mWatchedItems[$key]; 03119 } 03120 03129 public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) { 03130 return $this->getWatchedItem( $title, $checkRights )->isWatched(); 03131 } 03132 03140 public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) { 03141 $this->getWatchedItem( $title, $checkRights )->addWatch(); 03142 $this->invalidateCache(); 03143 } 03144 03152 public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) { 03153 $this->getWatchedItem( $title, $checkRights )->removeWatch(); 03154 $this->invalidateCache(); 03155 } 03156 03165 public function clearNotification( &$title, $oldid = 0 ) { 03166 global $wgUseEnotif, $wgShowUpdatedMarker; 03167 03168 // Do nothing if the database is locked to writes 03169 if ( wfReadOnly() ) { 03170 return; 03171 } 03172 03173 // Do nothing if not allowed to edit the watchlist 03174 if ( !$this->isAllowed( 'editmywatchlist' ) ) { 03175 return; 03176 } 03177 03178 // If we're working on user's talk page, we should update the talk page message indicator 03179 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) { 03180 if ( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this, $oldid ) ) ) { 03181 return; 03182 } 03183 03184 $nextid = $oldid ? $title->getNextRevisionID( $oldid ) : null; 03185 03186 if ( !$oldid || !$nextid ) { 03187 // If we're looking at the latest revision, we should definitely clear it 03188 $this->setNewtalk( false ); 03189 } else { 03190 // Otherwise we should update its revision, if it's present 03191 if ( $this->getNewtalk() ) { 03192 // Naturally the other one won't clear by itself 03193 $this->setNewtalk( false ); 03194 $this->setNewtalk( true, Revision::newFromId( $nextid ) ); 03195 } 03196 } 03197 } 03198 03199 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) { 03200 return; 03201 } 03202 03203 if ( $this->isAnon() ) { 03204 // Nothing else to do... 03205 return; 03206 } 03207 03208 // Only update the timestamp if the page is being watched. 03209 // The query to find out if it is watched is cached both in memcached and per-invocation, 03210 // and when it does have to be executed, it can be on a slave 03211 // If this is the user's newtalk page, we always update the timestamp 03212 $force = ''; 03213 if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) { 03214 $force = 'force'; 03215 } 03216 03217 $this->getWatchedItem( $title )->resetNotificationTimestamp( $force, $oldid ); 03218 } 03219 03226 public function clearAllNotifications() { 03227 if ( wfReadOnly() ) { 03228 return; 03229 } 03230 03231 // Do nothing if not allowed to edit the watchlist 03232 if ( !$this->isAllowed( 'editmywatchlist' ) ) { 03233 return; 03234 } 03235 03236 global $wgUseEnotif, $wgShowUpdatedMarker; 03237 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) { 03238 $this->setNewtalk( false ); 03239 return; 03240 } 03241 $id = $this->getId(); 03242 if ( $id != 0 ) { 03243 $dbw = wfGetDB( DB_MASTER ); 03244 $dbw->update( 'watchlist', 03245 array( /* SET */ 'wl_notificationtimestamp' => null ), 03246 array( /* WHERE */ 'wl_user' => $id ), 03247 __METHOD__ 03248 ); 03249 // We also need to clear here the "you have new message" notification for the own user_talk page; 03250 // it's cleared one page view later in WikiPage::doViewUpdates(). 03251 } 03252 } 03253 03267 protected function setCookie( $name, $value, $exp = 0, $secure = null, $params = array() ) { 03268 $params['secure'] = $secure; 03269 $this->getRequest()->response()->setcookie( $name, $value, $exp, $params ); 03270 } 03271 03281 protected function clearCookie( $name, $secure = null, $params = array() ) { 03282 $this->setCookie( $name, '', time() - 86400, $secure, $params ); 03283 } 03284 03293 public function setCookies( $request = null, $secure = null, $rememberMe = false ) { 03294 if ( $request === null ) { 03295 $request = $this->getRequest(); 03296 } 03297 03298 $this->load(); 03299 if ( 0 == $this->mId ) { 03300 return; 03301 } 03302 if ( !$this->mToken ) { 03303 // When token is empty or NULL generate a new one and then save it to the database 03304 // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey 03305 // Simply by setting every cell in the user_token column to NULL and letting them be 03306 // regenerated as users log back into the wiki. 03307 $this->setToken(); 03308 $this->saveSettings(); 03309 } 03310 $session = array( 03311 'wsUserID' => $this->mId, 03312 'wsToken' => $this->mToken, 03313 'wsUserName' => $this->getName() 03314 ); 03315 $cookies = array( 03316 'UserID' => $this->mId, 03317 'UserName' => $this->getName(), 03318 ); 03319 if ( $rememberMe ) { 03320 $cookies['Token'] = $this->mToken; 03321 } else { 03322 $cookies['Token'] = false; 03323 } 03324 03325 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) ); 03326 03327 foreach ( $session as $name => $value ) { 03328 $request->setSessionData( $name, $value ); 03329 } 03330 foreach ( $cookies as $name => $value ) { 03331 if ( $value === false ) { 03332 $this->clearCookie( $name ); 03333 } else { 03334 $this->setCookie( $name, $value, 0, $secure ); 03335 } 03336 } 03337 03345 if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) { 03346 $this->setCookie( 03347 'forceHTTPS', 03348 'true', 03349 $rememberMe ? 0 : null, 03350 false, 03351 array( 'prefix' => '' ) // no prefix 03352 ); 03353 } 03354 } 03355 03359 public function logout() { 03360 if ( wfRunHooks( 'UserLogout', array( &$this ) ) ) { 03361 $this->doLogout(); 03362 } 03363 } 03364 03369 public function doLogout() { 03370 $this->clearInstanceCache( 'defaults' ); 03371 03372 $this->getRequest()->setSessionData( 'wsUserID', 0 ); 03373 03374 $this->clearCookie( 'UserID' ); 03375 $this->clearCookie( 'Token' ); 03376 $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) ); 03377 03378 // Remember when user logged out, to prevent seeing cached pages 03379 $this->setCookie( 'LoggedOut', time(), time() + 86400 ); 03380 } 03381 03386 public function saveSettings() { 03387 global $wgAuth; 03388 03389 $this->load(); 03390 if ( wfReadOnly() ) { 03391 return; 03392 } 03393 if ( 0 == $this->mId ) { 03394 return; 03395 } 03396 03397 $this->mTouched = self::newTouchedTimestamp(); 03398 if ( !$wgAuth->allowSetLocalPassword() ) { 03399 $this->mPassword = ''; 03400 } 03401 03402 $dbw = wfGetDB( DB_MASTER ); 03403 $dbw->update( 'user', 03404 array( /* SET */ 03405 'user_name' => $this->mName, 03406 'user_password' => $this->mPassword, 03407 'user_newpassword' => $this->mNewpassword, 03408 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 03409 'user_real_name' => $this->mRealName, 03410 'user_email' => $this->mEmail, 03411 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 03412 'user_touched' => $dbw->timestamp( $this->mTouched ), 03413 'user_token' => strval( $this->mToken ), 03414 'user_email_token' => $this->mEmailToken, 03415 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), 03416 'user_password_expires' => $dbw->timestampOrNull( $this->mPasswordExpires ), 03417 ), array( /* WHERE */ 03418 'user_id' => $this->mId 03419 ), __METHOD__ 03420 ); 03421 03422 $this->saveOptions(); 03423 03424 wfRunHooks( 'UserSaveSettings', array( $this ) ); 03425 $this->clearSharedCache(); 03426 $this->getUserPage()->invalidateCache(); 03427 } 03428 03433 public function idForName() { 03434 $s = trim( $this->getName() ); 03435 if ( $s === '' ) { 03436 return 0; 03437 } 03438 03439 $dbr = wfGetDB( DB_SLAVE ); 03440 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ ); 03441 if ( $id === false ) { 03442 $id = 0; 03443 } 03444 return $id; 03445 } 03446 03463 public static function createNew( $name, $params = array() ) { 03464 $user = new User; 03465 $user->load(); 03466 $user->setToken(); // init token 03467 if ( isset( $params['options'] ) ) { 03468 $user->mOptions = $params['options'] + (array)$user->mOptions; 03469 unset( $params['options'] ); 03470 } 03471 $dbw = wfGetDB( DB_MASTER ); 03472 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); 03473 03474 $fields = array( 03475 'user_id' => $seqVal, 03476 'user_name' => $name, 03477 'user_password' => $user->mPassword, 03478 'user_newpassword' => $user->mNewpassword, 03479 'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ), 03480 'user_email' => $user->mEmail, 03481 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), 03482 'user_real_name' => $user->mRealName, 03483 'user_token' => strval( $user->mToken ), 03484 'user_registration' => $dbw->timestamp( $user->mRegistration ), 03485 'user_editcount' => 0, 03486 'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ), 03487 ); 03488 foreach ( $params as $name => $value ) { 03489 $fields["user_$name"] = $value; 03490 } 03491 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) ); 03492 if ( $dbw->affectedRows() ) { 03493 $newUser = User::newFromId( $dbw->insertId() ); 03494 } else { 03495 $newUser = null; 03496 } 03497 return $newUser; 03498 } 03499 03526 public function addToDatabase() { 03527 $this->load(); 03528 if ( !$this->mToken ) { 03529 $this->setToken(); // init token 03530 } 03531 03532 $this->mTouched = self::newTouchedTimestamp(); 03533 03534 $dbw = wfGetDB( DB_MASTER ); 03535 $inWrite = $dbw->writesOrCallbacksPending(); 03536 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); 03537 $dbw->insert( 'user', 03538 array( 03539 'user_id' => $seqVal, 03540 'user_name' => $this->mName, 03541 'user_password' => $this->mPassword, 03542 'user_newpassword' => $this->mNewpassword, 03543 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 03544 'user_email' => $this->mEmail, 03545 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 03546 'user_real_name' => $this->mRealName, 03547 'user_token' => strval( $this->mToken ), 03548 'user_registration' => $dbw->timestamp( $this->mRegistration ), 03549 'user_editcount' => 0, 03550 'user_touched' => $dbw->timestamp( $this->mTouched ), 03551 ), __METHOD__, 03552 array( 'IGNORE' ) 03553 ); 03554 if ( !$dbw->affectedRows() ) { 03555 if ( !$inWrite ) { 03556 // XXX: Get out of REPEATABLE-READ so the SELECT below works. 03557 // Often this case happens early in views before any writes. 03558 // This shows up at least with CentralAuth. 03559 $dbw->commit( __METHOD__, 'flush' ); 03560 } 03561 $this->mId = $dbw->selectField( 'user', 'user_id', 03562 array( 'user_name' => $this->mName ), __METHOD__ ); 03563 $loaded = false; 03564 if ( $this->mId ) { 03565 if ( $this->loadFromDatabase() ) { 03566 $loaded = true; 03567 } 03568 } 03569 if ( !$loaded ) { 03570 throw new MWException( __METHOD__ . ": hit a key conflict attempting " . 03571 "to insert user '{$this->mName}' row, but it was not present in select!" ); 03572 } 03573 return Status::newFatal( 'userexists' ); 03574 } 03575 $this->mId = $dbw->insertId(); 03576 03577 // Clear instance cache other than user table data, which is already accurate 03578 $this->clearInstanceCache(); 03579 03580 $this->saveOptions(); 03581 return Status::newGood(); 03582 } 03583 03589 public function spreadAnyEditBlock() { 03590 if ( $this->isLoggedIn() && $this->isBlocked() ) { 03591 return $this->spreadBlock(); 03592 } 03593 return false; 03594 } 03595 03601 protected function spreadBlock() { 03602 wfDebug( __METHOD__ . "()\n" ); 03603 $this->load(); 03604 if ( $this->mId == 0 ) { 03605 return false; 03606 } 03607 03608 $userblock = Block::newFromTarget( $this->getName() ); 03609 if ( !$userblock ) { 03610 return false; 03611 } 03612 03613 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() ); 03614 } 03615 03620 public function isBlockedFromCreateAccount() { 03621 $this->getBlockedStatus(); 03622 if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) { 03623 return $this->mBlock; 03624 } 03625 03626 # bug 13611: if the IP address the user is trying to create an account from is 03627 # blocked with createaccount disabled, prevent new account creation there even 03628 # when the user is logged in 03629 if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) { 03630 $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() ); 03631 } 03632 return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' ) 03633 ? $this->mBlockedFromCreateAccount 03634 : false; 03635 } 03636 03641 public function isBlockedFromEmailuser() { 03642 $this->getBlockedStatus(); 03643 return $this->mBlock && $this->mBlock->prevents( 'sendemail' ); 03644 } 03645 03650 public function isAllowedToCreateAccount() { 03651 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); 03652 } 03653 03659 public function getUserPage() { 03660 return Title::makeTitle( NS_USER, $this->getName() ); 03661 } 03662 03668 public function getTalkPage() { 03669 $title = $this->getUserPage(); 03670 return $title->getTalkPage(); 03671 } 03672 03678 public function isNewbie() { 03679 return !$this->isAllowed( 'autoconfirmed' ); 03680 } 03681 03687 public function checkPassword( $password ) { 03688 global $wgAuth, $wgLegacyEncoding; 03689 $this->load(); 03690 03691 // Certain authentication plugins do NOT want to save 03692 // domain passwords in a mysql database, so we should 03693 // check this (in case $wgAuth->strict() is false). 03694 03695 if ( $wgAuth->authenticate( $this->getName(), $password ) ) { 03696 return true; 03697 } elseif ( $wgAuth->strict() ) { 03698 // Auth plugin doesn't allow local authentication 03699 return false; 03700 } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) { 03701 // Auth plugin doesn't allow local authentication for this user name 03702 return false; 03703 } 03704 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) { 03705 return true; 03706 } elseif ( $wgLegacyEncoding ) { 03707 // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted 03708 // Check for this with iconv 03709 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ); 03710 if ( $cp1252Password != $password 03711 && self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) 03712 ) { 03713 return true; 03714 } 03715 } 03716 return false; 03717 } 03718 03727 public function checkTemporaryPassword( $plaintext ) { 03728 global $wgNewPasswordExpiry; 03729 03730 $this->load(); 03731 if ( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) { 03732 if ( is_null( $this->mNewpassTime ) ) { 03733 return true; 03734 } 03735 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry; 03736 return ( time() < $expiry ); 03737 } else { 03738 return false; 03739 } 03740 } 03741 03750 public function editToken( $salt = '', $request = null ) { 03751 wfDeprecated( __METHOD__, '1.19' ); 03752 return $this->getEditToken( $salt, $request ); 03753 } 03754 03767 public function getEditToken( $salt = '', $request = null ) { 03768 if ( $request == null ) { 03769 $request = $this->getRequest(); 03770 } 03771 03772 if ( $this->isAnon() ) { 03773 return EDIT_TOKEN_SUFFIX; 03774 } else { 03775 $token = $request->getSessionData( 'wsEditToken' ); 03776 if ( $token === null ) { 03777 $token = MWCryptRand::generateHex( 32 ); 03778 $request->setSessionData( 'wsEditToken', $token ); 03779 } 03780 if ( is_array( $salt ) ) { 03781 $salt = implode( '|', $salt ); 03782 } 03783 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX; 03784 } 03785 } 03786 03793 public static function generateToken() { 03794 return MWCryptRand::generateHex( 32 ); 03795 } 03796 03808 public function matchEditToken( $val, $salt = '', $request = null ) { 03809 $sessionToken = $this->getEditToken( $salt, $request ); 03810 if ( $val != $sessionToken ) { 03811 wfDebug( "User::matchEditToken: broken session data\n" ); 03812 } 03813 return $val == $sessionToken; 03814 } 03815 03825 public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) { 03826 $sessionToken = $this->getEditToken( $salt, $request ); 03827 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 ); 03828 } 03829 03837 public function sendConfirmationMail( $type = 'created' ) { 03838 global $wgLang; 03839 $expiration = null; // gets passed-by-ref and defined in next line. 03840 $token = $this->confirmationToken( $expiration ); 03841 $url = $this->confirmationTokenUrl( $token ); 03842 $invalidateURL = $this->invalidationTokenUrl( $token ); 03843 $this->saveSettings(); 03844 03845 if ( $type == 'created' || $type === false ) { 03846 $message = 'confirmemail_body'; 03847 } elseif ( $type === true ) { 03848 $message = 'confirmemail_body_changed'; 03849 } else { 03850 // Messages: confirmemail_body_changed, confirmemail_body_set 03851 $message = 'confirmemail_body_' . $type; 03852 } 03853 03854 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(), 03855 wfMessage( $message, 03856 $this->getRequest()->getIP(), 03857 $this->getName(), 03858 $url, 03859 $wgLang->timeanddate( $expiration, false ), 03860 $invalidateURL, 03861 $wgLang->date( $expiration, false ), 03862 $wgLang->time( $expiration, false ) )->text() ); 03863 } 03864 03875 public function sendMail( $subject, $body, $from = null, $replyto = null ) { 03876 if ( is_null( $from ) ) { 03877 global $wgPasswordSender; 03878 $sender = new MailAddress( $wgPasswordSender, 03879 wfMessage( 'emailsender' )->inContentLanguage()->text() ); 03880 } else { 03881 $sender = new MailAddress( $from ); 03882 } 03883 03884 $to = new MailAddress( $this ); 03885 return UserMailer::send( $to, $sender, $subject, $body, $replyto ); 03886 } 03887 03898 protected function confirmationToken( &$expiration ) { 03899 global $wgUserEmailConfirmationTokenExpiry; 03900 $now = time(); 03901 $expires = $now + $wgUserEmailConfirmationTokenExpiry; 03902 $expiration = wfTimestamp( TS_MW, $expires ); 03903 $this->load(); 03904 $token = MWCryptRand::generateHex( 32 ); 03905 $hash = md5( $token ); 03906 $this->mEmailToken = $hash; 03907 $this->mEmailTokenExpires = $expiration; 03908 return $token; 03909 } 03910 03916 protected function confirmationTokenUrl( $token ) { 03917 return $this->getTokenUrl( 'ConfirmEmail', $token ); 03918 } 03919 03925 protected function invalidationTokenUrl( $token ) { 03926 return $this->getTokenUrl( 'InvalidateEmail', $token ); 03927 } 03928 03943 protected function getTokenUrl( $page, $token ) { 03944 // Hack to bypass localization of 'Special:' 03945 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" ); 03946 return $title->getCanonicalURL(); 03947 } 03948 03956 public function confirmEmail() { 03957 // Check if it's already confirmed, so we don't touch the database 03958 // and fire the ConfirmEmailComplete hook on redundant confirmations. 03959 if ( !$this->isEmailConfirmed() ) { 03960 $this->setEmailAuthenticationTimestamp( wfTimestampNow() ); 03961 wfRunHooks( 'ConfirmEmailComplete', array( $this ) ); 03962 } 03963 return true; 03964 } 03965 03973 public function invalidateEmail() { 03974 $this->load(); 03975 $this->mEmailToken = null; 03976 $this->mEmailTokenExpires = null; 03977 $this->setEmailAuthenticationTimestamp( null ); 03978 wfRunHooks( 'InvalidateEmailComplete', array( $this ) ); 03979 return true; 03980 } 03981 03986 public function setEmailAuthenticationTimestamp( $timestamp ) { 03987 $this->load(); 03988 $this->mEmailAuthenticated = $timestamp; 03989 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) ); 03990 } 03991 03997 public function canSendEmail() { 03998 global $wgEnableEmail, $wgEnableUserEmail; 03999 if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) { 04000 return false; 04001 } 04002 $canSend = $this->isEmailConfirmed(); 04003 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) ); 04004 return $canSend; 04005 } 04006 04012 public function canReceiveEmail() { 04013 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' ); 04014 } 04015 04026 public function isEmailConfirmed() { 04027 global $wgEmailAuthentication; 04028 $this->load(); 04029 $confirmed = true; 04030 if ( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) { 04031 if ( $this->isAnon() ) { 04032 return false; 04033 } 04034 if ( !Sanitizer::validateEmail( $this->mEmail ) ) { 04035 return false; 04036 } 04037 if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) { 04038 return false; 04039 } 04040 return true; 04041 } else { 04042 return $confirmed; 04043 } 04044 } 04045 04050 public function isEmailConfirmationPending() { 04051 global $wgEmailAuthentication; 04052 return $wgEmailAuthentication && 04053 !$this->isEmailConfirmed() && 04054 $this->mEmailToken && 04055 $this->mEmailTokenExpires > wfTimestamp(); 04056 } 04057 04065 public function getRegistration() { 04066 if ( $this->isAnon() ) { 04067 return false; 04068 } 04069 $this->load(); 04070 return $this->mRegistration; 04071 } 04072 04079 public function getFirstEditTimestamp() { 04080 if ( $this->getId() == 0 ) { 04081 return false; // anons 04082 } 04083 $dbr = wfGetDB( DB_SLAVE ); 04084 $time = $dbr->selectField( 'revision', 'rev_timestamp', 04085 array( 'rev_user' => $this->getId() ), 04086 __METHOD__, 04087 array( 'ORDER BY' => 'rev_timestamp ASC' ) 04088 ); 04089 if ( !$time ) { 04090 return false; // no edits 04091 } 04092 return wfTimestamp( TS_MW, $time ); 04093 } 04094 04101 public static function getGroupPermissions( $groups ) { 04102 global $wgGroupPermissions, $wgRevokePermissions; 04103 $rights = array(); 04104 // grant every granted permission first 04105 foreach ( $groups as $group ) { 04106 if ( isset( $wgGroupPermissions[$group] ) ) { 04107 $rights = array_merge( $rights, 04108 // array_filter removes empty items 04109 array_keys( array_filter( $wgGroupPermissions[$group] ) ) ); 04110 } 04111 } 04112 // now revoke the revoked permissions 04113 foreach ( $groups as $group ) { 04114 if ( isset( $wgRevokePermissions[$group] ) ) { 04115 $rights = array_diff( $rights, 04116 array_keys( array_filter( $wgRevokePermissions[$group] ) ) ); 04117 } 04118 } 04119 return array_unique( $rights ); 04120 } 04121 04128 public static function getGroupsWithPermission( $role ) { 04129 global $wgGroupPermissions; 04130 $allowedGroups = array(); 04131 foreach ( array_keys( $wgGroupPermissions ) as $group ) { 04132 if ( self::groupHasPermission( $group, $role ) ) { 04133 $allowedGroups[] = $group; 04134 } 04135 } 04136 return $allowedGroups; 04137 } 04138 04151 public static function groupHasPermission( $group, $role ) { 04152 global $wgGroupPermissions, $wgRevokePermissions; 04153 return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role] 04154 && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] ); 04155 } 04156 04164 public static function isEveryoneAllowed( $right ) { 04165 global $wgGroupPermissions, $wgRevokePermissions; 04166 static $cache = array(); 04167 04168 // Use the cached results, except in unit tests which rely on 04169 // being able change the permission mid-request 04170 if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) { 04171 return $cache[$right]; 04172 } 04173 04174 if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) { 04175 $cache[$right] = false; 04176 return false; 04177 } 04178 04179 // If it's revoked anywhere, then everyone doesn't have it 04180 foreach ( $wgRevokePermissions as $rights ) { 04181 if ( isset( $rights[$right] ) && $rights[$right] ) { 04182 $cache[$right] = false; 04183 return false; 04184 } 04185 } 04186 04187 // Allow extensions (e.g. OAuth) to say false 04188 if ( !wfRunHooks( 'UserIsEveryoneAllowed', array( $right ) ) ) { 04189 $cache[$right] = false; 04190 return false; 04191 } 04192 04193 $cache[$right] = true; 04194 return true; 04195 } 04196 04203 public static function getGroupName( $group ) { 04204 $msg = wfMessage( "group-$group" ); 04205 return $msg->isBlank() ? $group : $msg->text(); 04206 } 04207 04215 public static function getGroupMember( $group, $username = '#' ) { 04216 $msg = wfMessage( "group-$group-member", $username ); 04217 return $msg->isBlank() ? $group : $msg->text(); 04218 } 04219 04226 public static function getAllGroups() { 04227 global $wgGroupPermissions, $wgRevokePermissions; 04228 return array_diff( 04229 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ), 04230 self::getImplicitGroups() 04231 ); 04232 } 04233 04238 public static function getAllRights() { 04239 if ( self::$mAllRights === false ) { 04240 global $wgAvailableRights; 04241 if ( count( $wgAvailableRights ) ) { 04242 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) ); 04243 } else { 04244 self::$mAllRights = self::$mCoreRights; 04245 } 04246 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) ); 04247 } 04248 return self::$mAllRights; 04249 } 04250 04255 public static function getImplicitGroups() { 04256 global $wgImplicitGroups; 04257 $groups = $wgImplicitGroups; 04258 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead 04259 return $groups; 04260 } 04261 04268 public static function getGroupPage( $group ) { 04269 $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage(); 04270 if ( $msg->exists() ) { 04271 $title = Title::newFromText( $msg->text() ); 04272 if ( is_object( $title ) ) { 04273 return $title; 04274 } 04275 } 04276 return false; 04277 } 04278 04287 public static function makeGroupLinkHTML( $group, $text = '' ) { 04288 if ( $text == '' ) { 04289 $text = self::getGroupName( $group ); 04290 } 04291 $title = self::getGroupPage( $group ); 04292 if ( $title ) { 04293 return Linker::link( $title, htmlspecialchars( $text ) ); 04294 } else { 04295 return $text; 04296 } 04297 } 04298 04307 public static function makeGroupLinkWiki( $group, $text = '' ) { 04308 if ( $text == '' ) { 04309 $text = self::getGroupName( $group ); 04310 } 04311 $title = self::getGroupPage( $group ); 04312 if ( $title ) { 04313 $page = $title->getPrefixedText(); 04314 return "[[$page|$text]]"; 04315 } else { 04316 return $text; 04317 } 04318 } 04319 04329 public static function changeableByGroup( $group ) { 04330 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; 04331 04332 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); 04333 if ( empty( $wgAddGroups[$group] ) ) { 04334 // Don't add anything to $groups 04335 } elseif ( $wgAddGroups[$group] === true ) { 04336 // You get everything 04337 $groups['add'] = self::getAllGroups(); 04338 } elseif ( is_array( $wgAddGroups[$group] ) ) { 04339 $groups['add'] = $wgAddGroups[$group]; 04340 } 04341 04342 // Same thing for remove 04343 if ( empty( $wgRemoveGroups[$group] ) ) { 04344 } elseif ( $wgRemoveGroups[$group] === true ) { 04345 $groups['remove'] = self::getAllGroups(); 04346 } elseif ( is_array( $wgRemoveGroups[$group] ) ) { 04347 $groups['remove'] = $wgRemoveGroups[$group]; 04348 } 04349 04350 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility 04351 if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) { 04352 foreach ( $wgGroupsAddToSelf as $key => $value ) { 04353 if ( is_int( $key ) ) { 04354 $wgGroupsAddToSelf['user'][] = $value; 04355 } 04356 } 04357 } 04358 04359 if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) { 04360 foreach ( $wgGroupsRemoveFromSelf as $key => $value ) { 04361 if ( is_int( $key ) ) { 04362 $wgGroupsRemoveFromSelf['user'][] = $value; 04363 } 04364 } 04365 } 04366 04367 // Now figure out what groups the user can add to him/herself 04368 if ( empty( $wgGroupsAddToSelf[$group] ) ) { 04369 } elseif ( $wgGroupsAddToSelf[$group] === true ) { 04370 // No idea WHY this would be used, but it's there 04371 $groups['add-self'] = User::getAllGroups(); 04372 } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) { 04373 $groups['add-self'] = $wgGroupsAddToSelf[$group]; 04374 } 04375 04376 if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) { 04377 } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) { 04378 $groups['remove-self'] = User::getAllGroups(); 04379 } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) { 04380 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; 04381 } 04382 04383 return $groups; 04384 } 04385 04393 public function changeableGroups() { 04394 if ( $this->isAllowed( 'userrights' ) ) { 04395 // This group gives the right to modify everything (reverse- 04396 // compatibility with old "userrights lets you change 04397 // everything") 04398 // Using array_merge to make the groups reindexed 04399 $all = array_merge( User::getAllGroups() ); 04400 return array( 04401 'add' => $all, 04402 'remove' => $all, 04403 'add-self' => array(), 04404 'remove-self' => array() 04405 ); 04406 } 04407 04408 // Okay, it's not so simple, we will have to go through the arrays 04409 $groups = array( 04410 'add' => array(), 04411 'remove' => array(), 04412 'add-self' => array(), 04413 'remove-self' => array() 04414 ); 04415 $addergroups = $this->getEffectiveGroups(); 04416 04417 foreach ( $addergroups as $addergroup ) { 04418 $groups = array_merge_recursive( 04419 $groups, $this->changeableByGroup( $addergroup ) 04420 ); 04421 $groups['add'] = array_unique( $groups['add'] ); 04422 $groups['remove'] = array_unique( $groups['remove'] ); 04423 $groups['add-self'] = array_unique( $groups['add-self'] ); 04424 $groups['remove-self'] = array_unique( $groups['remove-self'] ); 04425 } 04426 return $groups; 04427 } 04428 04433 public function incEditCount() { 04434 if ( !$this->isAnon() ) { 04435 $dbw = wfGetDB( DB_MASTER ); 04436 $dbw->update( 04437 'user', 04438 array( 'user_editcount=user_editcount+1' ), 04439 array( 'user_id' => $this->getId() ), 04440 __METHOD__ 04441 ); 04442 04443 // Lazy initialization check... 04444 if ( $dbw->affectedRows() == 0 ) { 04445 // Now here's a goddamn hack... 04446 $dbr = wfGetDB( DB_SLAVE ); 04447 if ( $dbr !== $dbw ) { 04448 // If we actually have a slave server, the count is 04449 // at least one behind because the current transaction 04450 // has not been committed and replicated. 04451 $this->initEditCount( 1 ); 04452 } else { 04453 // But if DB_SLAVE is selecting the master, then the 04454 // count we just read includes the revision that was 04455 // just added in the working transaction. 04456 $this->initEditCount(); 04457 } 04458 } 04459 } 04460 // edit count in user cache too 04461 $this->invalidateCache(); 04462 } 04463 04470 protected function initEditCount( $add = 0 ) { 04471 // Pull from a slave to be less cruel to servers 04472 // Accuracy isn't the point anyway here 04473 $dbr = wfGetDB( DB_SLAVE ); 04474 $count = (int)$dbr->selectField( 04475 'revision', 04476 'COUNT(rev_user)', 04477 array( 'rev_user' => $this->getId() ), 04478 __METHOD__ 04479 ); 04480 $count = $count + $add; 04481 04482 $dbw = wfGetDB( DB_MASTER ); 04483 $dbw->update( 04484 'user', 04485 array( 'user_editcount' => $count ), 04486 array( 'user_id' => $this->getId() ), 04487 __METHOD__ 04488 ); 04489 04490 return $count; 04491 } 04492 04499 public static function getRightDescription( $right ) { 04500 $key = "right-$right"; 04501 $msg = wfMessage( $key ); 04502 return $msg->isBlank() ? $right : $msg->text(); 04503 } 04504 04512 public static function oldCrypt( $password, $userId ) { 04513 global $wgPasswordSalt; 04514 if ( $wgPasswordSalt ) { 04515 return md5( $userId . '-' . md5( $password ) ); 04516 } else { 04517 return md5( $password ); 04518 } 04519 } 04520 04529 public static function crypt( $password, $salt = false ) { 04530 global $wgPasswordSalt; 04531 04532 $hash = ''; 04533 if ( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) { 04534 return $hash; 04535 } 04536 04537 if ( $wgPasswordSalt ) { 04538 if ( $salt === false ) { 04539 $salt = MWCryptRand::generateHex( 8 ); 04540 } 04541 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); 04542 } else { 04543 return ':A:' . md5( $password ); 04544 } 04545 } 04546 04557 public static function comparePasswords( $hash, $password, $userId = false ) { 04558 $type = substr( $hash, 0, 3 ); 04559 04560 $result = false; 04561 if ( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) { 04562 return $result; 04563 } 04564 04565 if ( $type == ':A:' ) { 04566 // Unsalted 04567 return md5( $password ) === substr( $hash, 3 ); 04568 } elseif ( $type == ':B:' ) { 04569 // Salted 04570 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 ); 04571 return md5( $salt . '-' . md5( $password ) ) === $realHash; 04572 } else { 04573 // Old-style 04574 return self::oldCrypt( $password, $userId ) === $hash; 04575 } 04576 } 04577 04599 public function addNewUserLogEntry( $action = false, $reason = '' ) { 04600 global $wgUser, $wgNewUserLog; 04601 if ( empty( $wgNewUserLog ) ) { 04602 return true; // disabled 04603 } 04604 04605 if ( $action === true ) { 04606 $action = 'byemail'; 04607 } elseif ( $action === false ) { 04608 if ( $this->getName() == $wgUser->getName() ) { 04609 $action = 'create'; 04610 } else { 04611 $action = 'create2'; 04612 } 04613 } 04614 04615 if ( $action === 'create' || $action === 'autocreate' ) { 04616 $performer = $this; 04617 } else { 04618 $performer = $wgUser; 04619 } 04620 04621 $logEntry = new ManualLogEntry( 'newusers', $action ); 04622 $logEntry->setPerformer( $performer ); 04623 $logEntry->setTarget( $this->getUserPage() ); 04624 $logEntry->setComment( $reason ); 04625 $logEntry->setParameters( array( 04626 '4::userid' => $this->getId(), 04627 ) ); 04628 $logid = $logEntry->insert(); 04629 04630 if ( $action !== 'autocreate' ) { 04631 $logEntry->publish( $logid ); 04632 } 04633 04634 return (int)$logid; 04635 } 04636 04644 public function addNewUserLogEntryAutoCreate() { 04645 $this->addNewUserLogEntry( 'autocreate' ); 04646 04647 return true; 04648 } 04649 04655 protected function loadOptions( $data = null ) { 04656 global $wgContLang; 04657 04658 $this->load(); 04659 04660 if ( $this->mOptionsLoaded ) { 04661 return; 04662 } 04663 04664 $this->mOptions = self::getDefaultOptions(); 04665 04666 if ( !$this->getId() ) { 04667 // For unlogged-in users, load language/variant options from request. 04668 // There's no need to do it for logged-in users: they can set preferences, 04669 // and handling of page content is done by $pageLang->getPreferredVariant() and such, 04670 // so don't override user's choice (especially when the user chooses site default). 04671 $variant = $wgContLang->getDefaultVariant(); 04672 $this->mOptions['variant'] = $variant; 04673 $this->mOptions['language'] = $variant; 04674 $this->mOptionsLoaded = true; 04675 return; 04676 } 04677 04678 // Maybe load from the object 04679 if ( !is_null( $this->mOptionOverrides ) ) { 04680 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" ); 04681 foreach ( $this->mOptionOverrides as $key => $value ) { 04682 $this->mOptions[$key] = $value; 04683 } 04684 } else { 04685 if ( !is_array( $data ) ) { 04686 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" ); 04687 // Load from database 04688 $dbr = wfGetDB( DB_SLAVE ); 04689 04690 $res = $dbr->select( 04691 'user_properties', 04692 array( 'up_property', 'up_value' ), 04693 array( 'up_user' => $this->getId() ), 04694 __METHOD__ 04695 ); 04696 04697 $this->mOptionOverrides = array(); 04698 $data = array(); 04699 foreach ( $res as $row ) { 04700 $data[$row->up_property] = $row->up_value; 04701 } 04702 } 04703 foreach ( $data as $property => $value ) { 04704 $this->mOptionOverrides[$property] = $value; 04705 $this->mOptions[$property] = $value; 04706 } 04707 } 04708 04709 $this->mOptionsLoaded = true; 04710 04711 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) ); 04712 } 04713 04717 protected function saveOptions() { 04718 $this->loadOptions(); 04719 04720 // Not using getOptions(), to keep hidden preferences in database 04721 $saveOptions = $this->mOptions; 04722 04723 // Allow hooks to abort, for instance to save to a global profile. 04724 // Reset options to default state before saving. 04725 if ( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) { 04726 return; 04727 } 04728 04729 $userId = $this->getId(); 04730 $insert_rows = array(); 04731 foreach ( $saveOptions as $key => $value ) { 04732 // Don't bother storing default values 04733 $defaultOption = self::getDefaultOption( $key ); 04734 if ( ( is_null( $defaultOption ) && 04735 !( $value === false || is_null( $value ) ) ) || 04736 $value != $defaultOption 04737 ) { 04738 $insert_rows[] = array( 04739 'up_user' => $userId, 04740 'up_property' => $key, 04741 'up_value' => $value, 04742 ); 04743 } 04744 } 04745 04746 $dbw = wfGetDB( DB_MASTER ); 04747 // Find and delete any prior preference rows... 04748 $res = $dbw->select( 'user_properties', 04749 array( 'up_property' ), array( 'up_user' => $userId ), __METHOD__ ); 04750 $priorKeys = array(); 04751 foreach ( $res as $row ) { 04752 $priorKeys[] = $row->up_property; 04753 } 04754 if ( count( $priorKeys ) ) { 04755 // Do the DELETE by PRIMARY KEY for prior rows. 04756 // In the past a very large portion of calls to this function are for setting 04757 // 'rememberpassword' for new accounts (a preference that has since been removed). 04758 // Doing a blanket per-user DELETE for new accounts with no rows in the table 04759 // caused gap locks on [max user ID,+infinity) which caused high contention since 04760 // updates would pile up on each other as they are for higher (newer) user IDs. 04761 // It might not be necessary these days, but it shouldn't hurt either. 04762 $dbw->delete( 'user_properties', 04763 array( 'up_user' => $userId, 'up_property' => $priorKeys ), __METHOD__ ); 04764 } 04765 // Insert the new preference rows 04766 $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) ); 04767 } 04768 04792 public static function passwordChangeInputAttribs() { 04793 global $wgMinimalPasswordLength; 04794 04795 if ( $wgMinimalPasswordLength == 0 ) { 04796 return array(); 04797 } 04798 04799 # Note that the pattern requirement will always be satisfied if the 04800 # input is empty, so we need required in all cases. 04801 # 04802 # @todo FIXME: Bug 23769: This needs to not claim the password is required 04803 # if e-mail confirmation is being used. Since HTML5 input validation 04804 # is b0rked anyway in some browsers, just return nothing. When it's 04805 # re-enabled, fix this code to not output required for e-mail 04806 # registration. 04807 #$ret = array( 'required' ); 04808 $ret = array(); 04809 04810 # We can't actually do this right now, because Opera 9.6 will print out 04811 # the entered password visibly in its error message! When other 04812 # browsers add support for this attribute, or Opera fixes its support, 04813 # we can add support with a version check to avoid doing this on Opera 04814 # versions where it will be a problem. Reported to Opera as 04815 # DSK-262266, but they don't have a public bug tracker for us to follow. 04816 /* 04817 if ( $wgMinimalPasswordLength > 1 ) { 04818 $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}'; 04819 $ret['title'] = wfMessage( 'passwordtooshort' ) 04820 ->numParams( $wgMinimalPasswordLength )->text(); 04821 } 04822 */ 04823 04824 return $ret; 04825 } 04826 04832 public static function selectFields() { 04833 return array( 04834 'user_id', 04835 'user_name', 04836 'user_real_name', 04837 'user_password', 04838 'user_newpassword', 04839 'user_newpass_time', 04840 'user_email', 04841 'user_touched', 04842 'user_token', 04843 'user_email_authenticated', 04844 'user_email_token', 04845 'user_email_token_expires', 04846 'user_password_expires', 04847 'user_registration', 04848 'user_editcount', 04849 ); 04850 } 04851 04859 static function newFatalPermissionDeniedStatus( $permission ) { 04860 global $wgLang; 04861 04862 $groups = array_map( 04863 array( 'User', 'makeGroupLinkWiki' ), 04864 User::getGroupsWithPermission( $permission ) 04865 ); 04866 04867 if ( $groups ) { 04868 return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ); 04869 } else { 04870 return Status::newFatal( 'badaccess-group0' ); 04871 } 04872 } 04873 }