MediaWiki  REL1_22
User.php
Go to the documentation of this file.
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 }