MediaWiki
REL1_22
|
00001 <?php 00027 define( 'USER_TOKEN_LENGTH', 32 ); 00028 00033 define( 'MW_USER_VERSION', 8 ); 00034 00039 define( 'EDIT_TOKEN_SUFFIX', '+\\' ); 00040 00045 class PasswordError extends MWException { 00046 // NOP 00047 } 00048 00059 class User { 00064 const USER_TOKEN_LENGTH = USER_TOKEN_LENGTH; 00065 const MW_USER_VERSION = MW_USER_VERSION; 00066 const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX; 00067 00071 const MAX_WATCHED_ITEMS_CACHE = 100; 00072 00079 static $mCacheVars = array( 00080 // user table 00081 'mId', 00082 'mName', 00083 'mRealName', 00084 'mPassword', 00085 'mNewpassword', 00086 'mNewpassTime', 00087 'mEmail', 00088 'mTouched', 00089 'mToken', 00090 'mEmailAuthenticated', 00091 'mEmailToken', 00092 'mEmailTokenExpires', 00093 'mRegistration', 00094 'mEditCount', 00095 // user_groups table 00096 'mGroups', 00097 // user_properties table 00098 'mOptionOverrides', 00099 ); 00100 00107 static $mCoreRights = array( 00108 'apihighlimits', 00109 'autoconfirmed', 00110 'autopatrol', 00111 'bigdelete', 00112 'block', 00113 'blockemail', 00114 'bot', 00115 'browsearchive', 00116 'createaccount', 00117 'createpage', 00118 'createtalk', 00119 'delete', 00120 'deletedhistory', 00121 'deletedtext', 00122 'deletelogentry', 00123 'deleterevision', 00124 'edit', 00125 'editinterface', 00126 'editprotected', 00127 'editmyoptions', 00128 'editmyprivateinfo', 00129 'editmyusercss', 00130 'editmyuserjs', 00131 'editmywatchlist', 00132 'editsemiprotected', 00133 'editusercssjs', #deprecated 00134 'editusercss', 00135 'edituserjs', 00136 'hideuser', 00137 'import', 00138 'importupload', 00139 'ipblock-exempt', 00140 'markbotedits', 00141 'mergehistory', 00142 'minoredit', 00143 'move', 00144 'movefile', 00145 'move-rootuserpages', 00146 'move-subpages', 00147 'nominornewtalk', 00148 'noratelimit', 00149 'override-export-depth', 00150 'passwordreset', 00151 'patrol', 00152 'patrolmarks', 00153 'protect', 00154 'proxyunbannable', 00155 'purge', 00156 'read', 00157 'reupload', 00158 'reupload-own', 00159 'reupload-shared', 00160 'rollback', 00161 'sendemail', 00162 'siteadmin', 00163 'suppressionlog', 00164 'suppressredirect', 00165 'suppressrevision', 00166 'unblockself', 00167 'undelete', 00168 'unwatchedpages', 00169 'upload', 00170 'upload_by_url', 00171 'userrights', 00172 'userrights-interwiki', 00173 'viewmyprivateinfo', 00174 'viewmywatchlist', 00175 'writeapi', 00176 ); 00180 static $mAllRights = false; 00181 00184 var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, 00185 $mEmail, $mTouched, $mToken, $mEmailAuthenticated, 00186 $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount, 00187 $mGroups, $mOptionOverrides; 00189 00194 var $mOptionsLoaded; 00195 00199 private $mLoadedItems = array(); 00201 00211 var $mFrom; 00212 00216 var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mRights, 00217 $mBlockreason, $mEffectiveGroups, $mImplicitGroups, $mFormerGroups, $mBlockedGlobally, 00218 $mLocked, $mHideName, $mOptions; 00219 00223 private $mRequest; 00224 00228 var $mBlock; 00229 00233 var $mAllowUsertalk; 00234 00238 private $mBlockedFromCreateAccount = false; 00239 00243 private $mWatchedItems = array(); 00244 00245 static $idCacheByName = array(); 00246 00257 function __construct() { 00258 $this->clearInstanceCache( 'defaults' ); 00259 } 00260 00264 function __toString() { 00265 return $this->getName(); 00266 } 00267 00271 public function load() { 00272 if ( $this->mLoadedItems === true ) { 00273 return; 00274 } 00275 wfProfileIn( __METHOD__ ); 00276 00277 // Set it now to avoid infinite recursion in accessors 00278 $this->mLoadedItems = true; 00279 00280 switch ( $this->mFrom ) { 00281 case 'defaults': 00282 $this->loadDefaults(); 00283 break; 00284 case 'name': 00285 $this->mId = self::idFromName( $this->mName ); 00286 if ( !$this->mId ) { 00287 // Nonexistent user placeholder object 00288 $this->loadDefaults( $this->mName ); 00289 } else { 00290 $this->loadFromId(); 00291 } 00292 break; 00293 case 'id': 00294 $this->loadFromId(); 00295 break; 00296 case 'session': 00297 if ( !$this->loadFromSession() ) { 00298 // Loading from session failed. Load defaults. 00299 $this->loadDefaults(); 00300 } 00301 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) ); 00302 break; 00303 default: 00304 wfProfileOut( __METHOD__ ); 00305 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" ); 00306 } 00307 wfProfileOut( __METHOD__ ); 00308 } 00309 00314 public function loadFromId() { 00315 global $wgMemc; 00316 if ( $this->mId == 0 ) { 00317 $this->loadDefaults(); 00318 return false; 00319 } 00320 00321 // Try cache 00322 $key = wfMemcKey( 'user', 'id', $this->mId ); 00323 $data = $wgMemc->get( $key ); 00324 if ( !is_array( $data ) || $data['mVersion'] < MW_USER_VERSION ) { 00325 // Object is expired, load from DB 00326 $data = false; 00327 } 00328 00329 if ( !$data ) { 00330 wfDebug( "User: cache miss for user {$this->mId}\n" ); 00331 // Load from DB 00332 if ( !$this->loadFromDatabase() ) { 00333 // Can't load from ID, user is anonymous 00334 return false; 00335 } 00336 $this->saveToCache(); 00337 } else { 00338 wfDebug( "User: got user {$this->mId} from cache\n" ); 00339 // Restore from cache 00340 foreach ( self::$mCacheVars as $name ) { 00341 $this->$name = $data[$name]; 00342 } 00343 } 00344 00345 $this->mLoadedItems = true; 00346 00347 return true; 00348 } 00349 00353 public function saveToCache() { 00354 $this->load(); 00355 $this->loadGroups(); 00356 $this->loadOptions(); 00357 if ( $this->isAnon() ) { 00358 // Anonymous users are uncached 00359 return; 00360 } 00361 $data = array(); 00362 foreach ( self::$mCacheVars as $name ) { 00363 $data[$name] = $this->$name; 00364 } 00365 $data['mVersion'] = MW_USER_VERSION; 00366 $key = wfMemcKey( 'user', 'id', $this->mId ); 00367 global $wgMemc; 00368 $wgMemc->set( $key, $data ); 00369 } 00370 00373 00390 public static function newFromName( $name, $validate = 'valid' ) { 00391 if ( $validate === true ) { 00392 $validate = 'valid'; 00393 } 00394 $name = self::getCanonicalName( $name, $validate ); 00395 if ( $name === false ) { 00396 return false; 00397 } else { 00398 // Create unloaded user object 00399 $u = new User; 00400 $u->mName = $name; 00401 $u->mFrom = 'name'; 00402 $u->setItemLoaded( 'name' ); 00403 return $u; 00404 } 00405 } 00406 00413 public static function newFromId( $id ) { 00414 $u = new User; 00415 $u->mId = $id; 00416 $u->mFrom = 'id'; 00417 $u->setItemLoaded( 'id' ); 00418 return $u; 00419 } 00420 00431 public static function newFromConfirmationCode( $code ) { 00432 $dbr = wfGetDB( DB_SLAVE ); 00433 $id = $dbr->selectField( 'user', 'user_id', array( 00434 'user_email_token' => md5( $code ), 00435 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ), 00436 ) ); 00437 if ( $id !== false ) { 00438 return User::newFromId( $id ); 00439 } else { 00440 return null; 00441 } 00442 } 00443 00451 public static function newFromSession( WebRequest $request = null ) { 00452 $user = new User; 00453 $user->mFrom = 'session'; 00454 $user->mRequest = $request; 00455 return $user; 00456 } 00457 00472 public static function newFromRow( $row, $data = null ) { 00473 $user = new User; 00474 $user->loadFromRow( $row, $data ); 00475 return $user; 00476 } 00477 00479 00485 public static function whoIs( $id ) { 00486 return UserCache::singleton()->getProp( $id, 'name' ); 00487 } 00488 00495 public static function whoIsReal( $id ) { 00496 return UserCache::singleton()->getProp( $id, 'real_name' ); 00497 } 00498 00504 public static function idFromName( $name ) { 00505 $nt = Title::makeTitleSafe( NS_USER, $name ); 00506 if ( is_null( $nt ) ) { 00507 // Illegal name 00508 return null; 00509 } 00510 00511 if ( isset( self::$idCacheByName[$name] ) ) { 00512 return self::$idCacheByName[$name]; 00513 } 00514 00515 $dbr = wfGetDB( DB_SLAVE ); 00516 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ ); 00517 00518 if ( $s === false ) { 00519 $result = null; 00520 } else { 00521 $result = $s->user_id; 00522 } 00523 00524 self::$idCacheByName[$name] = $result; 00525 00526 if ( count( self::$idCacheByName ) > 1000 ) { 00527 self::$idCacheByName = array(); 00528 } 00529 00530 return $result; 00531 } 00532 00536 public static function resetIdByNameCache() { 00537 self::$idCacheByName = array(); 00538 } 00539 00556 public static function isIP( $name ) { 00557 return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name ) || IP::isIPv6( $name ); 00558 } 00559 00571 public static function isValidUserName( $name ) { 00572 global $wgContLang, $wgMaxNameChars; 00573 00574 if ( $name == '' 00575 || User::isIP( $name ) 00576 || strpos( $name, '/' ) !== false 00577 || strlen( $name ) > $wgMaxNameChars 00578 || $name != $wgContLang->ucfirst( $name ) ) { 00579 wfDebugLog( 'username', __METHOD__ . 00580 ": '$name' invalid due to empty, IP, slash, length, or lowercase" ); 00581 return false; 00582 } 00583 00584 // Ensure that the name can't be misresolved as a different title, 00585 // such as with extra namespace keys at the start. 00586 $parsed = Title::newFromText( $name ); 00587 if ( is_null( $parsed ) 00588 || $parsed->getNamespace() 00589 || strcmp( $name, $parsed->getPrefixedText() ) ) { 00590 wfDebugLog( 'username', __METHOD__ . 00591 ": '$name' invalid due to ambiguous prefixes" ); 00592 return false; 00593 } 00594 00595 // Check an additional blacklist of troublemaker characters. 00596 // Should these be merged into the title char list? 00597 $unicodeBlacklist = '/[' . 00598 '\x{0080}-\x{009f}' . # iso-8859-1 control chars 00599 '\x{00a0}' . # non-breaking space 00600 '\x{2000}-\x{200f}' . # various whitespace 00601 '\x{2028}-\x{202f}' . # breaks and control chars 00602 '\x{3000}' . # ideographic space 00603 '\x{e000}-\x{f8ff}' . # private use 00604 ']/u'; 00605 if ( preg_match( $unicodeBlacklist, $name ) ) { 00606 wfDebugLog( 'username', __METHOD__ . 00607 ": '$name' invalid due to blacklisted characters" ); 00608 return false; 00609 } 00610 00611 return true; 00612 } 00613 00625 public static function isUsableName( $name ) { 00626 global $wgReservedUsernames; 00627 // Must be a valid username, obviously ;) 00628 if ( !self::isValidUserName( $name ) ) { 00629 return false; 00630 } 00631 00632 static $reservedUsernames = false; 00633 if ( !$reservedUsernames ) { 00634 $reservedUsernames = $wgReservedUsernames; 00635 wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) ); 00636 } 00637 00638 // Certain names may be reserved for batch processes. 00639 foreach ( $reservedUsernames as $reserved ) { 00640 if ( substr( $reserved, 0, 4 ) == 'msg:' ) { 00641 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text(); 00642 } 00643 if ( $reserved == $name ) { 00644 return false; 00645 } 00646 } 00647 return true; 00648 } 00649 00662 public static function isCreatableName( $name ) { 00663 global $wgInvalidUsernameCharacters; 00664 00665 // Ensure that the username isn't longer than 235 bytes, so that 00666 // (at least for the builtin skins) user javascript and css files 00667 // will work. (bug 23080) 00668 if ( strlen( $name ) > 235 ) { 00669 wfDebugLog( 'username', __METHOD__ . 00670 ": '$name' invalid due to length" ); 00671 return false; 00672 } 00673 00674 // Preg yells if you try to give it an empty string 00675 if ( $wgInvalidUsernameCharacters !== '' ) { 00676 if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) { 00677 wfDebugLog( 'username', __METHOD__ . 00678 ": '$name' invalid due to wgInvalidUsernameCharacters" ); 00679 return false; 00680 } 00681 } 00682 00683 return self::isUsableName( $name ); 00684 } 00685 00692 public function isValidPassword( $password ) { 00693 //simple boolean wrapper for getPasswordValidity 00694 return $this->getPasswordValidity( $password ) === true; 00695 } 00696 00703 public function getPasswordValidity( $password ) { 00704 global $wgMinimalPasswordLength, $wgContLang; 00705 00706 static $blockedLogins = array( 00707 'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589 00708 'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605 00709 ); 00710 00711 $result = false; //init $result to false for the internal checks 00712 00713 if ( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) { 00714 return $result; 00715 } 00716 00717 if ( $result === false ) { 00718 if ( strlen( $password ) < $wgMinimalPasswordLength ) { 00719 return 'passwordtooshort'; 00720 } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) { 00721 return 'password-name-match'; 00722 } elseif ( isset( $blockedLogins[$this->getName()] ) && $password == $blockedLogins[$this->getName()] ) { 00723 return 'password-login-forbidden'; 00724 } else { 00725 //it seems weird returning true here, but this is because of the 00726 //initialization of $result to false above. If the hook is never run or it 00727 //doesn't modify $result, then we will likely get down into this if with 00728 //a valid password. 00729 return true; 00730 } 00731 } elseif ( $result === true ) { 00732 return true; 00733 } else { 00734 return $result; //the isValidPassword hook set a string $result and returned true 00735 } 00736 } 00737 00765 public static function isValidEmailAddr( $addr ) { 00766 wfDeprecated( __METHOD__, '1.18' ); 00767 return Sanitizer::validateEmail( $addr ); 00768 } 00769 00783 public static function getCanonicalName( $name, $validate = 'valid' ) { 00784 // Force usernames to capital 00785 global $wgContLang; 00786 $name = $wgContLang->ucfirst( $name ); 00787 00788 # Reject names containing '#'; these will be cleaned up 00789 # with title normalisation, but then it's too late to 00790 # check elsewhere 00791 if ( strpos( $name, '#' ) !== false ) { 00792 return false; 00793 } 00794 00795 // Clean up name according to title rules 00796 $t = ( $validate === 'valid' ) ? 00797 Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name ); 00798 // Check for invalid titles 00799 if ( is_null( $t ) ) { 00800 return false; 00801 } 00802 00803 // Reject various classes of invalid names 00804 global $wgAuth; 00805 $name = $wgAuth->getCanonicalName( $t->getText() ); 00806 00807 switch ( $validate ) { 00808 case false: 00809 break; 00810 case 'valid': 00811 if ( !User::isValidUserName( $name ) ) { 00812 $name = false; 00813 } 00814 break; 00815 case 'usable': 00816 if ( !User::isUsableName( $name ) ) { 00817 $name = false; 00818 } 00819 break; 00820 case 'creatable': 00821 if ( !User::isCreatableName( $name ) ) { 00822 $name = false; 00823 } 00824 break; 00825 default: 00826 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ ); 00827 } 00828 return $name; 00829 } 00830 00839 public static function edits( $uid ) { 00840 wfDeprecated( __METHOD__, '1.21' ); 00841 $user = self::newFromId( $uid ); 00842 return $user->getEditCount(); 00843 } 00844 00850 public static function randomPassword() { 00851 global $wgMinimalPasswordLength; 00852 // Decide the final password length based on our min password length, stopping at a minimum of 10 chars 00853 $length = max( 10, $wgMinimalPasswordLength ); 00854 // Multiply by 1.25 to get the number of hex characters we need 00855 $length = $length * 1.25; 00856 // Generate random hex chars 00857 $hex = MWCryptRand::generateHex( $length ); 00858 // Convert from base 16 to base 32 to get a proper password like string 00859 return wfBaseConvert( $hex, 16, 32 ); 00860 } 00861 00870 public function loadDefaults( $name = false ) { 00871 wfProfileIn( __METHOD__ ); 00872 00873 $this->mId = 0; 00874 $this->mName = $name; 00875 $this->mRealName = ''; 00876 $this->mPassword = $this->mNewpassword = ''; 00877 $this->mNewpassTime = null; 00878 $this->mEmail = ''; 00879 $this->mOptionOverrides = null; 00880 $this->mOptionsLoaded = false; 00881 00882 $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' ); 00883 if ( $loggedOut !== null ) { 00884 $this->mTouched = wfTimestamp( TS_MW, $loggedOut ); 00885 } else { 00886 $this->mTouched = '1'; # Allow any pages to be cached 00887 } 00888 00889 $this->mToken = null; // Don't run cryptographic functions till we need a token 00890 $this->mEmailAuthenticated = null; 00891 $this->mEmailToken = ''; 00892 $this->mEmailTokenExpires = null; 00893 $this->mRegistration = wfTimestamp( TS_MW ); 00894 $this->mGroups = array(); 00895 00896 wfRunHooks( 'UserLoadDefaults', array( $this, $name ) ); 00897 00898 wfProfileOut( __METHOD__ ); 00899 } 00900 00913 public function isItemLoaded( $item, $all = 'all' ) { 00914 return ( $this->mLoadedItems === true && $all === 'all' ) || 00915 ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true ); 00916 } 00917 00923 protected function setItemLoaded( $item ) { 00924 if ( is_array( $this->mLoadedItems ) ) { 00925 $this->mLoadedItems[$item] = true; 00926 } 00927 } 00928 00933 private function loadFromSession() { 00934 $result = null; 00935 wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) ); 00936 if ( $result !== null ) { 00937 return $result; 00938 } 00939 00940 $request = $this->getRequest(); 00941 00942 $cookieId = $request->getCookie( 'UserID' ); 00943 $sessId = $request->getSessionData( 'wsUserID' ); 00944 00945 if ( $cookieId !== null ) { 00946 $sId = intval( $cookieId ); 00947 if ( $sessId !== null && $cookieId != $sessId ) { 00948 wfDebugLog( 'loginSessions', "Session user ID ($sessId) and 00949 cookie user ID ($sId) don't match!" ); 00950 return false; 00951 } 00952 $request->setSessionData( 'wsUserID', $sId ); 00953 } elseif ( $sessId !== null && $sessId != 0 ) { 00954 $sId = $sessId; 00955 } else { 00956 return false; 00957 } 00958 00959 if ( $request->getSessionData( 'wsUserName' ) !== null ) { 00960 $sName = $request->getSessionData( 'wsUserName' ); 00961 } elseif ( $request->getCookie( 'UserName' ) !== null ) { 00962 $sName = $request->getCookie( 'UserName' ); 00963 $request->setSessionData( 'wsUserName', $sName ); 00964 } else { 00965 return false; 00966 } 00967 00968 $proposedUser = User::newFromId( $sId ); 00969 if ( !$proposedUser->isLoggedIn() ) { 00970 // Not a valid ID 00971 return false; 00972 } 00973 00974 global $wgBlockDisablesLogin; 00975 if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) { 00976 // User blocked and we've disabled blocked user logins 00977 return false; 00978 } 00979 00980 if ( $request->getSessionData( 'wsToken' ) ) { 00981 $passwordCorrect = ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) ); 00982 $from = 'session'; 00983 } elseif ( $request->getCookie( 'Token' ) ) { 00984 # Get the token from DB/cache and clean it up to remove garbage padding. 00985 # This deals with historical problems with bugs and the default column value. 00986 $token = rtrim( $proposedUser->getToken( false ) ); // correct token 00987 // Make comparison in constant time (bug 61346) 00988 $passwordCorrect = strlen( $token ) && $this->compareSecrets( $token, $request->getCookie( 'Token' ) ); 00989 $from = 'cookie'; 00990 } else { 00991 // No session or persistent login cookie 00992 return false; 00993 } 00994 00995 if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) { 00996 $this->loadFromUserObject( $proposedUser ); 00997 $request->setSessionData( 'wsToken', $this->mToken ); 00998 wfDebug( "User: logged in from $from\n" ); 00999 return true; 01000 } else { 01001 // Invalid credentials 01002 wfDebug( "User: can't log in from $from, invalid credentials\n" ); 01003 return false; 01004 } 01005 } 01006 01013 protected function compareSecrets( $answer, $test ) { 01014 if ( strlen( $answer ) !== strlen( $test ) ) { 01015 $passwordCorrect = false; 01016 } else { 01017 $result = 0; 01018 for ( $i = 0; $i < strlen( $answer ); $i++ ) { 01019 $result |= ord( $answer{$i} ) ^ ord( $test{$i} ); 01020 } 01021 $passwordCorrect = ( $result == 0 ); 01022 } 01023 return $passwordCorrect; 01024 } 01025 01032 public function loadFromDatabase() { 01033 // Paranoia 01034 $this->mId = intval( $this->mId ); 01035 01036 // Anonymous user 01037 if ( !$this->mId ) { 01038 $this->loadDefaults(); 01039 return false; 01040 } 01041 01042 $dbr = wfGetDB( DB_MASTER ); 01043 $s = $dbr->selectRow( 'user', self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__ ); 01044 01045 wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) ); 01046 01047 if ( $s !== false ) { 01048 // Initialise user table data 01049 $this->loadFromRow( $s ); 01050 $this->mGroups = null; // deferred 01051 $this->getEditCount(); // revalidation for nulls 01052 return true; 01053 } else { 01054 // Invalid user_id 01055 $this->mId = 0; 01056 $this->loadDefaults(); 01057 return false; 01058 } 01059 } 01060 01070 public function loadFromRow( $row, $data = null ) { 01071 $all = true; 01072 01073 $this->mGroups = null; // deferred 01074 01075 if ( isset( $row->user_name ) ) { 01076 $this->mName = $row->user_name; 01077 $this->mFrom = 'name'; 01078 $this->setItemLoaded( 'name' ); 01079 } else { 01080 $all = false; 01081 } 01082 01083 if ( isset( $row->user_real_name ) ) { 01084 $this->mRealName = $row->user_real_name; 01085 $this->setItemLoaded( 'realname' ); 01086 } else { 01087 $all = false; 01088 } 01089 01090 if ( isset( $row->user_id ) ) { 01091 $this->mId = intval( $row->user_id ); 01092 $this->mFrom = 'id'; 01093 $this->setItemLoaded( 'id' ); 01094 } else { 01095 $all = false; 01096 } 01097 01098 if ( isset( $row->user_editcount ) ) { 01099 $this->mEditCount = $row->user_editcount; 01100 } else { 01101 $all = false; 01102 } 01103 01104 if ( isset( $row->user_password ) ) { 01105 $this->mPassword = $row->user_password; 01106 $this->mNewpassword = $row->user_newpassword; 01107 $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time ); 01108 $this->mEmail = $row->user_email; 01109 if ( isset( $row->user_options ) ) { 01110 $this->decodeOptions( $row->user_options ); 01111 } 01112 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched ); 01113 $this->mToken = $row->user_token; 01114 if ( $this->mToken == '' ) { 01115 $this->mToken = null; 01116 } 01117 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); 01118 $this->mEmailToken = $row->user_email_token; 01119 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); 01120 $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration ); 01121 } else { 01122 $all = false; 01123 } 01124 01125 if ( $all ) { 01126 $this->mLoadedItems = true; 01127 } 01128 01129 if ( is_array( $data ) ) { 01130 if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) { 01131 $this->mGroups = $data['user_groups']; 01132 } 01133 if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) { 01134 $this->loadOptions( $data['user_properties'] ); 01135 } 01136 } 01137 } 01138 01144 protected function loadFromUserObject( $user ) { 01145 $user->load(); 01146 $user->loadGroups(); 01147 $user->loadOptions(); 01148 foreach ( self::$mCacheVars as $var ) { 01149 $this->$var = $user->$var; 01150 } 01151 } 01152 01156 private function loadGroups() { 01157 if ( is_null( $this->mGroups ) ) { 01158 $dbr = wfGetDB( DB_MASTER ); 01159 $res = $dbr->select( 'user_groups', 01160 array( 'ug_group' ), 01161 array( 'ug_user' => $this->mId ), 01162 __METHOD__ ); 01163 $this->mGroups = array(); 01164 foreach ( $res as $row ) { 01165 $this->mGroups[] = $row->ug_group; 01166 } 01167 } 01168 } 01169 01184 public function addAutopromoteOnceGroups( $event ) { 01185 global $wgAutopromoteOnceLogInRC, $wgAuth; 01186 01187 $toPromote = array(); 01188 if ( $this->getId() ) { 01189 $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event ); 01190 if ( count( $toPromote ) ) { 01191 $oldGroups = $this->getGroups(); // previous groups 01192 01193 foreach ( $toPromote as $group ) { 01194 $this->addGroup( $group ); 01195 } 01196 // update groups in external authentication database 01197 $wgAuth->updateExternalDBGroups( $this, $toPromote ); 01198 01199 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups 01200 01201 $logEntry = new ManualLogEntry( 'rights', 'autopromote' ); 01202 $logEntry->setPerformer( $this ); 01203 $logEntry->setTarget( $this->getUserPage() ); 01204 $logEntry->setParameters( array( 01205 '4::oldgroups' => $oldGroups, 01206 '5::newgroups' => $newGroups, 01207 ) ); 01208 $logid = $logEntry->insert(); 01209 if ( $wgAutopromoteOnceLogInRC ) { 01210 $logEntry->publish( $logid ); 01211 } 01212 } 01213 } 01214 return $toPromote; 01215 } 01216 01225 public function clearInstanceCache( $reloadFrom = false ) { 01226 $this->mNewtalk = -1; 01227 $this->mDatePreference = null; 01228 $this->mBlockedby = -1; # Unset 01229 $this->mHash = false; 01230 $this->mRights = null; 01231 $this->mEffectiveGroups = null; 01232 $this->mImplicitGroups = null; 01233 $this->mGroups = null; 01234 $this->mOptions = null; 01235 $this->mOptionsLoaded = false; 01236 $this->mEditCount = null; 01237 01238 if ( $reloadFrom ) { 01239 $this->mLoadedItems = array(); 01240 $this->mFrom = $reloadFrom; 01241 } 01242 } 01243 01250 public static function getDefaultOptions() { 01251 global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin; 01252 01253 static $defOpt = null; 01254 if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) { 01255 // Disabling this for the unit tests, as they rely on being able to change $wgContLang 01256 // mid-request and see that change reflected in the return value of this function. 01257 // Which is insane and would never happen during normal MW operation 01258 return $defOpt; 01259 } 01260 01261 $defOpt = $wgDefaultUserOptions; 01262 // Default language setting 01263 $defOpt['language'] = $wgContLang->getCode(); 01264 foreach ( LanguageConverter::$languagesWithVariants as $langCode ) { 01265 $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode; 01266 } 01267 foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) { 01268 $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] ); 01269 } 01270 $defOpt['skin'] = $wgDefaultSkin; 01271 01272 wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) ); 01273 01274 return $defOpt; 01275 } 01276 01283 public static function getDefaultOption( $opt ) { 01284 $defOpts = self::getDefaultOptions(); 01285 if ( isset( $defOpts[$opt] ) ) { 01286 return $defOpts[$opt]; 01287 } else { 01288 return null; 01289 } 01290 } 01291 01299 private function getBlockedStatus( $bFromSlave = true ) { 01300 global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff; 01301 01302 if ( -1 != $this->mBlockedby ) { 01303 return; 01304 } 01305 01306 wfProfileIn( __METHOD__ ); 01307 wfDebug( __METHOD__ . ": checking...\n" ); 01308 01309 // Initialize data... 01310 // Otherwise something ends up stomping on $this->mBlockedby when 01311 // things get lazy-loaded later, causing false positive block hits 01312 // due to -1 !== 0. Probably session-related... Nothing should be 01313 // overwriting mBlockedby, surely? 01314 $this->load(); 01315 01316 # We only need to worry about passing the IP address to the Block generator if the 01317 # user is not immune to autoblocks/hardblocks, and they are the current user so we 01318 # know which IP address they're actually coming from 01319 if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) { 01320 $ip = $this->getRequest()->getIP(); 01321 } else { 01322 $ip = null; 01323 } 01324 01325 // User/IP blocking 01326 $block = Block::newFromTarget( $this, $ip, !$bFromSlave ); 01327 01328 // Proxy blocking 01329 if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' ) 01330 && !in_array( $ip, $wgProxyWhitelist ) ) 01331 { 01332 // Local list 01333 if ( self::isLocallyBlockedProxy( $ip ) ) { 01334 $block = new Block; 01335 $block->setBlocker( wfMessage( 'proxyblocker' )->text() ); 01336 $block->mReason = wfMessage( 'proxyblockreason' )->text(); 01337 $block->setTarget( $ip ); 01338 } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) { 01339 $block = new Block; 01340 $block->setBlocker( wfMessage( 'sorbs' )->text() ); 01341 $block->mReason = wfMessage( 'sorbsreason' )->text(); 01342 $block->setTarget( $ip ); 01343 } 01344 } 01345 01346 // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled 01347 if ( !$block instanceof Block 01348 && $wgApplyIpBlocksToXff 01349 && $ip !== null 01350 && !$this->isAllowed( 'proxyunbannable' ) 01351 && !in_array( $ip, $wgProxyWhitelist ) 01352 ) { 01353 $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' ); 01354 $xff = array_map( 'trim', explode( ',', $xff ) ); 01355 $xff = array_diff( $xff, array( $ip ) ); 01356 $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave ); 01357 $block = Block::chooseBlock( $xffblocks, $xff ); 01358 if ( $block instanceof Block ) { 01359 # Mangle the reason to alert the user that the block 01360 # originated from matching the X-Forwarded-For header. 01361 $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text(); 01362 } 01363 } 01364 01365 if ( $block instanceof Block ) { 01366 wfDebug( __METHOD__ . ": Found block.\n" ); 01367 $this->mBlock = $block; 01368 $this->mBlockedby = $block->getByName(); 01369 $this->mBlockreason = $block->mReason; 01370 $this->mHideName = $block->mHideName; 01371 $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' ); 01372 } else { 01373 $this->mBlockedby = ''; 01374 $this->mHideName = 0; 01375 $this->mAllowUsertalk = false; 01376 } 01377 01378 // Extensions 01379 wfRunHooks( 'GetBlockedStatus', array( &$this ) ); 01380 01381 wfProfileOut( __METHOD__ ); 01382 } 01383 01391 public function isDnsBlacklisted( $ip, $checkWhitelist = false ) { 01392 global $wgEnableSorbs, $wgEnableDnsBlacklist, 01393 $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist; 01394 01395 if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs ) { 01396 return false; 01397 } 01398 01399 if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) { 01400 return false; 01401 } 01402 01403 $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl ); 01404 return $this->inDnsBlacklist( $ip, $urls ); 01405 } 01406 01414 public function inDnsBlacklist( $ip, $bases ) { 01415 wfProfileIn( __METHOD__ ); 01416 01417 $found = false; 01418 // @todo FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170) 01419 if ( IP::isIPv4( $ip ) ) { 01420 // Reverse IP, bug 21255 01421 $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) ); 01422 01423 foreach ( (array)$bases as $base ) { 01424 // Make hostname 01425 // If we have an access key, use that too (ProjectHoneypot, etc.) 01426 if ( is_array( $base ) ) { 01427 if ( count( $base ) >= 2 ) { 01428 // Access key is 1, base URL is 0 01429 $host = "{$base[1]}.$ipReversed.{$base[0]}"; 01430 } else { 01431 $host = "$ipReversed.{$base[0]}"; 01432 } 01433 } else { 01434 $host = "$ipReversed.$base"; 01435 } 01436 01437 // Send query 01438 $ipList = gethostbynamel( $host ); 01439 01440 if ( $ipList ) { 01441 wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" ); 01442 $found = true; 01443 break; 01444 } else { 01445 wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base.\n" ); 01446 } 01447 } 01448 } 01449 01450 wfProfileOut( __METHOD__ ); 01451 return $found; 01452 } 01453 01461 public static function isLocallyBlockedProxy( $ip ) { 01462 global $wgProxyList; 01463 01464 if ( !$wgProxyList ) { 01465 return false; 01466 } 01467 wfProfileIn( __METHOD__ ); 01468 01469 if ( !is_array( $wgProxyList ) ) { 01470 // Load from the specified file 01471 $wgProxyList = array_map( 'trim', file( $wgProxyList ) ); 01472 } 01473 01474 if ( !is_array( $wgProxyList ) ) { 01475 $ret = false; 01476 } elseif ( array_search( $ip, $wgProxyList ) !== false ) { 01477 $ret = true; 01478 } elseif ( array_key_exists( $ip, $wgProxyList ) ) { 01479 // Old-style flipped proxy list 01480 $ret = true; 01481 } else { 01482 $ret = false; 01483 } 01484 wfProfileOut( __METHOD__ ); 01485 return $ret; 01486 } 01487 01493 public function isPingLimitable() { 01494 global $wgRateLimitsExcludedIPs; 01495 if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) { 01496 // No other good way currently to disable rate limits 01497 // for specific IPs. :P 01498 // But this is a crappy hack and should die. 01499 return false; 01500 } 01501 return !$this->isAllowed( 'noratelimit' ); 01502 } 01503 01515 public function pingLimiter( $action = 'edit', $incrBy = 1 ) { 01516 // Call the 'PingLimiter' hook 01517 $result = false; 01518 if ( !wfRunHooks( 'PingLimiter', array( &$this, $action, &$result, $incrBy ) ) ) { 01519 return $result; 01520 } 01521 01522 global $wgRateLimits; 01523 if ( !isset( $wgRateLimits[$action] ) ) { 01524 return false; 01525 } 01526 01527 // Some groups shouldn't trigger the ping limiter, ever 01528 if ( !$this->isPingLimitable() ) { 01529 return false; 01530 } 01531 01532 global $wgMemc, $wgRateLimitLog; 01533 wfProfileIn( __METHOD__ ); 01534 01535 $limits = $wgRateLimits[$action]; 01536 $keys = array(); 01537 $id = $this->getId(); 01538 $userLimit = false; 01539 01540 if ( isset( $limits['anon'] ) && $id == 0 ) { 01541 $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon']; 01542 } 01543 01544 if ( isset( $limits['user'] ) && $id != 0 ) { 01545 $userLimit = $limits['user']; 01546 } 01547 if ( $this->isNewbie() ) { 01548 if ( isset( $limits['newbie'] ) && $id != 0 ) { 01549 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie']; 01550 } 01551 if ( isset( $limits['ip'] ) ) { 01552 $ip = $this->getRequest()->getIP(); 01553 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip']; 01554 } 01555 if ( isset( $limits['subnet'] ) ) { 01556 $ip = $this->getRequest()->getIP(); 01557 $matches = array(); 01558 $subnet = false; 01559 if ( IP::isIPv6( $ip ) ) { 01560 $parts = IP::parseRange( "$ip/64" ); 01561 $subnet = $parts[0]; 01562 } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) { 01563 // IPv4 01564 $subnet = $matches[1]; 01565 } 01566 if ( $subnet !== false ) { 01567 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet']; 01568 } 01569 } 01570 } 01571 // Check for group-specific permissions 01572 // If more than one group applies, use the group with the highest limit 01573 foreach ( $this->getGroups() as $group ) { 01574 if ( isset( $limits[$group] ) ) { 01575 if ( $userLimit === false || $limits[$group] > $userLimit ) { 01576 $userLimit = $limits[$group]; 01577 } 01578 } 01579 } 01580 // Set the user limit key 01581 if ( $userLimit !== false ) { 01582 list( $max, $period ) = $userLimit; 01583 wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" ); 01584 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit; 01585 } 01586 01587 $triggered = false; 01588 foreach ( $keys as $key => $limit ) { 01589 list( $max, $period ) = $limit; 01590 $summary = "(limit $max in {$period}s)"; 01591 $count = $wgMemc->get( $key ); 01592 // Already pinged? 01593 if ( $count ) { 01594 if ( $count >= $max ) { 01595 wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" ); 01596 if ( $wgRateLimitLog ) { 01597 wfSuppressWarnings(); 01598 file_put_contents( $wgRateLimitLog, wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", FILE_APPEND ); 01599 wfRestoreWarnings(); 01600 } 01601 $triggered = true; 01602 } else { 01603 wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" ); 01604 } 01605 } else { 01606 wfDebug( __METHOD__ . ": adding record for $key $summary\n" ); 01607 if ( $incrBy > 0 ) { 01608 $wgMemc->add( $key, 0, intval( $period ) ); // first ping 01609 } 01610 } 01611 if ( $incrBy > 0 ) { 01612 $wgMemc->incr( $key, $incrBy ); 01613 } 01614 } 01615 01616 wfProfileOut( __METHOD__ ); 01617 return $triggered; 01618 } 01619 01626 public function isBlocked( $bFromSlave = true ) { // hacked from false due to horrible probs on site 01627 return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' ); 01628 } 01629 01636 public function getBlock( $bFromSlave = true ) { 01637 $this->getBlockedStatus( $bFromSlave ); 01638 return $this->mBlock instanceof Block ? $this->mBlock : null; 01639 } 01640 01648 function isBlockedFrom( $title, $bFromSlave = false ) { 01649 global $wgBlockAllowsUTEdit; 01650 wfProfileIn( __METHOD__ ); 01651 01652 $blocked = $this->isBlocked( $bFromSlave ); 01653 $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false ); 01654 // If a user's name is suppressed, they cannot make edits anywhere 01655 if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() && 01656 $title->getNamespace() == NS_USER_TALK ) { 01657 $blocked = false; 01658 wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" ); 01659 } 01660 01661 wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) ); 01662 01663 wfProfileOut( __METHOD__ ); 01664 return $blocked; 01665 } 01666 01671 public function blockedBy() { 01672 $this->getBlockedStatus(); 01673 return $this->mBlockedby; 01674 } 01675 01680 public function blockedFor() { 01681 $this->getBlockedStatus(); 01682 return $this->mBlockreason; 01683 } 01684 01689 public function getBlockId() { 01690 $this->getBlockedStatus(); 01691 return ( $this->mBlock ? $this->mBlock->getId() : false ); 01692 } 01693 01702 public function isBlockedGlobally( $ip = '' ) { 01703 if ( $this->mBlockedGlobally !== null ) { 01704 return $this->mBlockedGlobally; 01705 } 01706 // User is already an IP? 01707 if ( IP::isIPAddress( $this->getName() ) ) { 01708 $ip = $this->getName(); 01709 } elseif ( !$ip ) { 01710 $ip = $this->getRequest()->getIP(); 01711 } 01712 $blocked = false; 01713 wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) ); 01714 $this->mBlockedGlobally = (bool)$blocked; 01715 return $this->mBlockedGlobally; 01716 } 01717 01723 public function isLocked() { 01724 if ( $this->mLocked !== null ) { 01725 return $this->mLocked; 01726 } 01727 global $wgAuth; 01728 StubObject::unstub( $wgAuth ); 01729 $authUser = $wgAuth->getUserInstance( $this ); 01730 $this->mLocked = (bool)$authUser->isLocked(); 01731 return $this->mLocked; 01732 } 01733 01739 public function isHidden() { 01740 if ( $this->mHideName !== null ) { 01741 return $this->mHideName; 01742 } 01743 $this->getBlockedStatus(); 01744 if ( !$this->mHideName ) { 01745 global $wgAuth; 01746 StubObject::unstub( $wgAuth ); 01747 $authUser = $wgAuth->getUserInstance( $this ); 01748 $this->mHideName = (bool)$authUser->isHidden(); 01749 } 01750 return $this->mHideName; 01751 } 01752 01757 public function getId() { 01758 if ( $this->mId === null && $this->mName !== null && User::isIP( $this->mName ) ) { 01759 // Special case, we know the user is anonymous 01760 return 0; 01761 } elseif ( !$this->isItemLoaded( 'id' ) ) { 01762 // Don't load if this was initialized from an ID 01763 $this->load(); 01764 } 01765 return $this->mId; 01766 } 01767 01772 public function setId( $v ) { 01773 $this->mId = $v; 01774 $this->clearInstanceCache( 'id' ); 01775 } 01776 01781 public function getName() { 01782 if ( $this->isItemLoaded( 'name', 'only' ) ) { 01783 // Special case optimisation 01784 return $this->mName; 01785 } else { 01786 $this->load(); 01787 if ( $this->mName === false ) { 01788 // Clean up IPs 01789 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() ); 01790 } 01791 return $this->mName; 01792 } 01793 } 01794 01808 public function setName( $str ) { 01809 $this->load(); 01810 $this->mName = $str; 01811 } 01812 01817 public function getTitleKey() { 01818 return str_replace( ' ', '_', $this->getName() ); 01819 } 01820 01825 public function getNewtalk() { 01826 $this->load(); 01827 01828 // Load the newtalk status if it is unloaded (mNewtalk=-1) 01829 if ( $this->mNewtalk === -1 ) { 01830 $this->mNewtalk = false; # reset talk page status 01831 01832 // Check memcached separately for anons, who have no 01833 // entire User object stored in there. 01834 if ( !$this->mId ) { 01835 global $wgDisableAnonTalk; 01836 if ( $wgDisableAnonTalk ) { 01837 // Anon newtalk disabled by configuration. 01838 $this->mNewtalk = false; 01839 } else { 01840 global $wgMemc; 01841 $key = wfMemcKey( 'newtalk', 'ip', $this->getName() ); 01842 $newtalk = $wgMemc->get( $key ); 01843 if ( strval( $newtalk ) !== '' ) { 01844 $this->mNewtalk = (bool)$newtalk; 01845 } else { 01846 // Since we are caching this, make sure it is up to date by getting it 01847 // from the master 01848 $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true ); 01849 $wgMemc->set( $key, (int)$this->mNewtalk, 1800 ); 01850 } 01851 } 01852 } else { 01853 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId ); 01854 } 01855 } 01856 01857 return (bool)$this->mNewtalk; 01858 } 01859 01873 public function getNewMessageLinks() { 01874 $talks = array(); 01875 if ( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) { 01876 return $talks; 01877 } elseif ( !$this->getNewtalk() ) { 01878 return array(); 01879 } 01880 $utp = $this->getTalkPage(); 01881 $dbr = wfGetDB( DB_SLAVE ); 01882 // Get the "last viewed rev" timestamp from the oldest message notification 01883 $timestamp = $dbr->selectField( 'user_newtalk', 01884 'MIN(user_last_timestamp)', 01885 $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ), 01886 __METHOD__ ); 01887 $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null; 01888 return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) ); 01889 } 01890 01896 public function getNewMessageRevisionId() { 01897 $newMessageRevisionId = null; 01898 $newMessageLinks = $this->getNewMessageLinks(); 01899 if ( $newMessageLinks ) { 01900 // Note: getNewMessageLinks() never returns more than a single link 01901 // and it is always for the same wiki, but we double-check here in 01902 // case that changes some time in the future. 01903 if ( count( $newMessageLinks ) === 1 01904 && $newMessageLinks[0]['wiki'] === wfWikiID() 01905 && $newMessageLinks[0]['rev'] 01906 ) { 01907 $newMessageRevision = $newMessageLinks[0]['rev']; 01908 $newMessageRevisionId = $newMessageRevision->getId(); 01909 } 01910 } 01911 return $newMessageRevisionId; 01912 } 01913 01923 protected function checkNewtalk( $field, $id, $fromMaster = false ) { 01924 if ( $fromMaster ) { 01925 $db = wfGetDB( DB_MASTER ); 01926 } else { 01927 $db = wfGetDB( DB_SLAVE ); 01928 } 01929 $ok = $db->selectField( 'user_newtalk', $field, 01930 array( $field => $id ), __METHOD__ ); 01931 return $ok !== false; 01932 } 01933 01941 protected function updateNewtalk( $field, $id, $curRev = null ) { 01942 // Get timestamp of the talk page revision prior to the current one 01943 $prevRev = $curRev ? $curRev->getPrevious() : false; 01944 $ts = $prevRev ? $prevRev->getTimestamp() : null; 01945 // Mark the user as having new messages since this revision 01946 $dbw = wfGetDB( DB_MASTER ); 01947 $dbw->insert( 'user_newtalk', 01948 array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ), 01949 __METHOD__, 01950 'IGNORE' ); 01951 if ( $dbw->affectedRows() ) { 01952 wfDebug( __METHOD__ . ": set on ($field, $id)\n" ); 01953 return true; 01954 } else { 01955 wfDebug( __METHOD__ . " already set ($field, $id)\n" ); 01956 return false; 01957 } 01958 } 01959 01966 protected function deleteNewtalk( $field, $id ) { 01967 $dbw = wfGetDB( DB_MASTER ); 01968 $dbw->delete( 'user_newtalk', 01969 array( $field => $id ), 01970 __METHOD__ ); 01971 if ( $dbw->affectedRows() ) { 01972 wfDebug( __METHOD__ . ": killed on ($field, $id)\n" ); 01973 return true; 01974 } else { 01975 wfDebug( __METHOD__ . ": already gone ($field, $id)\n" ); 01976 return false; 01977 } 01978 } 01979 01985 public function setNewtalk( $val, $curRev = null ) { 01986 if ( wfReadOnly() ) { 01987 return; 01988 } 01989 01990 $this->load(); 01991 $this->mNewtalk = $val; 01992 01993 if ( $this->isAnon() ) { 01994 $field = 'user_ip'; 01995 $id = $this->getName(); 01996 } else { 01997 $field = 'user_id'; 01998 $id = $this->getId(); 01999 } 02000 global $wgMemc; 02001 02002 if ( $val ) { 02003 $changed = $this->updateNewtalk( $field, $id, $curRev ); 02004 } else { 02005 $changed = $this->deleteNewtalk( $field, $id ); 02006 } 02007 02008 if ( $this->isAnon() ) { 02009 // Anons have a separate memcached space, since 02010 // user records aren't kept for them. 02011 $key = wfMemcKey( 'newtalk', 'ip', $id ); 02012 $wgMemc->set( $key, $val ? 1 : 0, 1800 ); 02013 } 02014 if ( $changed ) { 02015 $this->invalidateCache(); 02016 } 02017 } 02018 02024 private static function newTouchedTimestamp() { 02025 global $wgClockSkewFudge; 02026 return wfTimestamp( TS_MW, time() + $wgClockSkewFudge ); 02027 } 02028 02036 private function clearSharedCache() { 02037 $this->load(); 02038 if ( $this->mId ) { 02039 global $wgMemc; 02040 $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) ); 02041 } 02042 } 02043 02049 public function invalidateCache() { 02050 if ( wfReadOnly() ) { 02051 return; 02052 } 02053 $this->load(); 02054 if ( $this->mId ) { 02055 $this->mTouched = self::newTouchedTimestamp(); 02056 02057 $dbw = wfGetDB( DB_MASTER ); 02058 $userid = $this->mId; 02059 $touched = $this->mTouched; 02060 $method = __METHOD__; 02061 $dbw->onTransactionIdle( function() use ( $dbw, $userid, $touched, $method ) { 02062 // Prevent contention slams by checking user_touched first 02063 $encTouched = $dbw->addQuotes( $dbw->timestamp( $touched ) ); 02064 $needsPurge = $dbw->selectField( 'user', '1', 02065 array( 'user_id' => $userid, 'user_touched < ' . $encTouched ) ); 02066 if ( $needsPurge ) { 02067 $dbw->update( 'user', 02068 array( 'user_touched' => $dbw->timestamp( $touched ) ), 02069 array( 'user_id' => $userid, 'user_touched < ' . $encTouched ), 02070 $method 02071 ); 02072 } 02073 } ); 02074 $this->clearSharedCache(); 02075 } 02076 } 02077 02083 public function validateCache( $timestamp ) { 02084 $this->load(); 02085 return ( $timestamp >= $this->mTouched ); 02086 } 02087 02092 public function getTouched() { 02093 $this->load(); 02094 return $this->mTouched; 02095 } 02096 02113 public function setPassword( $str ) { 02114 global $wgAuth; 02115 02116 if ( $str !== null ) { 02117 if ( !$wgAuth->allowPasswordChange() ) { 02118 throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() ); 02119 } 02120 02121 if ( !$this->isValidPassword( $str ) ) { 02122 global $wgMinimalPasswordLength; 02123 $valid = $this->getPasswordValidity( $str ); 02124 if ( is_array( $valid ) ) { 02125 $message = array_shift( $valid ); 02126 $params = $valid; 02127 } else { 02128 $message = $valid; 02129 $params = array( $wgMinimalPasswordLength ); 02130 } 02131 throw new PasswordError( wfMessage( $message, $params )->text() ); 02132 } 02133 } 02134 02135 if ( !$wgAuth->setPassword( $this, $str ) ) { 02136 throw new PasswordError( wfMessage( 'externaldberror' )->text() ); 02137 } 02138 02139 $this->setInternalPassword( $str ); 02140 02141 return true; 02142 } 02143 02151 public function setInternalPassword( $str ) { 02152 $this->load(); 02153 $this->setToken(); 02154 02155 if ( $str === null ) { 02156 // Save an invalid hash... 02157 $this->mPassword = ''; 02158 } else { 02159 $this->mPassword = self::crypt( $str ); 02160 } 02161 $this->mNewpassword = ''; 02162 $this->mNewpassTime = null; 02163 } 02164 02170 public function getToken( $forceCreation = true ) { 02171 $this->load(); 02172 if ( !$this->mToken && $forceCreation ) { 02173 $this->setToken(); 02174 } 02175 return $this->mToken; 02176 } 02177 02184 public function setToken( $token = false ) { 02185 $this->load(); 02186 if ( !$token ) { 02187 $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH ); 02188 } else { 02189 $this->mToken = $token; 02190 } 02191 } 02192 02199 public function setNewpassword( $str, $throttle = true ) { 02200 $this->load(); 02201 $this->mNewpassword = self::crypt( $str ); 02202 if ( $throttle ) { 02203 $this->mNewpassTime = wfTimestampNow(); 02204 } 02205 } 02206 02212 public function isPasswordReminderThrottled() { 02213 global $wgPasswordReminderResendTime; 02214 $this->load(); 02215 if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) { 02216 return false; 02217 } 02218 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600; 02219 return time() < $expiry; 02220 } 02221 02226 public function getEmail() { 02227 $this->load(); 02228 wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) ); 02229 return $this->mEmail; 02230 } 02231 02236 public function getEmailAuthenticationTimestamp() { 02237 $this->load(); 02238 wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) ); 02239 return $this->mEmailAuthenticated; 02240 } 02241 02246 public function setEmail( $str ) { 02247 $this->load(); 02248 if ( $str == $this->mEmail ) { 02249 return; 02250 } 02251 $this->mEmail = $str; 02252 $this->invalidateEmail(); 02253 wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) ); 02254 } 02255 02263 public function setEmailWithConfirmation( $str ) { 02264 global $wgEnableEmail, $wgEmailAuthentication; 02265 02266 if ( !$wgEnableEmail ) { 02267 return Status::newFatal( 'emaildisabled' ); 02268 } 02269 02270 $oldaddr = $this->getEmail(); 02271 if ( $str === $oldaddr ) { 02272 return Status::newGood( true ); 02273 } 02274 02275 $this->setEmail( $str ); 02276 02277 if ( $str !== '' && $wgEmailAuthentication ) { 02278 // Send a confirmation request to the new address if needed 02279 $type = $oldaddr != '' ? 'changed' : 'set'; 02280 $result = $this->sendConfirmationMail( $type ); 02281 if ( $result->isGood() ) { 02282 // Say the the caller that a confirmation mail has been sent 02283 $result->value = 'eauth'; 02284 } 02285 } else { 02286 $result = Status::newGood( true ); 02287 } 02288 02289 return $result; 02290 } 02291 02296 public function getRealName() { 02297 if ( !$this->isItemLoaded( 'realname' ) ) { 02298 $this->load(); 02299 } 02300 02301 return $this->mRealName; 02302 } 02303 02308 public function setRealName( $str ) { 02309 $this->load(); 02310 $this->mRealName = $str; 02311 } 02312 02323 public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) { 02324 global $wgHiddenPrefs; 02325 $this->loadOptions(); 02326 02327 # We want 'disabled' preferences to always behave as the default value for 02328 # users, even if they have set the option explicitly in their settings (ie they 02329 # set it, and then it was disabled removing their ability to change it). But 02330 # we don't want to erase the preferences in the database in case the preference 02331 # is re-enabled again. So don't touch $mOptions, just override the returned value 02332 if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) { 02333 return self::getDefaultOption( $oname ); 02334 } 02335 02336 if ( array_key_exists( $oname, $this->mOptions ) ) { 02337 return $this->mOptions[$oname]; 02338 } else { 02339 return $defaultOverride; 02340 } 02341 } 02342 02348 public function getOptions() { 02349 global $wgHiddenPrefs; 02350 $this->loadOptions(); 02351 $options = $this->mOptions; 02352 02353 # We want 'disabled' preferences to always behave as the default value for 02354 # users, even if they have set the option explicitly in their settings (ie they 02355 # set it, and then it was disabled removing their ability to change it). But 02356 # we don't want to erase the preferences in the database in case the preference 02357 # is re-enabled again. So don't touch $mOptions, just override the returned value 02358 foreach ( $wgHiddenPrefs as $pref ) { 02359 $default = self::getDefaultOption( $pref ); 02360 if ( $default !== null ) { 02361 $options[$pref] = $default; 02362 } 02363 } 02364 02365 return $options; 02366 } 02367 02375 public function getBoolOption( $oname ) { 02376 return (bool)$this->getOption( $oname ); 02377 } 02378 02387 public function getIntOption( $oname, $defaultOverride = 0 ) { 02388 $val = $this->getOption( $oname ); 02389 if ( $val == '' ) { 02390 $val = $defaultOverride; 02391 } 02392 return intval( $val ); 02393 } 02394 02401 public function setOption( $oname, $val ) { 02402 $this->loadOptions(); 02403 02404 // Explicitly NULL values should refer to defaults 02405 if ( is_null( $val ) ) { 02406 $val = self::getDefaultOption( $oname ); 02407 } 02408 02409 $this->mOptions[$oname] = $val; 02410 } 02411 02421 public function getTokenFromOption( $oname ) { 02422 global $wgHiddenPrefs; 02423 if ( in_array( $oname, $wgHiddenPrefs ) ) { 02424 return false; 02425 } 02426 02427 $token = $this->getOption( $oname ); 02428 if ( !$token ) { 02429 $token = $this->resetTokenFromOption( $oname ); 02430 $this->saveSettings(); 02431 } 02432 return $token; 02433 } 02434 02444 public function resetTokenFromOption( $oname ) { 02445 global $wgHiddenPrefs; 02446 if ( in_array( $oname, $wgHiddenPrefs ) ) { 02447 return false; 02448 } 02449 02450 $token = MWCryptRand::generateHex( 40 ); 02451 $this->setOption( $oname, $token ); 02452 return $token; 02453 } 02454 02476 public static function listOptionKinds() { 02477 return array( 02478 'registered', 02479 'registered-multiselect', 02480 'registered-checkmatrix', 02481 'userjs', 02482 'unused' 02483 ); 02484 } 02485 02497 public function getOptionKinds( IContextSource $context, $options = null ) { 02498 $this->loadOptions(); 02499 if ( $options === null ) { 02500 $options = $this->mOptions; 02501 } 02502 02503 $prefs = Preferences::getPreferences( $this, $context ); 02504 $mapping = array(); 02505 02506 // Multiselect and checkmatrix options are stored in the database with 02507 // one key per option, each having a boolean value. Extract those keys. 02508 $multiselectOptions = array(); 02509 foreach ( $prefs as $name => $info ) { 02510 if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) || 02511 ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) { 02512 $opts = HTMLFormField::flattenOptions( $info['options'] ); 02513 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name; 02514 02515 foreach ( $opts as $value ) { 02516 $multiselectOptions["$prefix$value"] = true; 02517 } 02518 02519 unset( $prefs[$name] ); 02520 } 02521 } 02522 $checkmatrixOptions = array(); 02523 foreach ( $prefs as $name => $info ) { 02524 if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) || 02525 ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) { 02526 $columns = HTMLFormField::flattenOptions( $info['columns'] ); 02527 $rows = HTMLFormField::flattenOptions( $info['rows'] ); 02528 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name; 02529 02530 foreach ( $columns as $column ) { 02531 foreach ( $rows as $row ) { 02532 $checkmatrixOptions["$prefix-$column-$row"] = true; 02533 } 02534 } 02535 02536 unset( $prefs[$name] ); 02537 } 02538 } 02539 02540 // $value is ignored 02541 foreach ( $options as $key => $value ) { 02542 if ( isset( $prefs[$key] ) ) { 02543 $mapping[$key] = 'registered'; 02544 } elseif ( isset( $multiselectOptions[$key] ) ) { 02545 $mapping[$key] = 'registered-multiselect'; 02546 } elseif ( isset( $checkmatrixOptions[$key] ) ) { 02547 $mapping[$key] = 'registered-checkmatrix'; 02548 } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) { 02549 $mapping[$key] = 'userjs'; 02550 } else { 02551 $mapping[$key] = 'unused'; 02552 } 02553 } 02554 02555 return $mapping; 02556 } 02557 02572 public function resetOptions( 02573 $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ), 02574 IContextSource $context = null 02575 ) { 02576 $this->load(); 02577 $defaultOptions = self::getDefaultOptions(); 02578 02579 if ( !is_array( $resetKinds ) ) { 02580 $resetKinds = array( $resetKinds ); 02581 } 02582 02583 if ( in_array( 'all', $resetKinds ) ) { 02584 $newOptions = $defaultOptions; 02585 } else { 02586 if ( $context === null ) { 02587 $context = RequestContext::getMain(); 02588 } 02589 02590 $optionKinds = $this->getOptionKinds( $context ); 02591 $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() ); 02592 $newOptions = array(); 02593 02594 // Use default values for the options that should be deleted, and 02595 // copy old values for the ones that shouldn't. 02596 foreach ( $this->mOptions as $key => $value ) { 02597 if ( in_array( $optionKinds[$key], $resetKinds ) ) { 02598 if ( array_key_exists( $key, $defaultOptions ) ) { 02599 $newOptions[$key] = $defaultOptions[$key]; 02600 } 02601 } else { 02602 $newOptions[$key] = $value; 02603 } 02604 } 02605 } 02606 02607 $this->mOptions = $newOptions; 02608 $this->mOptionsLoaded = true; 02609 } 02610 02615 public function getDatePreference() { 02616 // Important migration for old data rows 02617 if ( is_null( $this->mDatePreference ) ) { 02618 global $wgLang; 02619 $value = $this->getOption( 'date' ); 02620 $map = $wgLang->getDatePreferenceMigrationMap(); 02621 if ( isset( $map[$value] ) ) { 02622 $value = $map[$value]; 02623 } 02624 $this->mDatePreference = $value; 02625 } 02626 return $this->mDatePreference; 02627 } 02628 02635 public function requiresHTTPS() { 02636 global $wgSecureLogin; 02637 if ( !$wgSecureLogin ) { 02638 return false; 02639 } else { 02640 $https = $this->getBoolOption( 'prefershttps' ); 02641 wfRunHooks( 'UserRequiresHTTPS', array( $this, &$https ) ); 02642 if ( $https ) { 02643 $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() ); 02644 } 02645 return $https; 02646 } 02647 } 02648 02654 public function getStubThreshold() { 02655 global $wgMaxArticleSize; # Maximum article size, in Kb 02656 $threshold = $this->getIntOption( 'stubthreshold' ); 02657 if ( $threshold > $wgMaxArticleSize * 1024 ) { 02658 // If they have set an impossible value, disable the preference 02659 // so we can use the parser cache again. 02660 $threshold = 0; 02661 } 02662 return $threshold; 02663 } 02664 02669 public function getRights() { 02670 if ( is_null( $this->mRights ) ) { 02671 $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); 02672 wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) ); 02673 // Force reindexation of rights when a hook has unset one of them 02674 $this->mRights = array_values( array_unique( $this->mRights ) ); 02675 } 02676 return $this->mRights; 02677 } 02678 02684 public function getGroups() { 02685 $this->load(); 02686 $this->loadGroups(); 02687 return $this->mGroups; 02688 } 02689 02697 public function getEffectiveGroups( $recache = false ) { 02698 if ( $recache || is_null( $this->mEffectiveGroups ) ) { 02699 wfProfileIn( __METHOD__ ); 02700 $this->mEffectiveGroups = array_unique( array_merge( 02701 $this->getGroups(), // explicit groups 02702 $this->getAutomaticGroups( $recache ) // implicit groups 02703 ) ); 02704 // Hook for additional groups 02705 wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) ); 02706 // Force reindexation of groups when a hook has unset one of them 02707 $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) ); 02708 wfProfileOut( __METHOD__ ); 02709 } 02710 return $this->mEffectiveGroups; 02711 } 02712 02720 public function getAutomaticGroups( $recache = false ) { 02721 if ( $recache || is_null( $this->mImplicitGroups ) ) { 02722 wfProfileIn( __METHOD__ ); 02723 $this->mImplicitGroups = array( '*' ); 02724 if ( $this->getId() ) { 02725 $this->mImplicitGroups[] = 'user'; 02726 02727 $this->mImplicitGroups = array_unique( array_merge( 02728 $this->mImplicitGroups, 02729 Autopromote::getAutopromoteGroups( $this ) 02730 ) ); 02731 } 02732 if ( $recache ) { 02733 // Assure data consistency with rights/groups, 02734 // as getEffectiveGroups() depends on this function 02735 $this->mEffectiveGroups = null; 02736 } 02737 wfProfileOut( __METHOD__ ); 02738 } 02739 return $this->mImplicitGroups; 02740 } 02741 02751 public function getFormerGroups() { 02752 if ( is_null( $this->mFormerGroups ) ) { 02753 $dbr = wfGetDB( DB_MASTER ); 02754 $res = $dbr->select( 'user_former_groups', 02755 array( 'ufg_group' ), 02756 array( 'ufg_user' => $this->mId ), 02757 __METHOD__ ); 02758 $this->mFormerGroups = array(); 02759 foreach ( $res as $row ) { 02760 $this->mFormerGroups[] = $row->ufg_group; 02761 } 02762 } 02763 return $this->mFormerGroups; 02764 } 02765 02770 public function getEditCount() { 02771 if ( !$this->getId() ) { 02772 return null; 02773 } 02774 02775 if ( !isset( $this->mEditCount ) ) { 02776 /* Populate the count, if it has not been populated yet */ 02777 wfProfileIn( __METHOD__ ); 02778 $dbr = wfGetDB( DB_SLAVE ); 02779 // check if the user_editcount field has been initialized 02780 $count = $dbr->selectField( 02781 'user', 'user_editcount', 02782 array( 'user_id' => $this->mId ), 02783 __METHOD__ 02784 ); 02785 02786 if ( $count === null ) { 02787 // it has not been initialized. do so. 02788 $count = $this->initEditCount(); 02789 } 02790 $this->mEditCount = $count; 02791 wfProfileOut( __METHOD__ ); 02792 } 02793 return (int)$this->mEditCount; 02794 } 02795 02801 public function addGroup( $group ) { 02802 if ( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) { 02803 $dbw = wfGetDB( DB_MASTER ); 02804 if ( $this->getId() ) { 02805 $dbw->insert( 'user_groups', 02806 array( 02807 'ug_user' => $this->getID(), 02808 'ug_group' => $group, 02809 ), 02810 __METHOD__, 02811 array( 'IGNORE' ) ); 02812 } 02813 } 02814 $this->loadGroups(); 02815 $this->mGroups[] = $group; 02816 // In case loadGroups was not called before, we now have the right twice. 02817 // Get rid of the duplicate. 02818 $this->mGroups = array_unique( $this->mGroups ); 02819 02820 // Refresh the groups caches, and clear the rights cache so it will be 02821 // refreshed on the next call to $this->getRights(). 02822 $this->getEffectiveGroups( true ); 02823 $this->mRights = null; 02824 02825 $this->invalidateCache(); 02826 } 02827 02833 public function removeGroup( $group ) { 02834 $this->load(); 02835 if ( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) { 02836 $dbw = wfGetDB( DB_MASTER ); 02837 $dbw->delete( 'user_groups', 02838 array( 02839 'ug_user' => $this->getID(), 02840 'ug_group' => $group, 02841 ), __METHOD__ ); 02842 // Remember that the user was in this group 02843 $dbw->insert( 'user_former_groups', 02844 array( 02845 'ufg_user' => $this->getID(), 02846 'ufg_group' => $group, 02847 ), 02848 __METHOD__, 02849 array( 'IGNORE' ) ); 02850 } 02851 $this->loadGroups(); 02852 $this->mGroups = array_diff( $this->mGroups, array( $group ) ); 02853 02854 // Refresh the groups caches, and clear the rights cache so it will be 02855 // refreshed on the next call to $this->getRights(). 02856 $this->getEffectiveGroups( true ); 02857 $this->mRights = null; 02858 02859 $this->invalidateCache(); 02860 } 02861 02866 public function isLoggedIn() { 02867 return $this->getID() != 0; 02868 } 02869 02874 public function isAnon() { 02875 return !$this->isLoggedIn(); 02876 } 02877 02886 public function isAllowedAny( /*...*/ ) { 02887 $permissions = func_get_args(); 02888 foreach ( $permissions as $permission ) { 02889 if ( $this->isAllowed( $permission ) ) { 02890 return true; 02891 } 02892 } 02893 return false; 02894 } 02895 02901 public function isAllowedAll( /*...*/ ) { 02902 $permissions = func_get_args(); 02903 foreach ( $permissions as $permission ) { 02904 if ( !$this->isAllowed( $permission ) ) { 02905 return false; 02906 } 02907 } 02908 return true; 02909 } 02910 02916 public function isAllowed( $action = '' ) { 02917 if ( $action === '' ) { 02918 return true; // In the spirit of DWIM 02919 } 02920 // Patrolling may not be enabled 02921 if ( $action === 'patrol' || $action === 'autopatrol' ) { 02922 global $wgUseRCPatrol, $wgUseNPPatrol; 02923 if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) { 02924 return false; 02925 } 02926 } 02927 // Use strict parameter to avoid matching numeric 0 accidentally inserted 02928 // by misconfiguration: 0 == 'foo' 02929 return in_array( $action, $this->getRights(), true ); 02930 } 02931 02936 public function useRCPatrol() { 02937 global $wgUseRCPatrol; 02938 return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' ); 02939 } 02940 02945 public function useNPPatrol() { 02946 global $wgUseRCPatrol, $wgUseNPPatrol; 02947 return ( 02948 ( $wgUseRCPatrol || $wgUseNPPatrol ) 02949 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) ) 02950 ); 02951 } 02952 02958 public function getRequest() { 02959 if ( $this->mRequest ) { 02960 return $this->mRequest; 02961 } else { 02962 global $wgRequest; 02963 return $wgRequest; 02964 } 02965 } 02966 02973 public function getSkin() { 02974 wfDeprecated( __METHOD__, '1.18' ); 02975 return RequestContext::getMain()->getSkin(); 02976 } 02977 02987 public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) { 02988 $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey(); 02989 02990 if ( isset( $this->mWatchedItems[$key] ) ) { 02991 return $this->mWatchedItems[$key]; 02992 } 02993 02994 if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) { 02995 $this->mWatchedItems = array(); 02996 } 02997 02998 $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights ); 02999 return $this->mWatchedItems[$key]; 03000 } 03001 03010 public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) { 03011 return $this->getWatchedItem( $title, $checkRights )->isWatched(); 03012 } 03013 03021 public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) { 03022 $this->getWatchedItem( $title, $checkRights )->addWatch(); 03023 $this->invalidateCache(); 03024 } 03025 03033 public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) { 03034 $this->getWatchedItem( $title, $checkRights )->removeWatch(); 03035 $this->invalidateCache(); 03036 } 03037 03045 public function clearNotification( &$title ) { 03046 global $wgUseEnotif, $wgShowUpdatedMarker; 03047 03048 // Do nothing if the database is locked to writes 03049 if ( wfReadOnly() ) { 03050 return; 03051 } 03052 03053 // Do nothing if not allowed to edit the watchlist 03054 if ( !$this->isAllowed( 'editmywatchlist' ) ) { 03055 return; 03056 } 03057 03058 if ( $title->getNamespace() == NS_USER_TALK && 03059 $title->getText() == $this->getName() ) { 03060 if ( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) ) { 03061 return; 03062 } 03063 $this->setNewtalk( false ); 03064 } 03065 03066 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) { 03067 return; 03068 } 03069 03070 if ( $this->isAnon() ) { 03071 // Nothing else to do... 03072 return; 03073 } 03074 03075 // Only update the timestamp if the page is being watched. 03076 // The query to find out if it is watched is cached both in memcached and per-invocation, 03077 // and when it does have to be executed, it can be on a slave 03078 // If this is the user's newtalk page, we always update the timestamp 03079 $force = ''; 03080 if ( $title->getNamespace() == NS_USER_TALK && 03081 $title->getText() == $this->getName() ) 03082 { 03083 $force = 'force'; 03084 } 03085 03086 $this->getWatchedItem( $title )->resetNotificationTimestamp( $force ); 03087 } 03088 03095 public function clearAllNotifications() { 03096 if ( wfReadOnly() ) { 03097 return; 03098 } 03099 03100 // Do nothing if not allowed to edit the watchlist 03101 if ( !$this->isAllowed( 'editmywatchlist' ) ) { 03102 return; 03103 } 03104 03105 global $wgUseEnotif, $wgShowUpdatedMarker; 03106 if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) { 03107 $this->setNewtalk( false ); 03108 return; 03109 } 03110 $id = $this->getId(); 03111 if ( $id != 0 ) { 03112 $dbw = wfGetDB( DB_MASTER ); 03113 $dbw->update( 'watchlist', 03114 array( /* SET */ 03115 'wl_notificationtimestamp' => null 03116 ), array( /* WHERE */ 03117 'wl_user' => $id 03118 ), __METHOD__ 03119 ); 03120 # We also need to clear here the "you have new message" notification for the own user_talk page 03121 # This is cleared one page view later in Article::viewUpdates(); 03122 } 03123 } 03124 03131 private function decodeOptions( $str ) { 03132 wfDeprecated( __METHOD__, '1.19' ); 03133 if ( !$str ) { 03134 return; 03135 } 03136 03137 $this->mOptionsLoaded = true; 03138 $this->mOptionOverrides = array(); 03139 03140 // If an option is not set in $str, use the default value 03141 $this->mOptions = self::getDefaultOptions(); 03142 03143 $a = explode( "\n", $str ); 03144 foreach ( $a as $s ) { 03145 $m = array(); 03146 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) { 03147 $this->mOptions[$m[1]] = $m[2]; 03148 $this->mOptionOverrides[$m[1]] = $m[2]; 03149 } 03150 } 03151 } 03152 03166 protected function setCookie( $name, $value, $exp = 0, $secure = null, $params = array() ) { 03167 $params['secure'] = $secure; 03168 $this->getRequest()->response()->setcookie( $name, $value, $exp, $params ); 03169 } 03170 03180 protected function clearCookie( $name, $secure = null, $params = array() ) { 03181 $this->setCookie( $name, '', time() - 86400, $secure, $params ); 03182 } 03183 03191 public function setCookies( $request = null, $secure = null ) { 03192 if ( $request === null ) { 03193 $request = $this->getRequest(); 03194 } 03195 03196 $this->load(); 03197 if ( 0 == $this->mId ) { 03198 return; 03199 } 03200 if ( !$this->mToken ) { 03201 // When token is empty or NULL generate a new one and then save it to the database 03202 // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey 03203 // Simply by setting every cell in the user_token column to NULL and letting them be 03204 // regenerated as users log back into the wiki. 03205 $this->setToken(); 03206 $this->saveSettings(); 03207 } 03208 $session = array( 03209 'wsUserID' => $this->mId, 03210 'wsToken' => $this->mToken, 03211 'wsUserName' => $this->getName() 03212 ); 03213 $cookies = array( 03214 'UserID' => $this->mId, 03215 'UserName' => $this->getName(), 03216 ); 03217 if ( 1 == $this->getOption( 'rememberpassword' ) ) { 03218 $cookies['Token'] = $this->mToken; 03219 } else { 03220 $cookies['Token'] = false; 03221 } 03222 03223 wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) ); 03224 03225 foreach ( $session as $name => $value ) { 03226 $request->setSessionData( $name, $value ); 03227 } 03228 foreach ( $cookies as $name => $value ) { 03229 if ( $value === false ) { 03230 $this->clearCookie( $name ); 03231 } else { 03232 $this->setCookie( $name, $value, 0, $secure ); 03233 } 03234 } 03235 03243 if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) { 03244 $time = null; 03245 if ( ( 1 == $this->getOption( 'rememberpassword' ) ) ) { 03246 $time = 0; // set to $wgCookieExpiration 03247 } 03248 $this->setCookie( 03249 'forceHTTPS', 03250 'true', 03251 $time, 03252 false, 03253 array( 'prefix' => '' ) // no prefix 03254 ); 03255 } 03256 } 03257 03261 public function logout() { 03262 if ( wfRunHooks( 'UserLogout', array( &$this ) ) ) { 03263 $this->doLogout(); 03264 } 03265 } 03266 03271 public function doLogout() { 03272 $this->clearInstanceCache( 'defaults' ); 03273 03274 $this->getRequest()->setSessionData( 'wsUserID', 0 ); 03275 03276 $this->clearCookie( 'UserID' ); 03277 $this->clearCookie( 'Token' ); 03278 $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) ); 03279 03280 // Remember when user logged out, to prevent seeing cached pages 03281 $this->setCookie( 'LoggedOut', time(), time() + 86400 ); 03282 } 03283 03288 public function saveSettings() { 03289 global $wgAuth; 03290 03291 $this->load(); 03292 if ( wfReadOnly() ) { 03293 return; 03294 } 03295 if ( 0 == $this->mId ) { 03296 return; 03297 } 03298 03299 $this->mTouched = self::newTouchedTimestamp(); 03300 if ( !$wgAuth->allowSetLocalPassword() ) { 03301 $this->mPassword = ''; 03302 } 03303 03304 $dbw = wfGetDB( DB_MASTER ); 03305 $dbw->update( 'user', 03306 array( /* SET */ 03307 'user_name' => $this->mName, 03308 'user_password' => $this->mPassword, 03309 'user_newpassword' => $this->mNewpassword, 03310 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 03311 'user_real_name' => $this->mRealName, 03312 'user_email' => $this->mEmail, 03313 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 03314 'user_touched' => $dbw->timestamp( $this->mTouched ), 03315 'user_token' => strval( $this->mToken ), 03316 'user_email_token' => $this->mEmailToken, 03317 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), 03318 ), array( /* WHERE */ 03319 'user_id' => $this->mId 03320 ), __METHOD__ 03321 ); 03322 03323 $this->saveOptions(); 03324 03325 wfRunHooks( 'UserSaveSettings', array( $this ) ); 03326 $this->clearSharedCache(); 03327 $this->getUserPage()->invalidateCache(); 03328 } 03329 03334 public function idForName() { 03335 $s = trim( $this->getName() ); 03336 if ( $s === '' ) { 03337 return 0; 03338 } 03339 03340 $dbr = wfGetDB( DB_SLAVE ); 03341 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ ); 03342 if ( $id === false ) { 03343 $id = 0; 03344 } 03345 return $id; 03346 } 03347 03364 public static function createNew( $name, $params = array() ) { 03365 $user = new User; 03366 $user->load(); 03367 $user->setToken(); // init token 03368 if ( isset( $params['options'] ) ) { 03369 $user->mOptions = $params['options'] + (array)$user->mOptions; 03370 unset( $params['options'] ); 03371 } 03372 $dbw = wfGetDB( DB_MASTER ); 03373 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); 03374 03375 $fields = array( 03376 'user_id' => $seqVal, 03377 'user_name' => $name, 03378 'user_password' => $user->mPassword, 03379 'user_newpassword' => $user->mNewpassword, 03380 'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ), 03381 'user_email' => $user->mEmail, 03382 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), 03383 'user_real_name' => $user->mRealName, 03384 'user_token' => strval( $user->mToken ), 03385 'user_registration' => $dbw->timestamp( $user->mRegistration ), 03386 'user_editcount' => 0, 03387 'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ), 03388 ); 03389 foreach ( $params as $name => $value ) { 03390 $fields["user_$name"] = $value; 03391 } 03392 $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) ); 03393 if ( $dbw->affectedRows() ) { 03394 $newUser = User::newFromId( $dbw->insertId() ); 03395 } else { 03396 $newUser = null; 03397 } 03398 return $newUser; 03399 } 03400 03427 public function addToDatabase() { 03428 $this->load(); 03429 if ( !$this->mToken ) { 03430 $this->setToken(); // init token 03431 } 03432 03433 $this->mTouched = self::newTouchedTimestamp(); 03434 03435 $dbw = wfGetDB( DB_MASTER ); 03436 $inWrite = $dbw->writesOrCallbacksPending(); 03437 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' ); 03438 $dbw->insert( 'user', 03439 array( 03440 'user_id' => $seqVal, 03441 'user_name' => $this->mName, 03442 'user_password' => $this->mPassword, 03443 'user_newpassword' => $this->mNewpassword, 03444 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ), 03445 'user_email' => $this->mEmail, 03446 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), 03447 'user_real_name' => $this->mRealName, 03448 'user_token' => strval( $this->mToken ), 03449 'user_registration' => $dbw->timestamp( $this->mRegistration ), 03450 'user_editcount' => 0, 03451 'user_touched' => $dbw->timestamp( $this->mTouched ), 03452 ), __METHOD__, 03453 array( 'IGNORE' ) 03454 ); 03455 if ( !$dbw->affectedRows() ) { 03456 if ( !$inWrite ) { 03457 // XXX: Get out of REPEATABLE-READ so the SELECT below works. 03458 // Often this case happens early in views before any writes. 03459 // This shows up at least with CentralAuth. 03460 $dbw->commit( __METHOD__, 'flush' ); 03461 } 03462 $this->mId = $dbw->selectField( 'user', 'user_id', 03463 array( 'user_name' => $this->mName ), __METHOD__ ); 03464 $loaded = false; 03465 if ( $this->mId ) { 03466 if ( $this->loadFromDatabase() ) { 03467 $loaded = true; 03468 } 03469 } 03470 if ( !$loaded ) { 03471 throw new MWException( __METHOD__ . ": hit a key conflict attempting " . 03472 "to insert user '{$this->mName}' row, but it was not present in select!" ); 03473 } 03474 return Status::newFatal( 'userexists' ); 03475 } 03476 $this->mId = $dbw->insertId(); 03477 03478 // Clear instance cache other than user table data, which is already accurate 03479 $this->clearInstanceCache(); 03480 03481 $this->saveOptions(); 03482 return Status::newGood(); 03483 } 03484 03490 public function spreadAnyEditBlock() { 03491 if ( $this->isLoggedIn() && $this->isBlocked() ) { 03492 return $this->spreadBlock(); 03493 } 03494 return false; 03495 } 03496 03502 protected function spreadBlock() { 03503 wfDebug( __METHOD__ . "()\n" ); 03504 $this->load(); 03505 if ( $this->mId == 0 ) { 03506 return false; 03507 } 03508 03509 $userblock = Block::newFromTarget( $this->getName() ); 03510 if ( !$userblock ) { 03511 return false; 03512 } 03513 03514 return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() ); 03515 } 03516 03531 public function getPageRenderingHash() { 03532 wfDeprecated( __METHOD__, '1.17' ); 03533 03534 global $wgRenderHashAppend, $wgLang, $wgContLang; 03535 if ( $this->mHash ) { 03536 return $this->mHash; 03537 } 03538 03539 // stubthreshold is only included below for completeness, 03540 // since it disables the parser cache, its value will always 03541 // be 0 when this function is called by parsercache. 03542 03543 $confstr = $this->getOption( 'math' ); 03544 $confstr .= '!' . $this->getStubThreshold(); 03545 $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' ); 03546 $confstr .= '!' . $wgLang->getCode(); 03547 $confstr .= '!' . $this->getOption( 'thumbsize' ); 03548 // add in language specific options, if any 03549 $extra = $wgContLang->getExtraHashOptions(); 03550 $confstr .= $extra; 03551 03552 // Since the skin could be overloading link(), it should be 03553 // included here but in practice, none of our skins do that. 03554 03555 $confstr .= $wgRenderHashAppend; 03556 03557 // Give a chance for extensions to modify the hash, if they have 03558 // extra options or other effects on the parser cache. 03559 wfRunHooks( 'PageRenderingHash', array( &$confstr ) ); 03560 03561 // Make it a valid memcached key fragment 03562 $confstr = str_replace( ' ', '_', $confstr ); 03563 $this->mHash = $confstr; 03564 return $confstr; 03565 } 03566 03571 public function isBlockedFromCreateAccount() { 03572 $this->getBlockedStatus(); 03573 if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) { 03574 return $this->mBlock; 03575 } 03576 03577 # bug 13611: if the IP address the user is trying to create an account from is 03578 # blocked with createaccount disabled, prevent new account creation there even 03579 # when the user is logged in 03580 if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) { 03581 $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() ); 03582 } 03583 return $this->mBlockedFromCreateAccount instanceof Block && $this->mBlockedFromCreateAccount->prevents( 'createaccount' ) 03584 ? $this->mBlockedFromCreateAccount 03585 : false; 03586 } 03587 03592 public function isBlockedFromEmailuser() { 03593 $this->getBlockedStatus(); 03594 return $this->mBlock && $this->mBlock->prevents( 'sendemail' ); 03595 } 03596 03601 function isAllowedToCreateAccount() { 03602 return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); 03603 } 03604 03610 public function getUserPage() { 03611 return Title::makeTitle( NS_USER, $this->getName() ); 03612 } 03613 03619 public function getTalkPage() { 03620 $title = $this->getUserPage(); 03621 return $title->getTalkPage(); 03622 } 03623 03629 public function isNewbie() { 03630 return !$this->isAllowed( 'autoconfirmed' ); 03631 } 03632 03638 public function checkPassword( $password ) { 03639 global $wgAuth, $wgLegacyEncoding; 03640 $this->load(); 03641 03642 // Even though we stop people from creating passwords that 03643 // are shorter than this, doesn't mean people wont be able 03644 // to. Certain authentication plugins do NOT want to save 03645 // domain passwords in a mysql database, so we should 03646 // check this (in case $wgAuth->strict() is false). 03647 if ( !$this->isValidPassword( $password ) ) { 03648 return false; 03649 } 03650 03651 if ( $wgAuth->authenticate( $this->getName(), $password ) ) { 03652 return true; 03653 } elseif ( $wgAuth->strict() ) { 03654 // Auth plugin doesn't allow local authentication 03655 return false; 03656 } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) { 03657 // Auth plugin doesn't allow local authentication for this user name 03658 return false; 03659 } 03660 if ( self::comparePasswords( $this->mPassword, $password, $this->mId ) ) { 03661 return true; 03662 } elseif ( $wgLegacyEncoding ) { 03663 // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted 03664 // Check for this with iconv 03665 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password ); 03666 if ( $cp1252Password != $password && 03667 self::comparePasswords( $this->mPassword, $cp1252Password, $this->mId ) ) 03668 { 03669 return true; 03670 } 03671 } 03672 return false; 03673 } 03674 03683 public function checkTemporaryPassword( $plaintext ) { 03684 global $wgNewPasswordExpiry; 03685 03686 $this->load(); 03687 if ( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) { 03688 if ( is_null( $this->mNewpassTime ) ) { 03689 return true; 03690 } 03691 $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry; 03692 return ( time() < $expiry ); 03693 } else { 03694 return false; 03695 } 03696 } 03697 03706 public function editToken( $salt = '', $request = null ) { 03707 wfDeprecated( __METHOD__, '1.19' ); 03708 return $this->getEditToken( $salt, $request ); 03709 } 03710 03723 public function getEditToken( $salt = '', $request = null ) { 03724 if ( $request == null ) { 03725 $request = $this->getRequest(); 03726 } 03727 03728 if ( $this->isAnon() ) { 03729 return EDIT_TOKEN_SUFFIX; 03730 } else { 03731 $token = $request->getSessionData( 'wsEditToken' ); 03732 if ( $token === null ) { 03733 $token = MWCryptRand::generateHex( 32 ); 03734 $request->setSessionData( 'wsEditToken', $token ); 03735 } 03736 if ( is_array( $salt ) ) { 03737 $salt = implode( '|', $salt ); 03738 } 03739 return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX; 03740 } 03741 } 03742 03749 public static function generateToken() { 03750 return MWCryptRand::generateHex( 32 ); 03751 } 03752 03764 public function matchEditToken( $val, $salt = '', $request = null ) { 03765 $sessionToken = $this->getEditToken( $salt, $request ); 03766 if ( $val != $sessionToken ) { 03767 wfDebug( "User::matchEditToken: broken session data\n" ); 03768 } 03769 return $val == $sessionToken; 03770 } 03771 03781 public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) { 03782 $sessionToken = $this->getEditToken( $salt, $request ); 03783 return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 ); 03784 } 03785 03793 public function sendConfirmationMail( $type = 'created' ) { 03794 global $wgLang; 03795 $expiration = null; // gets passed-by-ref and defined in next line. 03796 $token = $this->confirmationToken( $expiration ); 03797 $url = $this->confirmationTokenUrl( $token ); 03798 $invalidateURL = $this->invalidationTokenUrl( $token ); 03799 $this->saveSettings(); 03800 03801 if ( $type == 'created' || $type === false ) { 03802 $message = 'confirmemail_body'; 03803 } elseif ( $type === true ) { 03804 $message = 'confirmemail_body_changed'; 03805 } else { 03806 // Messages: confirmemail_body_changed, confirmemail_body_set 03807 $message = 'confirmemail_body_' . $type; 03808 } 03809 03810 return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(), 03811 wfMessage( $message, 03812 $this->getRequest()->getIP(), 03813 $this->getName(), 03814 $url, 03815 $wgLang->timeanddate( $expiration, false ), 03816 $invalidateURL, 03817 $wgLang->date( $expiration, false ), 03818 $wgLang->time( $expiration, false ) )->text() ); 03819 } 03820 03831 public function sendMail( $subject, $body, $from = null, $replyto = null ) { 03832 if ( is_null( $from ) ) { 03833 global $wgPasswordSender, $wgPasswordSenderName; 03834 $sender = new MailAddress( $wgPasswordSender, $wgPasswordSenderName ); 03835 } else { 03836 $sender = new MailAddress( $from ); 03837 } 03838 03839 $to = new MailAddress( $this ); 03840 return UserMailer::send( $to, $sender, $subject, $body, $replyto ); 03841 } 03842 03853 protected function confirmationToken( &$expiration ) { 03854 global $wgUserEmailConfirmationTokenExpiry; 03855 $now = time(); 03856 $expires = $now + $wgUserEmailConfirmationTokenExpiry; 03857 $expiration = wfTimestamp( TS_MW, $expires ); 03858 $this->load(); 03859 $token = MWCryptRand::generateHex( 32 ); 03860 $hash = md5( $token ); 03861 $this->mEmailToken = $hash; 03862 $this->mEmailTokenExpires = $expiration; 03863 return $token; 03864 } 03865 03871 protected function confirmationTokenUrl( $token ) { 03872 return $this->getTokenUrl( 'ConfirmEmail', $token ); 03873 } 03874 03880 protected function invalidationTokenUrl( $token ) { 03881 return $this->getTokenUrl( 'InvalidateEmail', $token ); 03882 } 03883 03898 protected function getTokenUrl( $page, $token ) { 03899 // Hack to bypass localization of 'Special:' 03900 $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" ); 03901 return $title->getCanonicalURL(); 03902 } 03903 03911 public function confirmEmail() { 03912 // Check if it's already confirmed, so we don't touch the database 03913 // and fire the ConfirmEmailComplete hook on redundant confirmations. 03914 if ( !$this->isEmailConfirmed() ) { 03915 $this->setEmailAuthenticationTimestamp( wfTimestampNow() ); 03916 wfRunHooks( 'ConfirmEmailComplete', array( $this ) ); 03917 } 03918 return true; 03919 } 03920 03928 function invalidateEmail() { 03929 $this->load(); 03930 $this->mEmailToken = null; 03931 $this->mEmailTokenExpires = null; 03932 $this->setEmailAuthenticationTimestamp( null ); 03933 wfRunHooks( 'InvalidateEmailComplete', array( $this ) ); 03934 return true; 03935 } 03936 03941 function setEmailAuthenticationTimestamp( $timestamp ) { 03942 $this->load(); 03943 $this->mEmailAuthenticated = $timestamp; 03944 wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) ); 03945 } 03946 03952 public function canSendEmail() { 03953 global $wgEnableEmail, $wgEnableUserEmail; 03954 if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) { 03955 return false; 03956 } 03957 $canSend = $this->isEmailConfirmed(); 03958 wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) ); 03959 return $canSend; 03960 } 03961 03967 public function canReceiveEmail() { 03968 return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' ); 03969 } 03970 03981 public function isEmailConfirmed() { 03982 global $wgEmailAuthentication; 03983 $this->load(); 03984 $confirmed = true; 03985 if ( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) { 03986 if ( $this->isAnon() ) { 03987 return false; 03988 } 03989 if ( !Sanitizer::validateEmail( $this->mEmail ) ) { 03990 return false; 03991 } 03992 if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) { 03993 return false; 03994 } 03995 return true; 03996 } else { 03997 return $confirmed; 03998 } 03999 } 04000 04005 public function isEmailConfirmationPending() { 04006 global $wgEmailAuthentication; 04007 return $wgEmailAuthentication && 04008 !$this->isEmailConfirmed() && 04009 $this->mEmailToken && 04010 $this->mEmailTokenExpires > wfTimestamp(); 04011 } 04012 04020 public function getRegistration() { 04021 if ( $this->isAnon() ) { 04022 return false; 04023 } 04024 $this->load(); 04025 return $this->mRegistration; 04026 } 04027 04034 public function getFirstEditTimestamp() { 04035 if ( $this->getId() == 0 ) { 04036 return false; // anons 04037 } 04038 $dbr = wfGetDB( DB_SLAVE ); 04039 $time = $dbr->selectField( 'revision', 'rev_timestamp', 04040 array( 'rev_user' => $this->getId() ), 04041 __METHOD__, 04042 array( 'ORDER BY' => 'rev_timestamp ASC' ) 04043 ); 04044 if ( !$time ) { 04045 return false; // no edits 04046 } 04047 return wfTimestamp( TS_MW, $time ); 04048 } 04049 04056 public static function getGroupPermissions( $groups ) { 04057 global $wgGroupPermissions, $wgRevokePermissions; 04058 $rights = array(); 04059 // grant every granted permission first 04060 foreach ( $groups as $group ) { 04061 if ( isset( $wgGroupPermissions[$group] ) ) { 04062 $rights = array_merge( $rights, 04063 // array_filter removes empty items 04064 array_keys( array_filter( $wgGroupPermissions[$group] ) ) ); 04065 } 04066 } 04067 // now revoke the revoked permissions 04068 foreach ( $groups as $group ) { 04069 if ( isset( $wgRevokePermissions[$group] ) ) { 04070 $rights = array_diff( $rights, 04071 array_keys( array_filter( $wgRevokePermissions[$group] ) ) ); 04072 } 04073 } 04074 return array_unique( $rights ); 04075 } 04076 04083 public static function getGroupsWithPermission( $role ) { 04084 global $wgGroupPermissions; 04085 $allowedGroups = array(); 04086 foreach ( array_keys( $wgGroupPermissions ) as $group ) { 04087 if ( self::groupHasPermission( $group, $role ) ) { 04088 $allowedGroups[] = $group; 04089 } 04090 } 04091 return $allowedGroups; 04092 } 04093 04106 public static function groupHasPermission( $group, $role ) { 04107 global $wgGroupPermissions, $wgRevokePermissions; 04108 return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role] 04109 && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] ); 04110 } 04111 04119 public static function isEveryoneAllowed( $right ) { 04120 global $wgGroupPermissions, $wgRevokePermissions; 04121 static $cache = array(); 04122 04123 // Use the cached results, except in unit tests which rely on 04124 // being able change the permission mid-request 04125 if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) { 04126 return $cache[$right]; 04127 } 04128 04129 if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) { 04130 $cache[$right] = false; 04131 return false; 04132 } 04133 04134 // If it's revoked anywhere, then everyone doesn't have it 04135 foreach ( $wgRevokePermissions as $rights ) { 04136 if ( isset( $rights[$right] ) && $rights[$right] ) { 04137 $cache[$right] = false; 04138 return false; 04139 } 04140 } 04141 04142 // Allow extensions (e.g. OAuth) to say false 04143 if ( !wfRunHooks( 'UserIsEveryoneAllowed', array( $right ) ) ) { 04144 $cache[$right] = false; 04145 return false; 04146 } 04147 04148 $cache[$right] = true; 04149 return true; 04150 } 04151 04158 public static function getGroupName( $group ) { 04159 $msg = wfMessage( "group-$group" ); 04160 return $msg->isBlank() ? $group : $msg->text(); 04161 } 04162 04170 public static function getGroupMember( $group, $username = '#' ) { 04171 $msg = wfMessage( "group-$group-member", $username ); 04172 return $msg->isBlank() ? $group : $msg->text(); 04173 } 04174 04181 public static function getAllGroups() { 04182 global $wgGroupPermissions, $wgRevokePermissions; 04183 return array_diff( 04184 array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ), 04185 self::getImplicitGroups() 04186 ); 04187 } 04188 04193 public static function getAllRights() { 04194 if ( self::$mAllRights === false ) { 04195 global $wgAvailableRights; 04196 if ( count( $wgAvailableRights ) ) { 04197 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) ); 04198 } else { 04199 self::$mAllRights = self::$mCoreRights; 04200 } 04201 wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) ); 04202 } 04203 return self::$mAllRights; 04204 } 04205 04210 public static function getImplicitGroups() { 04211 global $wgImplicitGroups; 04212 $groups = $wgImplicitGroups; 04213 wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); #deprecated, use $wgImplictGroups instead 04214 return $groups; 04215 } 04216 04223 public static function getGroupPage( $group ) { 04224 $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage(); 04225 if ( $msg->exists() ) { 04226 $title = Title::newFromText( $msg->text() ); 04227 if ( is_object( $title ) ) { 04228 return $title; 04229 } 04230 } 04231 return false; 04232 } 04233 04242 public static function makeGroupLinkHTML( $group, $text = '' ) { 04243 if ( $text == '' ) { 04244 $text = self::getGroupName( $group ); 04245 } 04246 $title = self::getGroupPage( $group ); 04247 if ( $title ) { 04248 return Linker::link( $title, htmlspecialchars( $text ) ); 04249 } else { 04250 return $text; 04251 } 04252 } 04253 04262 public static function makeGroupLinkWiki( $group, $text = '' ) { 04263 if ( $text == '' ) { 04264 $text = self::getGroupName( $group ); 04265 } 04266 $title = self::getGroupPage( $group ); 04267 if ( $title ) { 04268 $page = $title->getPrefixedText(); 04269 return "[[$page|$text]]"; 04270 } else { 04271 return $text; 04272 } 04273 } 04274 04284 public static function changeableByGroup( $group ) { 04285 global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; 04286 04287 $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); 04288 if ( empty( $wgAddGroups[$group] ) ) { 04289 // Don't add anything to $groups 04290 } elseif ( $wgAddGroups[$group] === true ) { 04291 // You get everything 04292 $groups['add'] = self::getAllGroups(); 04293 } elseif ( is_array( $wgAddGroups[$group] ) ) { 04294 $groups['add'] = $wgAddGroups[$group]; 04295 } 04296 04297 // Same thing for remove 04298 if ( empty( $wgRemoveGroups[$group] ) ) { 04299 } elseif ( $wgRemoveGroups[$group] === true ) { 04300 $groups['remove'] = self::getAllGroups(); 04301 } elseif ( is_array( $wgRemoveGroups[$group] ) ) { 04302 $groups['remove'] = $wgRemoveGroups[$group]; 04303 } 04304 04305 // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility 04306 if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) { 04307 foreach ( $wgGroupsAddToSelf as $key => $value ) { 04308 if ( is_int( $key ) ) { 04309 $wgGroupsAddToSelf['user'][] = $value; 04310 } 04311 } 04312 } 04313 04314 if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) { 04315 foreach ( $wgGroupsRemoveFromSelf as $key => $value ) { 04316 if ( is_int( $key ) ) { 04317 $wgGroupsRemoveFromSelf['user'][] = $value; 04318 } 04319 } 04320 } 04321 04322 // Now figure out what groups the user can add to him/herself 04323 if ( empty( $wgGroupsAddToSelf[$group] ) ) { 04324 } elseif ( $wgGroupsAddToSelf[$group] === true ) { 04325 // No idea WHY this would be used, but it's there 04326 $groups['add-self'] = User::getAllGroups(); 04327 } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) { 04328 $groups['add-self'] = $wgGroupsAddToSelf[$group]; 04329 } 04330 04331 if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) { 04332 } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) { 04333 $groups['remove-self'] = User::getAllGroups(); 04334 } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) { 04335 $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; 04336 } 04337 04338 return $groups; 04339 } 04340 04348 public function changeableGroups() { 04349 if ( $this->isAllowed( 'userrights' ) ) { 04350 // This group gives the right to modify everything (reverse- 04351 // compatibility with old "userrights lets you change 04352 // everything") 04353 // Using array_merge to make the groups reindexed 04354 $all = array_merge( User::getAllGroups() ); 04355 return array( 04356 'add' => $all, 04357 'remove' => $all, 04358 'add-self' => array(), 04359 'remove-self' => array() 04360 ); 04361 } 04362 04363 // Okay, it's not so simple, we will have to go through the arrays 04364 $groups = array( 04365 'add' => array(), 04366 'remove' => array(), 04367 'add-self' => array(), 04368 'remove-self' => array() 04369 ); 04370 $addergroups = $this->getEffectiveGroups(); 04371 04372 foreach ( $addergroups as $addergroup ) { 04373 $groups = array_merge_recursive( 04374 $groups, $this->changeableByGroup( $addergroup ) 04375 ); 04376 $groups['add'] = array_unique( $groups['add'] ); 04377 $groups['remove'] = array_unique( $groups['remove'] ); 04378 $groups['add-self'] = array_unique( $groups['add-self'] ); 04379 $groups['remove-self'] = array_unique( $groups['remove-self'] ); 04380 } 04381 return $groups; 04382 } 04383 04388 public function incEditCount() { 04389 if ( !$this->isAnon() ) { 04390 $dbw = wfGetDB( DB_MASTER ); 04391 $dbw->update( 04392 'user', 04393 array( 'user_editcount=user_editcount+1' ), 04394 array( 'user_id' => $this->getId() ), 04395 __METHOD__ 04396 ); 04397 04398 // Lazy initialization check... 04399 if ( $dbw->affectedRows() == 0 ) { 04400 // Now here's a goddamn hack... 04401 $dbr = wfGetDB( DB_SLAVE ); 04402 if ( $dbr !== $dbw ) { 04403 // If we actually have a slave server, the count is 04404 // at least one behind because the current transaction 04405 // has not been committed and replicated. 04406 $this->initEditCount( 1 ); 04407 } else { 04408 // But if DB_SLAVE is selecting the master, then the 04409 // count we just read includes the revision that was 04410 // just added in the working transaction. 04411 $this->initEditCount(); 04412 } 04413 } 04414 } 04415 // edit count in user cache too 04416 $this->invalidateCache(); 04417 } 04418 04425 protected function initEditCount( $add = 0 ) { 04426 // Pull from a slave to be less cruel to servers 04427 // Accuracy isn't the point anyway here 04428 $dbr = wfGetDB( DB_SLAVE ); 04429 $count = (int)$dbr->selectField( 04430 'revision', 04431 'COUNT(rev_user)', 04432 array( 'rev_user' => $this->getId() ), 04433 __METHOD__ 04434 ); 04435 $count = $count + $add; 04436 04437 $dbw = wfGetDB( DB_MASTER ); 04438 $dbw->update( 04439 'user', 04440 array( 'user_editcount' => $count ), 04441 array( 'user_id' => $this->getId() ), 04442 __METHOD__ 04443 ); 04444 04445 return $count; 04446 } 04447 04454 public static function getRightDescription( $right ) { 04455 $key = "right-$right"; 04456 $msg = wfMessage( $key ); 04457 return $msg->isBlank() ? $right : $msg->text(); 04458 } 04459 04467 public static function oldCrypt( $password, $userId ) { 04468 global $wgPasswordSalt; 04469 if ( $wgPasswordSalt ) { 04470 return md5( $userId . '-' . md5( $password ) ); 04471 } else { 04472 return md5( $password ); 04473 } 04474 } 04475 04484 public static function crypt( $password, $salt = false ) { 04485 global $wgPasswordSalt; 04486 04487 $hash = ''; 04488 if ( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) { 04489 return $hash; 04490 } 04491 04492 if ( $wgPasswordSalt ) { 04493 if ( $salt === false ) { 04494 $salt = MWCryptRand::generateHex( 8 ); 04495 } 04496 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); 04497 } else { 04498 return ':A:' . md5( $password ); 04499 } 04500 } 04501 04512 public static function comparePasswords( $hash, $password, $userId = false ) { 04513 $type = substr( $hash, 0, 3 ); 04514 04515 $result = false; 04516 if ( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) { 04517 return $result; 04518 } 04519 04520 if ( $type == ':A:' ) { 04521 // Unsalted 04522 return md5( $password ) === substr( $hash, 3 ); 04523 } elseif ( $type == ':B:' ) { 04524 // Salted 04525 list( $salt, $realHash ) = explode( ':', substr( $hash, 3 ), 2 ); 04526 return md5( $salt . '-' . md5( $password ) ) === $realHash; 04527 } else { 04528 // Old-style 04529 return self::oldCrypt( $password, $userId ) === $hash; 04530 } 04531 } 04532 04554 public function addNewUserLogEntry( $action = false, $reason = '' ) { 04555 global $wgUser, $wgNewUserLog; 04556 if ( empty( $wgNewUserLog ) ) { 04557 return true; // disabled 04558 } 04559 04560 if ( $action === true ) { 04561 $action = 'byemail'; 04562 } elseif ( $action === false ) { 04563 if ( $this->getName() == $wgUser->getName() ) { 04564 $action = 'create'; 04565 } else { 04566 $action = 'create2'; 04567 } 04568 } 04569 04570 if ( $action === 'create' || $action === 'autocreate' ) { 04571 $performer = $this; 04572 } else { 04573 $performer = $wgUser; 04574 } 04575 04576 $logEntry = new ManualLogEntry( 'newusers', $action ); 04577 $logEntry->setPerformer( $performer ); 04578 $logEntry->setTarget( $this->getUserPage() ); 04579 $logEntry->setComment( $reason ); 04580 $logEntry->setParameters( array( 04581 '4::userid' => $this->getId(), 04582 ) ); 04583 $logid = $logEntry->insert(); 04584 04585 if ( $action !== 'autocreate' ) { 04586 $logEntry->publish( $logid ); 04587 } 04588 04589 return (int)$logid; 04590 } 04591 04599 public function addNewUserLogEntryAutoCreate() { 04600 $this->addNewUserLogEntry( 'autocreate' ); 04601 04602 return true; 04603 } 04604 04610 protected function loadOptions( $data = null ) { 04611 global $wgContLang; 04612 04613 $this->load(); 04614 04615 if ( $this->mOptionsLoaded ) { 04616 return; 04617 } 04618 04619 $this->mOptions = self::getDefaultOptions(); 04620 04621 if ( !$this->getId() ) { 04622 // For unlogged-in users, load language/variant options from request. 04623 // There's no need to do it for logged-in users: they can set preferences, 04624 // and handling of page content is done by $pageLang->getPreferredVariant() and such, 04625 // so don't override user's choice (especially when the user chooses site default). 04626 $variant = $wgContLang->getDefaultVariant(); 04627 $this->mOptions['variant'] = $variant; 04628 $this->mOptions['language'] = $variant; 04629 $this->mOptionsLoaded = true; 04630 return; 04631 } 04632 04633 // Maybe load from the object 04634 if ( !is_null( $this->mOptionOverrides ) ) { 04635 wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" ); 04636 foreach ( $this->mOptionOverrides as $key => $value ) { 04637 $this->mOptions[$key] = $value; 04638 } 04639 } else { 04640 if ( !is_array( $data ) ) { 04641 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" ); 04642 // Load from database 04643 $dbr = wfGetDB( DB_SLAVE ); 04644 04645 $res = $dbr->select( 04646 'user_properties', 04647 array( 'up_property', 'up_value' ), 04648 array( 'up_user' => $this->getId() ), 04649 __METHOD__ 04650 ); 04651 04652 $this->mOptionOverrides = array(); 04653 $data = array(); 04654 foreach ( $res as $row ) { 04655 $data[$row->up_property] = $row->up_value; 04656 } 04657 } 04658 foreach ( $data as $property => $value ) { 04659 $this->mOptionOverrides[$property] = $value; 04660 $this->mOptions[$property] = $value; 04661 } 04662 } 04663 04664 $this->mOptionsLoaded = true; 04665 04666 wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) ); 04667 } 04668 04672 protected function saveOptions() { 04673 $this->loadOptions(); 04674 04675 // Not using getOptions(), to keep hidden preferences in database 04676 $saveOptions = $this->mOptions; 04677 04678 // Allow hooks to abort, for instance to save to a global profile. 04679 // Reset options to default state before saving. 04680 if ( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) { 04681 return; 04682 } 04683 04684 $userId = $this->getId(); 04685 $insert_rows = array(); 04686 foreach ( $saveOptions as $key => $value ) { 04687 // Don't bother storing default values 04688 $defaultOption = self::getDefaultOption( $key ); 04689 if ( ( is_null( $defaultOption ) && 04690 !( $value === false || is_null( $value ) ) ) || 04691 $value != $defaultOption ) { 04692 $insert_rows[] = array( 04693 'up_user' => $userId, 04694 'up_property' => $key, 04695 'up_value' => $value, 04696 ); 04697 } 04698 } 04699 04700 $dbw = wfGetDB( DB_MASTER ); 04701 $hasRows = $dbw->selectField( 'user_properties', '1', 04702 array( 'up_user' => $userId ), __METHOD__ ); 04703 04704 if ( $hasRows ) { 04705 // Only do this delete if there is something there. A very large portion of 04706 // calls to this function are for setting 'rememberpassword' for new accounts. 04707 // Doing this delete for new accounts with no rows in the table rougly causes 04708 // gap locks on [max user ID,+infinity) which causes high contention since many 04709 // updates will pile up on each other since they are for higher (newer) user IDs. 04710 $dbw->delete( 'user_properties', array( 'up_user' => $userId ), __METHOD__ ); 04711 } 04712 $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) ); 04713 } 04714 04738 public static function passwordChangeInputAttribs() { 04739 global $wgMinimalPasswordLength; 04740 04741 if ( $wgMinimalPasswordLength == 0 ) { 04742 return array(); 04743 } 04744 04745 # Note that the pattern requirement will always be satisfied if the 04746 # input is empty, so we need required in all cases. 04747 # 04748 # @todo FIXME: Bug 23769: This needs to not claim the password is required 04749 # if e-mail confirmation is being used. Since HTML5 input validation 04750 # is b0rked anyway in some browsers, just return nothing. When it's 04751 # re-enabled, fix this code to not output required for e-mail 04752 # registration. 04753 #$ret = array( 'required' ); 04754 $ret = array(); 04755 04756 # We can't actually do this right now, because Opera 9.6 will print out 04757 # the entered password visibly in its error message! When other 04758 # browsers add support for this attribute, or Opera fixes its support, 04759 # we can add support with a version check to avoid doing this on Opera 04760 # versions where it will be a problem. Reported to Opera as 04761 # DSK-262266, but they don't have a public bug tracker for us to follow. 04762 /* 04763 if ( $wgMinimalPasswordLength > 1 ) { 04764 $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}'; 04765 $ret['title'] = wfMessage( 'passwordtooshort' ) 04766 ->numParams( $wgMinimalPasswordLength )->text(); 04767 } 04768 */ 04769 04770 return $ret; 04771 } 04772 04778 public static function selectFields() { 04779 return array( 04780 'user_id', 04781 'user_name', 04782 'user_real_name', 04783 'user_password', 04784 'user_newpassword', 04785 'user_newpass_time', 04786 'user_email', 04787 'user_touched', 04788 'user_token', 04789 'user_email_authenticated', 04790 'user_email_token', 04791 'user_email_token_expires', 04792 'user_registration', 04793 'user_editcount', 04794 ); 04795 } 04796 04804 static function newFatalPermissionDeniedStatus( $permission ) { 04805 global $wgLang; 04806 04807 $groups = array_map( 04808 array( 'User', 'makeGroupLinkWiki' ), 04809 User::getGroupsWithPermission( $permission ) 04810 ); 04811 04812 if ( $groups ) { 04813 return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) ); 04814 } else { 04815 return Status::newFatal( 'badaccess-group0' ); 04816 } 04817 } 04818 }