MediaWiki  REL1_24
User.php
Go to the documentation of this file.
00001 <?php
00027 define( 'EDIT_TOKEN_SUFFIX', '+\\' );
00028 
00039 class User implements IDBAccessObject {
00043     const TOKEN_LENGTH = 32;
00044 
00049     const EDIT_TOKEN_SUFFIX = EDIT_TOKEN_SUFFIX;
00050 
00054     const VERSION = 10;
00055 
00059     const MAX_WATCHED_ITEMS_CACHE = 100;
00060 
00064     private static $mPasswordFactory = null;
00065 
00072     protected static $mCacheVars = array(
00073         // user table
00074         'mId',
00075         'mName',
00076         'mRealName',
00077         'mEmail',
00078         'mTouched',
00079         'mToken',
00080         'mEmailAuthenticated',
00081         'mEmailToken',
00082         'mEmailTokenExpires',
00083         'mRegistration',
00084         'mEditCount',
00085         // user_groups table
00086         'mGroups',
00087         // user_properties table
00088         'mOptionOverrides',
00089     );
00090 
00097     protected static $mCoreRights = array(
00098         'apihighlimits',
00099         'autoconfirmed',
00100         'autopatrol',
00101         'bigdelete',
00102         'block',
00103         'blockemail',
00104         'bot',
00105         'browsearchive',
00106         'createaccount',
00107         'createpage',
00108         'createtalk',
00109         'delete',
00110         'deletedhistory',
00111         'deletedtext',
00112         'deletelogentry',
00113         'deleterevision',
00114         'edit',
00115         'editinterface',
00116         'editprotected',
00117         'editmyoptions',
00118         'editmyprivateinfo',
00119         'editmyusercss',
00120         'editmyuserjs',
00121         'editmywatchlist',
00122         'editsemiprotected',
00123         'editusercssjs', #deprecated
00124         'editusercss',
00125         'edituserjs',
00126         'hideuser',
00127         'import',
00128         'importupload',
00129         'ipblock-exempt',
00130         'markbotedits',
00131         'mergehistory',
00132         'minoredit',
00133         'move',
00134         'movefile',
00135         'move-categorypages',
00136         'move-rootuserpages',
00137         'move-subpages',
00138         'nominornewtalk',
00139         'noratelimit',
00140         'override-export-depth',
00141         'pagelang',
00142         'passwordreset',
00143         'patrol',
00144         'patrolmarks',
00145         'protect',
00146         'proxyunbannable',
00147         'purge',
00148         'read',
00149         'reupload',
00150         'reupload-own',
00151         'reupload-shared',
00152         'rollback',
00153         'sendemail',
00154         'siteadmin',
00155         'suppressionlog',
00156         'suppressredirect',
00157         'suppressrevision',
00158         'unblockself',
00159         'undelete',
00160         'unwatchedpages',
00161         'upload',
00162         'upload_by_url',
00163         'userrights',
00164         'userrights-interwiki',
00165         'viewmyprivateinfo',
00166         'viewmywatchlist',
00167         'viewsuppressed',
00168         'writeapi',
00169     );
00170 
00174     protected static $mAllRights = false;
00175 
00178     public $mId;
00179 
00180     public $mName;
00181 
00182     public $mRealName;
00183 
00188     public $mPassword;
00189 
00194     public $mNewpassword;
00195 
00196     public $mNewpassTime;
00197 
00198     public $mEmail;
00199 
00200     public $mTouched;
00201 
00202     protected $mToken;
00203 
00204     public $mEmailAuthenticated;
00205 
00206     protected $mEmailToken;
00207 
00208     protected $mEmailTokenExpires;
00209 
00210     protected $mRegistration;
00211 
00212     protected $mEditCount;
00213 
00214     public $mGroups;
00215 
00216     protected $mOptionOverrides;
00217 
00218     protected $mPasswordExpires;
00220 
00225     public $mOptionsLoaded;
00226 
00230     protected $mLoadedItems = array();
00232 
00242     public $mFrom;
00243 
00247     protected $mNewtalk;
00248 
00249     protected $mDatePreference;
00250 
00251     public $mBlockedby;
00252 
00253     protected $mHash;
00254 
00255     public $mRights;
00256 
00257     protected $mBlockreason;
00258 
00259     protected $mEffectiveGroups;
00260 
00261     protected $mImplicitGroups;
00262 
00263     protected $mFormerGroups;
00264 
00265     protected $mBlockedGlobally;
00266 
00267     protected $mLocked;
00268 
00269     public $mHideName;
00270 
00271     public $mOptions;
00272 
00276     private $mRequest;
00277 
00279     public $mBlock;
00280 
00282     protected $mAllowUsertalk;
00283 
00285     private $mBlockedFromCreateAccount = false;
00286 
00288     private $mWatchedItems = array();
00289 
00290     public static $idCacheByName = array();
00291 
00302     public function __construct() {
00303         $this->clearInstanceCache( 'defaults' );
00304     }
00305 
00309     public function __toString() {
00310         return $this->getName();
00311     }
00312 
00316     public function load() {
00317         if ( $this->mLoadedItems === true ) {
00318             return;
00319         }
00320         wfProfileIn( __METHOD__ );
00321 
00322         // Set it now to avoid infinite recursion in accessors
00323         $this->mLoadedItems = true;
00324 
00325         switch ( $this->mFrom ) {
00326             case 'defaults':
00327                 $this->loadDefaults();
00328                 break;
00329             case 'name':
00330                 $this->mId = self::idFromName( $this->mName );
00331                 if ( !$this->mId ) {
00332                     // Nonexistent user placeholder object
00333                     $this->loadDefaults( $this->mName );
00334                 } else {
00335                     $this->loadFromId();
00336                 }
00337                 break;
00338             case 'id':
00339                 $this->loadFromId();
00340                 break;
00341             case 'session':
00342                 if ( !$this->loadFromSession() ) {
00343                     // Loading from session failed. Load defaults.
00344                     $this->loadDefaults();
00345                 }
00346                 wfRunHooks( 'UserLoadAfterLoadFromSession', array( $this ) );
00347                 break;
00348             default:
00349                 wfProfileOut( __METHOD__ );
00350                 throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" );
00351         }
00352         wfProfileOut( __METHOD__ );
00353     }
00354 
00359     public function loadFromId() {
00360         global $wgMemc;
00361         if ( $this->mId == 0 ) {
00362             $this->loadDefaults();
00363             return false;
00364         }
00365 
00366         // Try cache
00367         $key = wfMemcKey( 'user', 'id', $this->mId );
00368         $data = $wgMemc->get( $key );
00369         if ( !is_array( $data ) || $data['mVersion'] != self::VERSION ) {
00370             // Object is expired, load from DB
00371             $data = false;
00372         }
00373 
00374         if ( !$data ) {
00375             wfDebug( "User: cache miss for user {$this->mId}\n" );
00376             // Load from DB
00377             if ( !$this->loadFromDatabase() ) {
00378                 // Can't load from ID, user is anonymous
00379                 return false;
00380             }
00381             $this->saveToCache();
00382         } else {
00383             wfDebug( "User: got user {$this->mId} from cache\n" );
00384             // Restore from cache
00385             foreach ( self::$mCacheVars as $name ) {
00386                 $this->$name = $data[$name];
00387             }
00388         }
00389 
00390         $this->mLoadedItems = true;
00391 
00392         return true;
00393     }
00394 
00398     public function saveToCache() {
00399         $this->load();
00400         $this->loadGroups();
00401         $this->loadOptions();
00402         if ( $this->isAnon() ) {
00403             // Anonymous users are uncached
00404             return;
00405         }
00406         $data = array();
00407         foreach ( self::$mCacheVars as $name ) {
00408             $data[$name] = $this->$name;
00409         }
00410         $data['mVersion'] = self::VERSION;
00411         $key = wfMemcKey( 'user', 'id', $this->mId );
00412         global $wgMemc;
00413         $wgMemc->set( $key, $data );
00414     }
00415 
00418 
00435     public static function newFromName( $name, $validate = 'valid' ) {
00436         if ( $validate === true ) {
00437             $validate = 'valid';
00438         }
00439         $name = self::getCanonicalName( $name, $validate );
00440         if ( $name === false ) {
00441             return false;
00442         } else {
00443             // Create unloaded user object
00444             $u = new User;
00445             $u->mName = $name;
00446             $u->mFrom = 'name';
00447             $u->setItemLoaded( 'name' );
00448             return $u;
00449         }
00450     }
00451 
00458     public static function newFromId( $id ) {
00459         $u = new User;
00460         $u->mId = $id;
00461         $u->mFrom = 'id';
00462         $u->setItemLoaded( 'id' );
00463         return $u;
00464     }
00465 
00476     public static function newFromConfirmationCode( $code ) {
00477         $dbr = wfGetDB( DB_SLAVE );
00478         $id = $dbr->selectField( 'user', 'user_id', array(
00479             'user_email_token' => md5( $code ),
00480             'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
00481             ) );
00482         if ( $id !== false ) {
00483             return User::newFromId( $id );
00484         } else {
00485             return null;
00486         }
00487     }
00488 
00496     public static function newFromSession( WebRequest $request = null ) {
00497         $user = new User;
00498         $user->mFrom = 'session';
00499         $user->mRequest = $request;
00500         return $user;
00501     }
00502 
00517     public static function newFromRow( $row, $data = null ) {
00518         $user = new User;
00519         $user->loadFromRow( $row, $data );
00520         return $user;
00521     }
00522 
00524 
00530     public static function whoIs( $id ) {
00531         return UserCache::singleton()->getProp( $id, 'name' );
00532     }
00533 
00540     public static function whoIsReal( $id ) {
00541         return UserCache::singleton()->getProp( $id, 'real_name' );
00542     }
00543 
00549     public static function idFromName( $name ) {
00550         $nt = Title::makeTitleSafe( NS_USER, $name );
00551         if ( is_null( $nt ) ) {
00552             // Illegal name
00553             return null;
00554         }
00555 
00556         if ( isset( self::$idCacheByName[$name] ) ) {
00557             return self::$idCacheByName[$name];
00558         }
00559 
00560         $dbr = wfGetDB( DB_SLAVE );
00561         $s = $dbr->selectRow(
00562             'user',
00563             array( 'user_id' ),
00564             array( 'user_name' => $nt->getText() ),
00565             __METHOD__
00566         );
00567 
00568         if ( $s === false ) {
00569             $result = null;
00570         } else {
00571             $result = $s->user_id;
00572         }
00573 
00574         self::$idCacheByName[$name] = $result;
00575 
00576         if ( count( self::$idCacheByName ) > 1000 ) {
00577             self::$idCacheByName = array();
00578         }
00579 
00580         return $result;
00581     }
00582 
00586     public static function resetIdByNameCache() {
00587         self::$idCacheByName = array();
00588     }
00589 
00606     public static function isIP( $name ) {
00607         return preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.(?:xxx|\d{1,3})$/', $name )
00608             || IP::isIPv6( $name );
00609     }
00610 
00622     public static function isValidUserName( $name ) {
00623         global $wgContLang, $wgMaxNameChars;
00624 
00625         if ( $name == ''
00626         || User::isIP( $name )
00627         || strpos( $name, '/' ) !== false
00628         || strlen( $name ) > $wgMaxNameChars
00629         || $name != $wgContLang->ucfirst( $name ) ) {
00630             wfDebugLog( 'username', __METHOD__ .
00631                 ": '$name' invalid due to empty, IP, slash, length, or lowercase" );
00632             return false;
00633         }
00634 
00635         // Ensure that the name can't be misresolved as a different title,
00636         // such as with extra namespace keys at the start.
00637         $parsed = Title::newFromText( $name );
00638         if ( is_null( $parsed )
00639             || $parsed->getNamespace()
00640             || strcmp( $name, $parsed->getPrefixedText() ) ) {
00641             wfDebugLog( 'username', __METHOD__ .
00642                 ": '$name' invalid due to ambiguous prefixes" );
00643             return false;
00644         }
00645 
00646         // Check an additional blacklist of troublemaker characters.
00647         // Should these be merged into the title char list?
00648         $unicodeBlacklist = '/[' .
00649             '\x{0080}-\x{009f}' . # iso-8859-1 control chars
00650             '\x{00a0}' .          # non-breaking space
00651             '\x{2000}-\x{200f}' . # various whitespace
00652             '\x{2028}-\x{202f}' . # breaks and control chars
00653             '\x{3000}' .          # ideographic space
00654             '\x{e000}-\x{f8ff}' . # private use
00655             ']/u';
00656         if ( preg_match( $unicodeBlacklist, $name ) ) {
00657             wfDebugLog( 'username', __METHOD__ .
00658                 ": '$name' invalid due to blacklisted characters" );
00659             return false;
00660         }
00661 
00662         return true;
00663     }
00664 
00676     public static function isUsableName( $name ) {
00677         global $wgReservedUsernames;
00678         // Must be a valid username, obviously ;)
00679         if ( !self::isValidUserName( $name ) ) {
00680             return false;
00681         }
00682 
00683         static $reservedUsernames = false;
00684         if ( !$reservedUsernames ) {
00685             $reservedUsernames = $wgReservedUsernames;
00686             wfRunHooks( 'UserGetReservedNames', array( &$reservedUsernames ) );
00687         }
00688 
00689         // Certain names may be reserved for batch processes.
00690         foreach ( $reservedUsernames as $reserved ) {
00691             if ( substr( $reserved, 0, 4 ) == 'msg:' ) {
00692                 $reserved = wfMessage( substr( $reserved, 4 ) )->inContentLanguage()->text();
00693             }
00694             if ( $reserved == $name ) {
00695                 return false;
00696             }
00697         }
00698         return true;
00699     }
00700 
00713     public static function isCreatableName( $name ) {
00714         global $wgInvalidUsernameCharacters;
00715 
00716         // Ensure that the username isn't longer than 235 bytes, so that
00717         // (at least for the builtin skins) user javascript and css files
00718         // will work. (bug 23080)
00719         if ( strlen( $name ) > 235 ) {
00720             wfDebugLog( 'username', __METHOD__ .
00721                 ": '$name' invalid due to length" );
00722             return false;
00723         }
00724 
00725         // Preg yells if you try to give it an empty string
00726         if ( $wgInvalidUsernameCharacters !== '' ) {
00727             if ( preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ) ) {
00728                 wfDebugLog( 'username', __METHOD__ .
00729                     ": '$name' invalid due to wgInvalidUsernameCharacters" );
00730                 return false;
00731             }
00732         }
00733 
00734         return self::isUsableName( $name );
00735     }
00736 
00743     public function isValidPassword( $password ) {
00744         //simple boolean wrapper for getPasswordValidity
00745         return $this->getPasswordValidity( $password ) === true;
00746     }
00747 
00748 
00755     public function getPasswordValidity( $password ) {
00756         $result = $this->checkPasswordValidity( $password );
00757         if ( $result->isGood() ) {
00758             return true;
00759         } else {
00760             $messages = array();
00761             foreach ( $result->getErrorsByType( 'error' ) as $error ) {
00762                 $messages[] = $error['message'];
00763             }
00764             foreach ( $result->getErrorsByType( 'warning' ) as $warning ) {
00765                 $messages[] = $warning['message'];
00766             }
00767             if ( count( $messages ) === 1 ) {
00768                 return $messages[0];
00769             }
00770             return $messages;
00771         }
00772     }
00773 
00782     public function checkPasswordValidity( $password ) {
00783         global $wgMinimalPasswordLength, $wgContLang;
00784 
00785         static $blockedLogins = array(
00786             'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589
00787             'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605
00788         );
00789 
00790         $status = Status::newGood();
00791 
00792         $result = false; //init $result to false for the internal checks
00793 
00794         if ( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) {
00795             $status->error( $result );
00796             return $status;
00797         }
00798 
00799         if ( $result === false ) {
00800             if ( strlen( $password ) < $wgMinimalPasswordLength ) {
00801                 $status->error( 'passwordtooshort', $wgMinimalPasswordLength );
00802                 return $status;
00803             } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
00804                 $status->error( 'password-name-match' );
00805                 return $status;
00806             } elseif ( isset( $blockedLogins[$this->getName()] )
00807                 && $password == $blockedLogins[$this->getName()]
00808             ) {
00809                 $status->error( 'password-login-forbidden' );
00810                 return $status;
00811             } else {
00812                 //it seems weird returning a Good status here, but this is because of the
00813                 //initialization of $result to false above. If the hook is never run or it
00814                 //doesn't modify $result, then we will likely get down into this if with
00815                 //a valid password.
00816                 return $status;
00817             }
00818         } elseif ( $result === true ) {
00819             return $status;
00820         } else {
00821             $status->error( $result );
00822             return $status; //the isValidPassword hook set a string $result and returned true
00823         }
00824     }
00825 
00831     public function expirePassword( $ts = 0 ) {
00832         $this->loadPasswords();
00833         $timestamp = wfTimestamp( TS_MW, $ts );
00834         $this->mPasswordExpires = $timestamp;
00835         $this->saveSettings();
00836     }
00837 
00843     public function resetPasswordExpiration( $load = true ) {
00844         global $wgPasswordExpirationDays;
00845         if ( $load ) {
00846             $this->load();
00847         }
00848         $newExpire = null;
00849         if ( $wgPasswordExpirationDays ) {
00850             $newExpire = wfTimestamp(
00851                 TS_MW,
00852                 time() + ( $wgPasswordExpirationDays * 24 * 3600 )
00853             );
00854         }
00855         // Give extensions a chance to force an expiration
00856         wfRunHooks( 'ResetPasswordExpiration', array( $this, &$newExpire ) );
00857         $this->mPasswordExpires = $newExpire;
00858     }
00859 
00869     public function getPasswordExpired() {
00870         global $wgPasswordExpireGrace;
00871         $expired = false;
00872         $now = wfTimestamp();
00873         $expiration = $this->getPasswordExpireDate();
00874         $expUnix = wfTimestamp( TS_UNIX, $expiration );
00875         if ( $expiration !== null && $expUnix < $now ) {
00876             $expired = ( $expUnix + $wgPasswordExpireGrace < $now ) ? 'hard' : 'soft';
00877         }
00878         return $expired;
00879     }
00880 
00888     public function getPasswordExpireDate() {
00889         $this->load();
00890         return $this->mPasswordExpires;
00891     }
00892 
00906     public static function getCanonicalName( $name, $validate = 'valid' ) {
00907         // Force usernames to capital
00908         global $wgContLang;
00909         $name = $wgContLang->ucfirst( $name );
00910 
00911         # Reject names containing '#'; these will be cleaned up
00912         # with title normalisation, but then it's too late to
00913         # check elsewhere
00914         if ( strpos( $name, '#' ) !== false ) {
00915             return false;
00916         }
00917 
00918         // Clean up name according to title rules,
00919         // but only when validation is requested (bug 12654)
00920         $t = ( $validate !== false ) ?
00921             Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
00922         // Check for invalid titles
00923         if ( is_null( $t ) ) {
00924             return false;
00925         }
00926 
00927         // Reject various classes of invalid names
00928         global $wgAuth;
00929         $name = $wgAuth->getCanonicalName( $t->getText() );
00930 
00931         switch ( $validate ) {
00932             case false:
00933                 break;
00934             case 'valid':
00935                 if ( !User::isValidUserName( $name ) ) {
00936                     $name = false;
00937                 }
00938                 break;
00939             case 'usable':
00940                 if ( !User::isUsableName( $name ) ) {
00941                     $name = false;
00942                 }
00943                 break;
00944             case 'creatable':
00945                 if ( !User::isCreatableName( $name ) ) {
00946                     $name = false;
00947                 }
00948                 break;
00949             default:
00950                 throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
00951         }
00952         return $name;
00953     }
00954 
00963     public static function edits( $uid ) {
00964         wfDeprecated( __METHOD__, '1.21' );
00965         $user = self::newFromId( $uid );
00966         return $user->getEditCount();
00967     }
00968 
00974     public static function randomPassword() {
00975         global $wgMinimalPasswordLength;
00976         // Decide the final password length based on our min password length,
00977         // stopping at a minimum of 10 chars.
00978         $length = max( 10, $wgMinimalPasswordLength );
00979         // Multiply by 1.25 to get the number of hex characters we need
00980         $length = $length * 1.25;
00981         // Generate random hex chars
00982         $hex = MWCryptRand::generateHex( $length );
00983         // Convert from base 16 to base 32 to get a proper password like string
00984         return wfBaseConvert( $hex, 16, 32 );
00985     }
00986 
00995     public function loadDefaults( $name = false ) {
00996         wfProfileIn( __METHOD__ );
00997 
00998         $passwordFactory = self::getPasswordFactory();
00999 
01000         $this->mId = 0;
01001         $this->mName = $name;
01002         $this->mRealName = '';
01003         $this->mPassword = $passwordFactory->newFromCiphertext( null );
01004         $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
01005         $this->mNewpassTime = null;
01006         $this->mEmail = '';
01007         $this->mOptionOverrides = null;
01008         $this->mOptionsLoaded = false;
01009 
01010         $loggedOut = $this->getRequest()->getCookie( 'LoggedOut' );
01011         if ( $loggedOut !== null ) {
01012             $this->mTouched = wfTimestamp( TS_MW, $loggedOut );
01013         } else {
01014             $this->mTouched = '1'; # Allow any pages to be cached
01015         }
01016 
01017         $this->mToken = null; // Don't run cryptographic functions till we need a token
01018         $this->mEmailAuthenticated = null;
01019         $this->mEmailToken = '';
01020         $this->mEmailTokenExpires = null;
01021         $this->mPasswordExpires = null;
01022         $this->resetPasswordExpiration( false );
01023         $this->mRegistration = wfTimestamp( TS_MW );
01024         $this->mGroups = array();
01025 
01026         wfRunHooks( 'UserLoadDefaults', array( $this, $name ) );
01027 
01028         wfProfileOut( __METHOD__ );
01029     }
01030 
01043     public function isItemLoaded( $item, $all = 'all' ) {
01044         return ( $this->mLoadedItems === true && $all === 'all' ) ||
01045             ( isset( $this->mLoadedItems[$item] ) && $this->mLoadedItems[$item] === true );
01046     }
01047 
01053     protected function setItemLoaded( $item ) {
01054         if ( is_array( $this->mLoadedItems ) ) {
01055             $this->mLoadedItems[$item] = true;
01056         }
01057     }
01058 
01063     private function loadFromSession() {
01064         $result = null;
01065         wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
01066         if ( $result !== null ) {
01067             return $result;
01068         }
01069 
01070         $request = $this->getRequest();
01071 
01072         $cookieId = $request->getCookie( 'UserID' );
01073         $sessId = $request->getSessionData( 'wsUserID' );
01074 
01075         if ( $cookieId !== null ) {
01076             $sId = intval( $cookieId );
01077             if ( $sessId !== null && $cookieId != $sessId ) {
01078                 wfDebugLog( 'loginSessions', "Session user ID ($sessId) and
01079                     cookie user ID ($sId) don't match!" );
01080                 return false;
01081             }
01082             $request->setSessionData( 'wsUserID', $sId );
01083         } elseif ( $sessId !== null && $sessId != 0 ) {
01084             $sId = $sessId;
01085         } else {
01086             return false;
01087         }
01088 
01089         if ( $request->getSessionData( 'wsUserName' ) !== null ) {
01090             $sName = $request->getSessionData( 'wsUserName' );
01091         } elseif ( $request->getCookie( 'UserName' ) !== null ) {
01092             $sName = $request->getCookie( 'UserName' );
01093             $request->setSessionData( 'wsUserName', $sName );
01094         } else {
01095             return false;
01096         }
01097 
01098         $proposedUser = User::newFromId( $sId );
01099         if ( !$proposedUser->isLoggedIn() ) {
01100             // Not a valid ID
01101             return false;
01102         }
01103 
01104         global $wgBlockDisablesLogin;
01105         if ( $wgBlockDisablesLogin && $proposedUser->isBlocked() ) {
01106             // User blocked and we've disabled blocked user logins
01107             return false;
01108         }
01109 
01110         if ( $request->getSessionData( 'wsToken' ) ) {
01111             $passwordCorrect =
01112                 ( $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ) );
01113             $from = 'session';
01114         } elseif ( $request->getCookie( 'Token' ) ) {
01115             # Get the token from DB/cache and clean it up to remove garbage padding.
01116             # This deals with historical problems with bugs and the default column value.
01117             $token = rtrim( $proposedUser->getToken( false ) ); // correct token
01118             // Make comparison in constant time (bug 61346)
01119             $passwordCorrect = strlen( $token )
01120                 && hash_equals( $token, $request->getCookie( 'Token' ) );
01121             $from = 'cookie';
01122         } else {
01123             // No session or persistent login cookie
01124             return false;
01125         }
01126 
01127         if ( ( $sName === $proposedUser->getName() ) && $passwordCorrect ) {
01128             $this->loadFromUserObject( $proposedUser );
01129             $request->setSessionData( 'wsToken', $this->mToken );
01130             wfDebug( "User: logged in from $from\n" );
01131             return true;
01132         } else {
01133             // Invalid credentials
01134             wfDebug( "User: can't log in from $from, invalid credentials\n" );
01135             return false;
01136         }
01137     }
01138 
01146     public function loadFromDatabase( $flags = 0 ) {
01147         // Paranoia
01148         $this->mId = intval( $this->mId );
01149 
01150         // Anonymous user
01151         if ( !$this->mId ) {
01152             $this->loadDefaults();
01153             return false;
01154         }
01155 
01156         $dbr = wfGetDB( DB_MASTER );
01157         $s = $dbr->selectRow(
01158             'user',
01159             self::selectFields(),
01160             array( 'user_id' => $this->mId ),
01161             __METHOD__,
01162             ( $flags & self::READ_LOCKING == self::READ_LOCKING )
01163                 ? array( 'LOCK IN SHARE MODE' )
01164                 : array()
01165         );
01166 
01167         wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) );
01168 
01169         if ( $s !== false ) {
01170             // Initialise user table data
01171             $this->loadFromRow( $s );
01172             $this->mGroups = null; // deferred
01173             $this->getEditCount(); // revalidation for nulls
01174             return true;
01175         } else {
01176             // Invalid user_id
01177             $this->mId = 0;
01178             $this->loadDefaults();
01179             return false;
01180         }
01181     }
01182 
01192     public function loadFromRow( $row, $data = null ) {
01193         $all = true;
01194         $passwordFactory = self::getPasswordFactory();
01195 
01196         $this->mGroups = null; // deferred
01197 
01198         if ( isset( $row->user_name ) ) {
01199             $this->mName = $row->user_name;
01200             $this->mFrom = 'name';
01201             $this->setItemLoaded( 'name' );
01202         } else {
01203             $all = false;
01204         }
01205 
01206         if ( isset( $row->user_real_name ) ) {
01207             $this->mRealName = $row->user_real_name;
01208             $this->setItemLoaded( 'realname' );
01209         } else {
01210             $all = false;
01211         }
01212 
01213         if ( isset( $row->user_id ) ) {
01214             $this->mId = intval( $row->user_id );
01215             $this->mFrom = 'id';
01216             $this->setItemLoaded( 'id' );
01217         } else {
01218             $all = false;
01219         }
01220 
01221         if ( isset( $row->user_editcount ) ) {
01222             $this->mEditCount = $row->user_editcount;
01223         } else {
01224             $all = false;
01225         }
01226 
01227         if ( isset( $row->user_password ) ) {
01228             // Check for *really* old password hashes that don't even have a type
01229             // The old hash format was just an md5 hex hash, with no type information
01230             if ( preg_match( '/^[0-9a-f]{32}$/', $row->user_password ) ) {
01231                 $row->user_password = ":A:{$this->mId}:{$row->user_password}";
01232             }
01233 
01234             try {
01235                 $this->mPassword = $passwordFactory->newFromCiphertext( $row->user_password );
01236             } catch ( PasswordError $e ) {
01237                 wfDebug( 'Invalid password hash found in database.' );
01238                 $this->mPassword = $passwordFactory->newFromCiphertext( null );
01239             }
01240 
01241             try {
01242                 $this->mNewpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
01243             } catch ( PasswordError $e ) {
01244                 wfDebug( 'Invalid password hash found in database.' );
01245                 $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
01246             }
01247 
01248             $this->mNewpassTime = wfTimestampOrNull( TS_MW, $row->user_newpass_time );
01249             $this->mPasswordExpires = wfTimestampOrNull( TS_MW, $row->user_password_expires );
01250         }
01251 
01252         if ( isset( $row->user_email ) ) {
01253             $this->mEmail = $row->user_email;
01254             $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
01255             $this->mToken = $row->user_token;
01256             if ( $this->mToken == '' ) {
01257                 $this->mToken = null;
01258             }
01259             $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
01260             $this->mEmailToken = $row->user_email_token;
01261             $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
01262             $this->mRegistration = wfTimestampOrNull( TS_MW, $row->user_registration );
01263         } else {
01264             $all = false;
01265         }
01266 
01267         if ( $all ) {
01268             $this->mLoadedItems = true;
01269         }
01270 
01271         if ( is_array( $data ) ) {
01272             if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
01273                 $this->mGroups = $data['user_groups'];
01274             }
01275             if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
01276                 $this->loadOptions( $data['user_properties'] );
01277             }
01278         }
01279     }
01280 
01286     protected function loadFromUserObject( $user ) {
01287         $user->load();
01288         $user->loadGroups();
01289         $user->loadOptions();
01290         foreach ( self::$mCacheVars as $var ) {
01291             $this->$var = $user->$var;
01292         }
01293     }
01294 
01298     private function loadGroups() {
01299         if ( is_null( $this->mGroups ) ) {
01300             $dbr = wfGetDB( DB_MASTER );
01301             $res = $dbr->select( 'user_groups',
01302                 array( 'ug_group' ),
01303                 array( 'ug_user' => $this->mId ),
01304                 __METHOD__ );
01305             $this->mGroups = array();
01306             foreach ( $res as $row ) {
01307                 $this->mGroups[] = $row->ug_group;
01308             }
01309         }
01310     }
01311 
01321     private function loadPasswords() {
01322         if ( $this->getId() !== 0 && ( $this->mPassword === null || $this->mNewpassword === null ) ) {
01323             $this->loadFromRow( wfGetDB( DB_MASTER )->selectRow(
01324                     'user',
01325                     array( 'user_password', 'user_newpassword', 'user_newpass_time', 'user_password_expires' ),
01326                     array( 'user_id' => $this->getId() ),
01327                     __METHOD__
01328                 ) );
01329         }
01330     }
01331 
01346     public function addAutopromoteOnceGroups( $event ) {
01347         global $wgAutopromoteOnceLogInRC, $wgAuth;
01348 
01349         $toPromote = array();
01350         if ( $this->getId() ) {
01351             $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event );
01352             if ( count( $toPromote ) ) {
01353                 $oldGroups = $this->getGroups(); // previous groups
01354 
01355                 foreach ( $toPromote as $group ) {
01356                     $this->addGroup( $group );
01357                 }
01358                 // update groups in external authentication database
01359                 $wgAuth->updateExternalDBGroups( $this, $toPromote );
01360 
01361                 $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
01362 
01363                 $logEntry = new ManualLogEntry( 'rights', 'autopromote' );
01364                 $logEntry->setPerformer( $this );
01365                 $logEntry->setTarget( $this->getUserPage() );
01366                 $logEntry->setParameters( array(
01367                     '4::oldgroups' => $oldGroups,
01368                     '5::newgroups' => $newGroups,
01369                 ) );
01370                 $logid = $logEntry->insert();
01371                 if ( $wgAutopromoteOnceLogInRC ) {
01372                     $logEntry->publish( $logid );
01373                 }
01374             }
01375         }
01376         return $toPromote;
01377     }
01378 
01386     public function clearInstanceCache( $reloadFrom = false ) {
01387         $this->mNewtalk = -1;
01388         $this->mDatePreference = null;
01389         $this->mBlockedby = -1; # Unset
01390         $this->mHash = false;
01391         $this->mRights = null;
01392         $this->mEffectiveGroups = null;
01393         $this->mImplicitGroups = null;
01394         $this->mGroups = null;
01395         $this->mOptions = null;
01396         $this->mOptionsLoaded = false;
01397         $this->mEditCount = null;
01398 
01399         if ( $reloadFrom ) {
01400             $this->mLoadedItems = array();
01401             $this->mFrom = $reloadFrom;
01402         }
01403     }
01404 
01411     public static function getDefaultOptions() {
01412         global $wgNamespacesToBeSearchedDefault, $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
01413 
01414         static $defOpt = null;
01415         if ( !defined( 'MW_PHPUNIT_TEST' ) && $defOpt !== null ) {
01416             // Disabling this for the unit tests, as they rely on being able to change $wgContLang
01417             // mid-request and see that change reflected in the return value of this function.
01418             // Which is insane and would never happen during normal MW operation
01419             return $defOpt;
01420         }
01421 
01422         $defOpt = $wgDefaultUserOptions;
01423         // Default language setting
01424         $defOpt['language'] = $wgContLang->getCode();
01425         foreach ( LanguageConverter::$languagesWithVariants as $langCode ) {
01426             $defOpt[$langCode == $wgContLang->getCode() ? 'variant' : "variant-$langCode"] = $langCode;
01427         }
01428         foreach ( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
01429             $defOpt['searchNs' . $nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
01430         }
01431         $defOpt['skin'] = Skin::normalizeKey( $wgDefaultSkin );
01432 
01433         wfRunHooks( 'UserGetDefaultOptions', array( &$defOpt ) );
01434 
01435         return $defOpt;
01436     }
01437 
01444     public static function getDefaultOption( $opt ) {
01445         $defOpts = self::getDefaultOptions();
01446         if ( isset( $defOpts[$opt] ) ) {
01447             return $defOpts[$opt];
01448         } else {
01449             return null;
01450         }
01451     }
01452 
01459     private function getBlockedStatus( $bFromSlave = true ) {
01460         global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff;
01461 
01462         if ( -1 != $this->mBlockedby ) {
01463             return;
01464         }
01465 
01466         wfProfileIn( __METHOD__ );
01467         wfDebug( __METHOD__ . ": checking...\n" );
01468 
01469         // Initialize data...
01470         // Otherwise something ends up stomping on $this->mBlockedby when
01471         // things get lazy-loaded later, causing false positive block hits
01472         // due to -1 !== 0. Probably session-related... Nothing should be
01473         // overwriting mBlockedby, surely?
01474         $this->load();
01475 
01476         # We only need to worry about passing the IP address to the Block generator if the
01477         # user is not immune to autoblocks/hardblocks, and they are the current user so we
01478         # know which IP address they're actually coming from
01479         if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) {
01480             $ip = $this->getRequest()->getIP();
01481         } else {
01482             $ip = null;
01483         }
01484 
01485         // User/IP blocking
01486         $block = Block::newFromTarget( $this, $ip, !$bFromSlave );
01487 
01488         // Proxy blocking
01489         if ( !$block instanceof Block && $ip !== null && !$this->isAllowed( 'proxyunbannable' )
01490             && !in_array( $ip, $wgProxyWhitelist )
01491         ) {
01492             // Local list
01493             if ( self::isLocallyBlockedProxy( $ip ) ) {
01494                 $block = new Block;
01495                 $block->setBlocker( wfMessage( 'proxyblocker' )->text() );
01496                 $block->mReason = wfMessage( 'proxyblockreason' )->text();
01497                 $block->setTarget( $ip );
01498             } elseif ( $this->isAnon() && $this->isDnsBlacklisted( $ip ) ) {
01499                 $block = new Block;
01500                 $block->setBlocker( wfMessage( 'sorbs' )->text() );
01501                 $block->mReason = wfMessage( 'sorbsreason' )->text();
01502                 $block->setTarget( $ip );
01503             }
01504         }
01505 
01506         // (bug 23343) Apply IP blocks to the contents of XFF headers, if enabled
01507         if ( !$block instanceof Block
01508             && $wgApplyIpBlocksToXff
01509             && $ip !== null
01510             && !$this->isAllowed( 'proxyunbannable' )
01511             && !in_array( $ip, $wgProxyWhitelist )
01512         ) {
01513             $xff = $this->getRequest()->getHeader( 'X-Forwarded-For' );
01514             $xff = array_map( 'trim', explode( ',', $xff ) );
01515             $xff = array_diff( $xff, array( $ip ) );
01516             $xffblocks = Block::getBlocksForIPList( $xff, $this->isAnon(), !$bFromSlave );
01517             $block = Block::chooseBlock( $xffblocks, $xff );
01518             if ( $block instanceof Block ) {
01519                 # Mangle the reason to alert the user that the block
01520                 # originated from matching the X-Forwarded-For header.
01521                 $block->mReason = wfMessage( 'xffblockreason', $block->mReason )->text();
01522             }
01523         }
01524 
01525         if ( $block instanceof Block ) {
01526             wfDebug( __METHOD__ . ": Found block.\n" );
01527             $this->mBlock = $block;
01528             $this->mBlockedby = $block->getByName();
01529             $this->mBlockreason = $block->mReason;
01530             $this->mHideName = $block->mHideName;
01531             $this->mAllowUsertalk = !$block->prevents( 'editownusertalk' );
01532         } else {
01533             $this->mBlockedby = '';
01534             $this->mHideName = 0;
01535             $this->mAllowUsertalk = false;
01536         }
01537 
01538         // Extensions
01539         wfRunHooks( 'GetBlockedStatus', array( &$this ) );
01540 
01541         wfProfileOut( __METHOD__ );
01542     }
01543 
01551     public function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
01552         global $wgEnableDnsBlacklist, $wgDnsBlacklistUrls, $wgProxyWhitelist;
01553 
01554         if ( !$wgEnableDnsBlacklist ) {
01555             return false;
01556         }
01557 
01558         if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) ) {
01559             return false;
01560         }
01561 
01562         return $this->inDnsBlacklist( $ip, $wgDnsBlacklistUrls );
01563     }
01564 
01572     public function inDnsBlacklist( $ip, $bases ) {
01573         wfProfileIn( __METHOD__ );
01574 
01575         $found = false;
01576         // @todo FIXME: IPv6 ???  (http://bugs.php.net/bug.php?id=33170)
01577         if ( IP::isIPv4( $ip ) ) {
01578             // Reverse IP, bug 21255
01579             $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
01580 
01581             foreach ( (array)$bases as $base ) {
01582                 // Make hostname
01583                 // If we have an access key, use that too (ProjectHoneypot, etc.)
01584                 if ( is_array( $base ) ) {
01585                     if ( count( $base ) >= 2 ) {
01586                         // Access key is 1, base URL is 0
01587                         $host = "{$base[1]}.$ipReversed.{$base[0]}";
01588                     } else {
01589                         $host = "$ipReversed.{$base[0]}";
01590                     }
01591                 } else {
01592                     $host = "$ipReversed.$base";
01593                 }
01594 
01595                 // Send query
01596                 $ipList = gethostbynamel( $host );
01597 
01598                 if ( $ipList ) {
01599                     wfDebugLog( 'dnsblacklist', "Hostname $host is {$ipList[0]}, it's a proxy says $base!" );
01600                     $found = true;
01601                     break;
01602                 } else {
01603                     wfDebugLog( 'dnsblacklist', "Requested $host, not found in $base." );
01604                 }
01605             }
01606         }
01607 
01608         wfProfileOut( __METHOD__ );
01609         return $found;
01610     }
01611 
01619     public static function isLocallyBlockedProxy( $ip ) {
01620         global $wgProxyList;
01621 
01622         if ( !$wgProxyList ) {
01623             return false;
01624         }
01625         wfProfileIn( __METHOD__ );
01626 
01627         if ( !is_array( $wgProxyList ) ) {
01628             // Load from the specified file
01629             $wgProxyList = array_map( 'trim', file( $wgProxyList ) );
01630         }
01631 
01632         if ( !is_array( $wgProxyList ) ) {
01633             $ret = false;
01634         } elseif ( array_search( $ip, $wgProxyList ) !== false ) {
01635             $ret = true;
01636         } elseif ( array_key_exists( $ip, $wgProxyList ) ) {
01637             // Old-style flipped proxy list
01638             $ret = true;
01639         } else {
01640             $ret = false;
01641         }
01642         wfProfileOut( __METHOD__ );
01643         return $ret;
01644     }
01645 
01651     public function isPingLimitable() {
01652         global $wgRateLimitsExcludedIPs;
01653         if ( in_array( $this->getRequest()->getIP(), $wgRateLimitsExcludedIPs ) ) {
01654             // No other good way currently to disable rate limits
01655             // for specific IPs. :P
01656             // But this is a crappy hack and should die.
01657             return false;
01658         }
01659         return !$this->isAllowed( 'noratelimit' );
01660     }
01661 
01676     public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
01677         // Call the 'PingLimiter' hook
01678         $result = false;
01679         if ( !wfRunHooks( 'PingLimiter', array( &$this, $action, &$result, $incrBy ) ) ) {
01680             return $result;
01681         }
01682 
01683         global $wgRateLimits;
01684         if ( !isset( $wgRateLimits[$action] ) ) {
01685             return false;
01686         }
01687 
01688         // Some groups shouldn't trigger the ping limiter, ever
01689         if ( !$this->isPingLimitable() ) {
01690             return false;
01691         }
01692 
01693         global $wgMemc;
01694         wfProfileIn( __METHOD__ );
01695         wfProfileIn( __METHOD__ . '-' . $action );
01696 
01697         $limits = $wgRateLimits[$action];
01698         $keys = array();
01699         $id = $this->getId();
01700         $userLimit = false;
01701 
01702         if ( isset( $limits['anon'] ) && $id == 0 ) {
01703             $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
01704         }
01705 
01706         if ( isset( $limits['user'] ) && $id != 0 ) {
01707             $userLimit = $limits['user'];
01708         }
01709         if ( $this->isNewbie() ) {
01710             if ( isset( $limits['newbie'] ) && $id != 0 ) {
01711                 $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
01712             }
01713             if ( isset( $limits['ip'] ) ) {
01714                 $ip = $this->getRequest()->getIP();
01715                 $keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
01716             }
01717             if ( isset( $limits['subnet'] ) ) {
01718                 $ip = $this->getRequest()->getIP();
01719                 $matches = array();
01720                 $subnet = false;
01721                 if ( IP::isIPv6( $ip ) ) {
01722                     $parts = IP::parseRange( "$ip/64" );
01723                     $subnet = $parts[0];
01724                 } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
01725                     // IPv4
01726                     $subnet = $matches[1];
01727                 }
01728                 if ( $subnet !== false ) {
01729                     $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
01730                 }
01731             }
01732         }
01733         // Check for group-specific permissions
01734         // If more than one group applies, use the group with the highest limit
01735         foreach ( $this->getGroups() as $group ) {
01736             if ( isset( $limits[$group] ) ) {
01737                 if ( $userLimit === false || $limits[$group] > $userLimit ) {
01738                     $userLimit = $limits[$group];
01739                 }
01740             }
01741         }
01742         // Set the user limit key
01743         if ( $userLimit !== false ) {
01744             list( $max, $period ) = $userLimit;
01745             wfDebug( __METHOD__ . ": effective user limit: $max in {$period}s\n" );
01746             $keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit;
01747         }
01748 
01749         $triggered = false;
01750         foreach ( $keys as $key => $limit ) {
01751             list( $max, $period ) = $limit;
01752             $summary = "(limit $max in {$period}s)";
01753             $count = $wgMemc->get( $key );
01754             // Already pinged?
01755             if ( $count ) {
01756                 if ( $count >= $max ) {
01757                     wfDebugLog( 'ratelimit', "User '{$this->getName()}' " .
01758                         "(IP {$this->getRequest()->getIP()}) tripped $key at $count $summary" );
01759                     $triggered = true;
01760                 } else {
01761                     wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
01762                 }
01763             } else {
01764                 wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
01765                 if ( $incrBy > 0 ) {
01766                     $wgMemc->add( $key, 0, intval( $period ) ); // first ping
01767                 }
01768             }
01769             if ( $incrBy > 0 ) {
01770                 $wgMemc->incr( $key, $incrBy );
01771             }
01772         }
01773 
01774         wfProfileOut( __METHOD__ . '-' . $action );
01775         wfProfileOut( __METHOD__ );
01776         return $triggered;
01777     }
01778 
01786     public function isBlocked( $bFromSlave = true ) {
01787         return $this->getBlock( $bFromSlave ) instanceof Block && $this->getBlock()->prevents( 'edit' );
01788     }
01789 
01796     public function getBlock( $bFromSlave = true ) {
01797         $this->getBlockedStatus( $bFromSlave );
01798         return $this->mBlock instanceof Block ? $this->mBlock : null;
01799     }
01800 
01808     public function isBlockedFrom( $title, $bFromSlave = false ) {
01809         global $wgBlockAllowsUTEdit;
01810         wfProfileIn( __METHOD__ );
01811 
01812         $blocked = $this->isBlocked( $bFromSlave );
01813         $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
01814         // If a user's name is suppressed, they cannot make edits anywhere
01815         if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName()
01816             && $title->getNamespace() == NS_USER_TALK ) {
01817             $blocked = false;
01818             wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
01819         }
01820 
01821         wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
01822 
01823         wfProfileOut( __METHOD__ );
01824         return $blocked;
01825     }
01826 
01831     public function blockedBy() {
01832         $this->getBlockedStatus();
01833         return $this->mBlockedby;
01834     }
01835 
01840     public function blockedFor() {
01841         $this->getBlockedStatus();
01842         return $this->mBlockreason;
01843     }
01844 
01849     public function getBlockId() {
01850         $this->getBlockedStatus();
01851         return ( $this->mBlock ? $this->mBlock->getId() : false );
01852     }
01853 
01862     public function isBlockedGlobally( $ip = '' ) {
01863         if ( $this->mBlockedGlobally !== null ) {
01864             return $this->mBlockedGlobally;
01865         }
01866         // User is already an IP?
01867         if ( IP::isIPAddress( $this->getName() ) ) {
01868             $ip = $this->getName();
01869         } elseif ( !$ip ) {
01870             $ip = $this->getRequest()->getIP();
01871         }
01872         $blocked = false;
01873         wfRunHooks( 'UserIsBlockedGlobally', array( &$this, $ip, &$blocked ) );
01874         $this->mBlockedGlobally = (bool)$blocked;
01875         return $this->mBlockedGlobally;
01876     }
01877 
01883     public function isLocked() {
01884         if ( $this->mLocked !== null ) {
01885             return $this->mLocked;
01886         }
01887         global $wgAuth;
01888         $authUser = $wgAuth->getUserInstance( $this );
01889         $this->mLocked = (bool)$authUser->isLocked();
01890         return $this->mLocked;
01891     }
01892 
01898     public function isHidden() {
01899         if ( $this->mHideName !== null ) {
01900             return $this->mHideName;
01901         }
01902         $this->getBlockedStatus();
01903         if ( !$this->mHideName ) {
01904             global $wgAuth;
01905             $authUser = $wgAuth->getUserInstance( $this );
01906             $this->mHideName = (bool)$authUser->isHidden();
01907         }
01908         return $this->mHideName;
01909     }
01910 
01915     public function getId() {
01916         if ( $this->mId === null && $this->mName !== null && User::isIP( $this->mName ) ) {
01917             // Special case, we know the user is anonymous
01918             return 0;
01919         } elseif ( !$this->isItemLoaded( 'id' ) ) {
01920             // Don't load if this was initialized from an ID
01921             $this->load();
01922         }
01923         return $this->mId;
01924     }
01925 
01930     public function setId( $v ) {
01931         $this->mId = $v;
01932         $this->clearInstanceCache( 'id' );
01933     }
01934 
01939     public function getName() {
01940         if ( $this->isItemLoaded( 'name', 'only' ) ) {
01941             // Special case optimisation
01942             return $this->mName;
01943         } else {
01944             $this->load();
01945             if ( $this->mName === false ) {
01946                 // Clean up IPs
01947                 $this->mName = IP::sanitizeIP( $this->getRequest()->getIP() );
01948             }
01949             return $this->mName;
01950         }
01951     }
01952 
01966     public function setName( $str ) {
01967         $this->load();
01968         $this->mName = $str;
01969     }
01970 
01975     public function getTitleKey() {
01976         return str_replace( ' ', '_', $this->getName() );
01977     }
01978 
01983     public function getNewtalk() {
01984         $this->load();
01985 
01986         // Load the newtalk status if it is unloaded (mNewtalk=-1)
01987         if ( $this->mNewtalk === -1 ) {
01988             $this->mNewtalk = false; # reset talk page status
01989 
01990             // Check memcached separately for anons, who have no
01991             // entire User object stored in there.
01992             if ( !$this->mId ) {
01993                 global $wgDisableAnonTalk;
01994                 if ( $wgDisableAnonTalk ) {
01995                     // Anon newtalk disabled by configuration.
01996                     $this->mNewtalk = false;
01997                 } else {
01998                     global $wgMemc;
01999                     $key = wfMemcKey( 'newtalk', 'ip', $this->getName() );
02000                     $newtalk = $wgMemc->get( $key );
02001                     if ( strval( $newtalk ) !== '' ) {
02002                         $this->mNewtalk = (bool)$newtalk;
02003                     } else {
02004                         // Since we are caching this, make sure it is up to date by getting it
02005                         // from the master
02006                         $this->mNewtalk = $this->checkNewtalk( 'user_ip', $this->getName(), true );
02007                         $wgMemc->set( $key, (int)$this->mNewtalk, 1800 );
02008                     }
02009                 }
02010             } else {
02011                 $this->mNewtalk = $this->checkNewtalk( 'user_id', $this->mId );
02012             }
02013         }
02014 
02015         return (bool)$this->mNewtalk;
02016     }
02017 
02031     public function getNewMessageLinks() {
02032         $talks = array();
02033         if ( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) ) {
02034             return $talks;
02035         } elseif ( !$this->getNewtalk() ) {
02036             return array();
02037         }
02038         $utp = $this->getTalkPage();
02039         $dbr = wfGetDB( DB_SLAVE );
02040         // Get the "last viewed rev" timestamp from the oldest message notification
02041         $timestamp = $dbr->selectField( 'user_newtalk',
02042             'MIN(user_last_timestamp)',
02043             $this->isAnon() ? array( 'user_ip' => $this->getName() ) : array( 'user_id' => $this->getID() ),
02044             __METHOD__ );
02045         $rev = $timestamp ? Revision::loadFromTimestamp( $dbr, $utp, $timestamp ) : null;
02046         return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL(), 'rev' => $rev ) );
02047     }
02048 
02054     public function getNewMessageRevisionId() {
02055         $newMessageRevisionId = null;
02056         $newMessageLinks = $this->getNewMessageLinks();
02057         if ( $newMessageLinks ) {
02058             // Note: getNewMessageLinks() never returns more than a single link
02059             // and it is always for the same wiki, but we double-check here in
02060             // case that changes some time in the future.
02061             if ( count( $newMessageLinks ) === 1
02062                 && $newMessageLinks[0]['wiki'] === wfWikiID()
02063                 && $newMessageLinks[0]['rev']
02064             ) {
02065                 $newMessageRevision = $newMessageLinks[0]['rev'];
02066                 $newMessageRevisionId = $newMessageRevision->getId();
02067             }
02068         }
02069         return $newMessageRevisionId;
02070     }
02071 
02081     protected function checkNewtalk( $field, $id, $fromMaster = false ) {
02082         if ( $fromMaster ) {
02083             $db = wfGetDB( DB_MASTER );
02084         } else {
02085             $db = wfGetDB( DB_SLAVE );
02086         }
02087         $ok = $db->selectField( 'user_newtalk', $field,
02088             array( $field => $id ), __METHOD__ );
02089         return $ok !== false;
02090     }
02091 
02099     protected function updateNewtalk( $field, $id, $curRev = null ) {
02100         // Get timestamp of the talk page revision prior to the current one
02101         $prevRev = $curRev ? $curRev->getPrevious() : false;
02102         $ts = $prevRev ? $prevRev->getTimestamp() : null;
02103         // Mark the user as having new messages since this revision
02104         $dbw = wfGetDB( DB_MASTER );
02105         $dbw->insert( 'user_newtalk',
02106             array( $field => $id, 'user_last_timestamp' => $dbw->timestampOrNull( $ts ) ),
02107             __METHOD__,
02108             'IGNORE' );
02109         if ( $dbw->affectedRows() ) {
02110             wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
02111             return true;
02112         } else {
02113             wfDebug( __METHOD__ . " already set ($field, $id)\n" );
02114             return false;
02115         }
02116     }
02117 
02124     protected function deleteNewtalk( $field, $id ) {
02125         $dbw = wfGetDB( DB_MASTER );
02126         $dbw->delete( 'user_newtalk',
02127             array( $field => $id ),
02128             __METHOD__ );
02129         if ( $dbw->affectedRows() ) {
02130             wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
02131             return true;
02132         } else {
02133             wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
02134             return false;
02135         }
02136     }
02137 
02144     public function setNewtalk( $val, $curRev = null ) {
02145         if ( wfReadOnly() ) {
02146             return;
02147         }
02148 
02149         $this->load();
02150         $this->mNewtalk = $val;
02151 
02152         if ( $this->isAnon() ) {
02153             $field = 'user_ip';
02154             $id = $this->getName();
02155         } else {
02156             $field = 'user_id';
02157             $id = $this->getId();
02158         }
02159         global $wgMemc;
02160 
02161         if ( $val ) {
02162             $changed = $this->updateNewtalk( $field, $id, $curRev );
02163         } else {
02164             $changed = $this->deleteNewtalk( $field, $id );
02165         }
02166 
02167         if ( $this->isAnon() ) {
02168             // Anons have a separate memcached space, since
02169             // user records aren't kept for them.
02170             $key = wfMemcKey( 'newtalk', 'ip', $id );
02171             $wgMemc->set( $key, $val ? 1 : 0, 1800 );
02172         }
02173         if ( $changed ) {
02174             $this->invalidateCache();
02175         }
02176     }
02177 
02183     private static function newTouchedTimestamp() {
02184         global $wgClockSkewFudge;
02185         return wfTimestamp( TS_MW, time() + $wgClockSkewFudge );
02186     }
02187 
02195     public function clearSharedCache() {
02196         $this->load();
02197         if ( $this->mId ) {
02198             global $wgMemc;
02199             $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) );
02200         }
02201     }
02202 
02208     public function invalidateCache() {
02209         if ( wfReadOnly() ) {
02210             return;
02211         }
02212         $this->load();
02213         if ( $this->mId ) {
02214             $this->mTouched = self::newTouchedTimestamp();
02215 
02216             $dbw = wfGetDB( DB_MASTER );
02217             $userid = $this->mId;
02218             $touched = $this->mTouched;
02219             $method = __METHOD__;
02220             $dbw->onTransactionIdle( function () use ( $dbw, $userid, $touched, $method ) {
02221                 // Prevent contention slams by checking user_touched first
02222                 $encTouched = $dbw->addQuotes( $dbw->timestamp( $touched ) );
02223                 $needsPurge = $dbw->selectField( 'user', '1',
02224                     array( 'user_id' => $userid, 'user_touched < ' . $encTouched ) );
02225                 if ( $needsPurge ) {
02226                     $dbw->update( 'user',
02227                         array( 'user_touched' => $dbw->timestamp( $touched ) ),
02228                         array( 'user_id' => $userid, 'user_touched < ' . $encTouched ),
02229                         $method
02230                     );
02231                 }
02232             } );
02233             $this->clearSharedCache();
02234         }
02235     }
02236 
02242     public function validateCache( $timestamp ) {
02243         $this->load();
02244         return ( $timestamp >= $this->mTouched );
02245     }
02246 
02251     public function getTouched() {
02252         $this->load();
02253         return $this->mTouched;
02254     }
02255 
02260     public function getPassword() {
02261         $this->loadPasswords();
02262 
02263         return $this->mPassword;
02264     }
02265 
02270     public function getTemporaryPassword() {
02271         $this->loadPasswords();
02272 
02273         return $this->mNewpassword;
02274     }
02275 
02292     public function setPassword( $str ) {
02293         global $wgAuth;
02294 
02295         $this->loadPasswords();
02296 
02297         if ( $str !== null ) {
02298             if ( !$wgAuth->allowPasswordChange() ) {
02299                 throw new PasswordError( wfMessage( 'password-change-forbidden' )->text() );
02300             }
02301 
02302             if ( !$this->isValidPassword( $str ) ) {
02303                 global $wgMinimalPasswordLength;
02304                 $valid = $this->getPasswordValidity( $str );
02305                 if ( is_array( $valid ) ) {
02306                     $message = array_shift( $valid );
02307                     $params = $valid;
02308                 } else {
02309                     $message = $valid;
02310                     $params = array( $wgMinimalPasswordLength );
02311                 }
02312                 throw new PasswordError( wfMessage( $message, $params )->text() );
02313             }
02314         }
02315 
02316         if ( !$wgAuth->setPassword( $this, $str ) ) {
02317             throw new PasswordError( wfMessage( 'externaldberror' )->text() );
02318         }
02319 
02320         $this->setInternalPassword( $str );
02321 
02322         return true;
02323     }
02324 
02332     public function setInternalPassword( $str ) {
02333         $this->setToken();
02334 
02335         $passwordFactory = self::getPasswordFactory();
02336         if ( $str === null ) {
02337             $this->mPassword = $passwordFactory->newFromCiphertext( null );
02338         } else {
02339             $this->mPassword = $passwordFactory->newFromPlaintext( $str );
02340         }
02341 
02342         $this->mNewpassword = $passwordFactory->newFromCiphertext( null );
02343         $this->mNewpassTime = null;
02344     }
02345 
02352     public function getToken( $forceCreation = true ) {
02353         $this->load();
02354         if ( !$this->mToken && $forceCreation ) {
02355             $this->setToken();
02356         }
02357         return $this->mToken;
02358     }
02359 
02366     public function setToken( $token = false ) {
02367         $this->load();
02368         if ( !$token ) {
02369             $this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
02370         } else {
02371             $this->mToken = $token;
02372         }
02373     }
02374 
02382     public function setNewpassword( $str, $throttle = true ) {
02383         $this->loadPasswords();
02384 
02385         if ( $str === null ) {
02386             $this->mNewpassword = '';
02387             $this->mNewpassTime = null;
02388         } else {
02389             $this->mNewpassword = self::getPasswordFactory()->newFromPlaintext( $str );
02390             if ( $throttle ) {
02391                 $this->mNewpassTime = wfTimestampNow();
02392             }
02393         }
02394     }
02395 
02401     public function isPasswordReminderThrottled() {
02402         global $wgPasswordReminderResendTime;
02403         $this->load();
02404         if ( !$this->mNewpassTime || !$wgPasswordReminderResendTime ) {
02405             return false;
02406         }
02407         $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600;
02408         return time() < $expiry;
02409     }
02410 
02415     public function getEmail() {
02416         $this->load();
02417         wfRunHooks( 'UserGetEmail', array( $this, &$this->mEmail ) );
02418         return $this->mEmail;
02419     }
02420 
02425     public function getEmailAuthenticationTimestamp() {
02426         $this->load();
02427         wfRunHooks( 'UserGetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
02428         return $this->mEmailAuthenticated;
02429     }
02430 
02435     public function setEmail( $str ) {
02436         $this->load();
02437         if ( $str == $this->mEmail ) {
02438             return;
02439         }
02440         $this->invalidateEmail();
02441         $this->mEmail = $str;
02442         wfRunHooks( 'UserSetEmail', array( $this, &$this->mEmail ) );
02443     }
02444 
02452     public function setEmailWithConfirmation( $str ) {
02453         global $wgEnableEmail, $wgEmailAuthentication;
02454 
02455         if ( !$wgEnableEmail ) {
02456             return Status::newFatal( 'emaildisabled' );
02457         }
02458 
02459         $oldaddr = $this->getEmail();
02460         if ( $str === $oldaddr ) {
02461             return Status::newGood( true );
02462         }
02463 
02464         $this->setEmail( $str );
02465 
02466         if ( $str !== '' && $wgEmailAuthentication ) {
02467             // Send a confirmation request to the new address if needed
02468             $type = $oldaddr != '' ? 'changed' : 'set';
02469             $result = $this->sendConfirmationMail( $type );
02470             if ( $result->isGood() ) {
02471                 // Say the the caller that a confirmation mail has been sent
02472                 $result->value = 'eauth';
02473             }
02474         } else {
02475             $result = Status::newGood( true );
02476         }
02477 
02478         return $result;
02479     }
02480 
02485     public function getRealName() {
02486         if ( !$this->isItemLoaded( 'realname' ) ) {
02487             $this->load();
02488         }
02489 
02490         return $this->mRealName;
02491     }
02492 
02497     public function setRealName( $str ) {
02498         $this->load();
02499         $this->mRealName = $str;
02500     }
02501 
02512     public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
02513         global $wgHiddenPrefs;
02514         $this->loadOptions();
02515 
02516         # We want 'disabled' preferences to always behave as the default value for
02517         # users, even if they have set the option explicitly in their settings (ie they
02518         # set it, and then it was disabled removing their ability to change it).  But
02519         # we don't want to erase the preferences in the database in case the preference
02520         # is re-enabled again.  So don't touch $mOptions, just override the returned value
02521         if ( !$ignoreHidden && in_array( $oname, $wgHiddenPrefs ) ) {
02522             return self::getDefaultOption( $oname );
02523         }
02524 
02525         if ( array_key_exists( $oname, $this->mOptions ) ) {
02526             return $this->mOptions[$oname];
02527         } else {
02528             return $defaultOverride;
02529         }
02530     }
02531 
02537     public function getOptions() {
02538         global $wgHiddenPrefs;
02539         $this->loadOptions();
02540         $options = $this->mOptions;
02541 
02542         # We want 'disabled' preferences to always behave as the default value for
02543         # users, even if they have set the option explicitly in their settings (ie they
02544         # set it, and then it was disabled removing their ability to change it).  But
02545         # we don't want to erase the preferences in the database in case the preference
02546         # is re-enabled again.  So don't touch $mOptions, just override the returned value
02547         foreach ( $wgHiddenPrefs as $pref ) {
02548             $default = self::getDefaultOption( $pref );
02549             if ( $default !== null ) {
02550                 $options[$pref] = $default;
02551             }
02552         }
02553 
02554         return $options;
02555     }
02556 
02564     public function getBoolOption( $oname ) {
02565         return (bool)$this->getOption( $oname );
02566     }
02567 
02576     public function getIntOption( $oname, $defaultOverride = 0 ) {
02577         $val = $this->getOption( $oname );
02578         if ( $val == '' ) {
02579             $val = $defaultOverride;
02580         }
02581         return intval( $val );
02582     }
02583 
02592     public function setOption( $oname, $val ) {
02593         $this->loadOptions();
02594 
02595         // Explicitly NULL values should refer to defaults
02596         if ( is_null( $val ) ) {
02597             $val = self::getDefaultOption( $oname );
02598         }
02599 
02600         $this->mOptions[$oname] = $val;
02601     }
02602 
02612     public function getTokenFromOption( $oname ) {
02613         global $wgHiddenPrefs;
02614         if ( in_array( $oname, $wgHiddenPrefs ) ) {
02615             return false;
02616         }
02617 
02618         $token = $this->getOption( $oname );
02619         if ( !$token ) {
02620             $token = $this->resetTokenFromOption( $oname );
02621             $this->saveSettings();
02622         }
02623         return $token;
02624     }
02625 
02635     public function resetTokenFromOption( $oname ) {
02636         global $wgHiddenPrefs;
02637         if ( in_array( $oname, $wgHiddenPrefs ) ) {
02638             return false;
02639         }
02640 
02641         $token = MWCryptRand::generateHex( 40 );
02642         $this->setOption( $oname, $token );
02643         return $token;
02644     }
02645 
02669     public static function listOptionKinds() {
02670         return array(
02671             'registered',
02672             'registered-multiselect',
02673             'registered-checkmatrix',
02674             'userjs',
02675             'special',
02676             'unused'
02677         );
02678     }
02679 
02692     public function getOptionKinds( IContextSource $context, $options = null ) {
02693         $this->loadOptions();
02694         if ( $options === null ) {
02695             $options = $this->mOptions;
02696         }
02697 
02698         $prefs = Preferences::getPreferences( $this, $context );
02699         $mapping = array();
02700 
02701         // Pull out the "special" options, so they don't get converted as
02702         // multiselect or checkmatrix.
02703         $specialOptions = array_fill_keys( Preferences::getSaveBlacklist(), true );
02704         foreach ( $specialOptions as $name => $value ) {
02705             unset( $prefs[$name] );
02706         }
02707 
02708         // Multiselect and checkmatrix options are stored in the database with
02709         // one key per option, each having a boolean value. Extract those keys.
02710         $multiselectOptions = array();
02711         foreach ( $prefs as $name => $info ) {
02712             if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
02713                     ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
02714                 $opts = HTMLFormField::flattenOptions( $info['options'] );
02715                 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
02716 
02717                 foreach ( $opts as $value ) {
02718                     $multiselectOptions["$prefix$value"] = true;
02719                 }
02720 
02721                 unset( $prefs[$name] );
02722             }
02723         }
02724         $checkmatrixOptions = array();
02725         foreach ( $prefs as $name => $info ) {
02726             if ( ( isset( $info['type'] ) && $info['type'] == 'checkmatrix' ) ||
02727                     ( isset( $info['class'] ) && $info['class'] == 'HTMLCheckMatrix' ) ) {
02728                 $columns = HTMLFormField::flattenOptions( $info['columns'] );
02729                 $rows = HTMLFormField::flattenOptions( $info['rows'] );
02730                 $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
02731 
02732                 foreach ( $columns as $column ) {
02733                     foreach ( $rows as $row ) {
02734                         $checkmatrixOptions["$prefix$column-$row"] = true;
02735                     }
02736                 }
02737 
02738                 unset( $prefs[$name] );
02739             }
02740         }
02741 
02742         // $value is ignored
02743         foreach ( $options as $key => $value ) {
02744             if ( isset( $prefs[$key] ) ) {
02745                 $mapping[$key] = 'registered';
02746             } elseif ( isset( $multiselectOptions[$key] ) ) {
02747                 $mapping[$key] = 'registered-multiselect';
02748             } elseif ( isset( $checkmatrixOptions[$key] ) ) {
02749                 $mapping[$key] = 'registered-checkmatrix';
02750             } elseif ( isset( $specialOptions[$key] ) ) {
02751                 $mapping[$key] = 'special';
02752             } elseif ( substr( $key, 0, 7 ) === 'userjs-' ) {
02753                 $mapping[$key] = 'userjs';
02754             } else {
02755                 $mapping[$key] = 'unused';
02756             }
02757         }
02758 
02759         return $mapping;
02760     }
02761 
02776     public function resetOptions(
02777         $resetKinds = array( 'registered', 'registered-multiselect', 'registered-checkmatrix', 'unused' ),
02778         IContextSource $context = null
02779     ) {
02780         $this->load();
02781         $defaultOptions = self::getDefaultOptions();
02782 
02783         if ( !is_array( $resetKinds ) ) {
02784             $resetKinds = array( $resetKinds );
02785         }
02786 
02787         if ( in_array( 'all', $resetKinds ) ) {
02788             $newOptions = $defaultOptions;
02789         } else {
02790             if ( $context === null ) {
02791                 $context = RequestContext::getMain();
02792             }
02793 
02794             $optionKinds = $this->getOptionKinds( $context );
02795             $resetKinds = array_intersect( $resetKinds, self::listOptionKinds() );
02796             $newOptions = array();
02797 
02798             // Use default values for the options that should be deleted, and
02799             // copy old values for the ones that shouldn't.
02800             foreach ( $this->mOptions as $key => $value ) {
02801                 if ( in_array( $optionKinds[$key], $resetKinds ) ) {
02802                     if ( array_key_exists( $key, $defaultOptions ) ) {
02803                         $newOptions[$key] = $defaultOptions[$key];
02804                     }
02805                 } else {
02806                     $newOptions[$key] = $value;
02807                 }
02808             }
02809         }
02810 
02811         wfRunHooks( 'UserResetAllOptions', array( $this, &$newOptions, $this->mOptions, $resetKinds ) );
02812 
02813         $this->mOptions = $newOptions;
02814         $this->mOptionsLoaded = true;
02815     }
02816 
02821     public function getDatePreference() {
02822         // Important migration for old data rows
02823         if ( is_null( $this->mDatePreference ) ) {
02824             global $wgLang;
02825             $value = $this->getOption( 'date' );
02826             $map = $wgLang->getDatePreferenceMigrationMap();
02827             if ( isset( $map[$value] ) ) {
02828                 $value = $map[$value];
02829             }
02830             $this->mDatePreference = $value;
02831         }
02832         return $this->mDatePreference;
02833     }
02834 
02841     public function requiresHTTPS() {
02842         global $wgSecureLogin;
02843         if ( !$wgSecureLogin ) {
02844             return false;
02845         } else {
02846             $https = $this->getBoolOption( 'prefershttps' );
02847             wfRunHooks( 'UserRequiresHTTPS', array( $this, &$https ) );
02848             if ( $https ) {
02849                 $https = wfCanIPUseHTTPS( $this->getRequest()->getIP() );
02850             }
02851             return $https;
02852         }
02853     }
02854 
02860     public function getStubThreshold() {
02861         global $wgMaxArticleSize; # Maximum article size, in Kb
02862         $threshold = $this->getIntOption( 'stubthreshold' );
02863         if ( $threshold > $wgMaxArticleSize * 1024 ) {
02864             // If they have set an impossible value, disable the preference
02865             // so we can use the parser cache again.
02866             $threshold = 0;
02867         }
02868         return $threshold;
02869     }
02870 
02875     public function getRights() {
02876         if ( is_null( $this->mRights ) ) {
02877             $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
02878             wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) );
02879             // Force reindexation of rights when a hook has unset one of them
02880             $this->mRights = array_values( array_unique( $this->mRights ) );
02881         }
02882         return $this->mRights;
02883     }
02884 
02890     public function getGroups() {
02891         $this->load();
02892         $this->loadGroups();
02893         return $this->mGroups;
02894     }
02895 
02903     public function getEffectiveGroups( $recache = false ) {
02904         if ( $recache || is_null( $this->mEffectiveGroups ) ) {
02905             wfProfileIn( __METHOD__ );
02906             $this->mEffectiveGroups = array_unique( array_merge(
02907                 $this->getGroups(), // explicit groups
02908                 $this->getAutomaticGroups( $recache ) // implicit groups
02909             ) );
02910             // Hook for additional groups
02911             wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
02912             // Force reindexation of groups when a hook has unset one of them
02913             $this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
02914             wfProfileOut( __METHOD__ );
02915         }
02916         return $this->mEffectiveGroups;
02917     }
02918 
02926     public function getAutomaticGroups( $recache = false ) {
02927         if ( $recache || is_null( $this->mImplicitGroups ) ) {
02928             wfProfileIn( __METHOD__ );
02929             $this->mImplicitGroups = array( '*' );
02930             if ( $this->getId() ) {
02931                 $this->mImplicitGroups[] = 'user';
02932 
02933                 $this->mImplicitGroups = array_unique( array_merge(
02934                     $this->mImplicitGroups,
02935                     Autopromote::getAutopromoteGroups( $this )
02936                 ) );
02937             }
02938             if ( $recache ) {
02939                 // Assure data consistency with rights/groups,
02940                 // as getEffectiveGroups() depends on this function
02941                 $this->mEffectiveGroups = null;
02942             }
02943             wfProfileOut( __METHOD__ );
02944         }
02945         return $this->mImplicitGroups;
02946     }
02947 
02957     public function getFormerGroups() {
02958         if ( is_null( $this->mFormerGroups ) ) {
02959             $dbr = wfGetDB( DB_MASTER );
02960             $res = $dbr->select( 'user_former_groups',
02961                 array( 'ufg_group' ),
02962                 array( 'ufg_user' => $this->mId ),
02963                 __METHOD__ );
02964             $this->mFormerGroups = array();
02965             foreach ( $res as $row ) {
02966                 $this->mFormerGroups[] = $row->ufg_group;
02967             }
02968         }
02969         return $this->mFormerGroups;
02970     }
02971 
02976     public function getEditCount() {
02977         if ( !$this->getId() ) {
02978             return null;
02979         }
02980 
02981         if ( $this->mEditCount === null ) {
02982             /* Populate the count, if it has not been populated yet */
02983             wfProfileIn( __METHOD__ );
02984             $dbr = wfGetDB( DB_SLAVE );
02985             // check if the user_editcount field has been initialized
02986             $count = $dbr->selectField(
02987                 'user', 'user_editcount',
02988                 array( 'user_id' => $this->mId ),
02989                 __METHOD__
02990             );
02991 
02992             if ( $count === null ) {
02993                 // it has not been initialized. do so.
02994                 $count = $this->initEditCount();
02995             }
02996             $this->mEditCount = $count;
02997             wfProfileOut( __METHOD__ );
02998         }
02999         return (int)$this->mEditCount;
03000     }
03001 
03007     public function addGroup( $group ) {
03008         if ( wfRunHooks( 'UserAddGroup', array( $this, &$group ) ) ) {
03009             $dbw = wfGetDB( DB_MASTER );
03010             if ( $this->getId() ) {
03011                 $dbw->insert( 'user_groups',
03012                     array(
03013                         'ug_user' => $this->getID(),
03014                         'ug_group' => $group,
03015                     ),
03016                     __METHOD__,
03017                     array( 'IGNORE' ) );
03018             }
03019         }
03020         $this->loadGroups();
03021         $this->mGroups[] = $group;
03022         // In case loadGroups was not called before, we now have the right twice.
03023         // Get rid of the duplicate.
03024         $this->mGroups = array_unique( $this->mGroups );
03025 
03026         // Refresh the groups caches, and clear the rights cache so it will be
03027         // refreshed on the next call to $this->getRights().
03028         $this->getEffectiveGroups( true );
03029         $this->mRights = null;
03030 
03031         $this->invalidateCache();
03032     }
03033 
03039     public function removeGroup( $group ) {
03040         $this->load();
03041         if ( wfRunHooks( 'UserRemoveGroup', array( $this, &$group ) ) ) {
03042             $dbw = wfGetDB( DB_MASTER );
03043             $dbw->delete( 'user_groups',
03044                 array(
03045                     'ug_user' => $this->getID(),
03046                     'ug_group' => $group,
03047                 ), __METHOD__ );
03048             // Remember that the user was in this group
03049             $dbw->insert( 'user_former_groups',
03050                 array(
03051                     'ufg_user' => $this->getID(),
03052                     'ufg_group' => $group,
03053                 ),
03054                 __METHOD__,
03055                 array( 'IGNORE' ) );
03056         }
03057         $this->loadGroups();
03058         $this->mGroups = array_diff( $this->mGroups, array( $group ) );
03059 
03060         // Refresh the groups caches, and clear the rights cache so it will be
03061         // refreshed on the next call to $this->getRights().
03062         $this->getEffectiveGroups( true );
03063         $this->mRights = null;
03064 
03065         $this->invalidateCache();
03066     }
03067 
03072     public function isLoggedIn() {
03073         return $this->getID() != 0;
03074     }
03075 
03080     public function isAnon() {
03081         return !$this->isLoggedIn();
03082     }
03083 
03090     public function isAllowedAny( /*...*/ ) {
03091         $permissions = func_get_args();
03092         foreach ( $permissions as $permission ) {
03093             if ( $this->isAllowed( $permission ) ) {
03094                 return true;
03095             }
03096         }
03097         return false;
03098     }
03099 
03105     public function isAllowedAll( /*...*/ ) {
03106         $permissions = func_get_args();
03107         foreach ( $permissions as $permission ) {
03108             if ( !$this->isAllowed( $permission ) ) {
03109                 return false;
03110             }
03111         }
03112         return true;
03113     }
03114 
03120     public function isAllowed( $action = '' ) {
03121         if ( $action === '' ) {
03122             return true; // In the spirit of DWIM
03123         }
03124         // Patrolling may not be enabled
03125         if ( $action === 'patrol' || $action === 'autopatrol' ) {
03126             global $wgUseRCPatrol, $wgUseNPPatrol;
03127             if ( !$wgUseRCPatrol && !$wgUseNPPatrol ) {
03128                 return false;
03129             }
03130         }
03131         // Use strict parameter to avoid matching numeric 0 accidentally inserted
03132         // by misconfiguration: 0 == 'foo'
03133         return in_array( $action, $this->getRights(), true );
03134     }
03135 
03140     public function useRCPatrol() {
03141         global $wgUseRCPatrol;
03142         return $wgUseRCPatrol && $this->isAllowedAny( 'patrol', 'patrolmarks' );
03143     }
03144 
03149     public function useNPPatrol() {
03150         global $wgUseRCPatrol, $wgUseNPPatrol;
03151         return (
03152             ( $wgUseRCPatrol || $wgUseNPPatrol )
03153                 && ( $this->isAllowedAny( 'patrol', 'patrolmarks' ) )
03154         );
03155     }
03156 
03162     public function getRequest() {
03163         if ( $this->mRequest ) {
03164             return $this->mRequest;
03165         } else {
03166             global $wgRequest;
03167             return $wgRequest;
03168         }
03169     }
03170 
03177     public function getSkin() {
03178         wfDeprecated( __METHOD__, '1.18' );
03179         return RequestContext::getMain()->getSkin();
03180     }
03181 
03191     public function getWatchedItem( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
03192         $key = $checkRights . ':' . $title->getNamespace() . ':' . $title->getDBkey();
03193 
03194         if ( isset( $this->mWatchedItems[$key] ) ) {
03195             return $this->mWatchedItems[$key];
03196         }
03197 
03198         if ( count( $this->mWatchedItems ) >= self::MAX_WATCHED_ITEMS_CACHE ) {
03199             $this->mWatchedItems = array();
03200         }
03201 
03202         $this->mWatchedItems[$key] = WatchedItem::fromUserTitle( $this, $title, $checkRights );
03203         return $this->mWatchedItems[$key];
03204     }
03205 
03214     public function isWatched( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
03215         return $this->getWatchedItem( $title, $checkRights )->isWatched();
03216     }
03217 
03225     public function addWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
03226         $this->getWatchedItem( $title, $checkRights )->addWatch();
03227         $this->invalidateCache();
03228     }
03229 
03237     public function removeWatch( $title, $checkRights = WatchedItem::CHECK_USER_RIGHTS ) {
03238         $this->getWatchedItem( $title, $checkRights )->removeWatch();
03239         $this->invalidateCache();
03240     }
03241 
03250     public function clearNotification( &$title, $oldid = 0 ) {
03251         global $wgUseEnotif, $wgShowUpdatedMarker;
03252 
03253         // Do nothing if the database is locked to writes
03254         if ( wfReadOnly() ) {
03255             return;
03256         }
03257 
03258         // Do nothing if not allowed to edit the watchlist
03259         if ( !$this->isAllowed( 'editmywatchlist' ) ) {
03260             return;
03261         }
03262 
03263         // If we're working on user's talk page, we should update the talk page message indicator
03264         if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
03265             if ( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this, $oldid ) ) ) {
03266                 return;
03267             }
03268 
03269             $nextid = $oldid ? $title->getNextRevisionID( $oldid ) : null;
03270 
03271             if ( !$oldid || !$nextid ) {
03272                 // If we're looking at the latest revision, we should definitely clear it
03273                 $this->setNewtalk( false );
03274             } else {
03275                 // Otherwise we should update its revision, if it's present
03276                 if ( $this->getNewtalk() ) {
03277                     // Naturally the other one won't clear by itself
03278                     $this->setNewtalk( false );
03279                     $this->setNewtalk( true, Revision::newFromId( $nextid ) );
03280                 }
03281             }
03282         }
03283 
03284         if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
03285             return;
03286         }
03287 
03288         if ( $this->isAnon() ) {
03289             // Nothing else to do...
03290             return;
03291         }
03292 
03293         // Only update the timestamp if the page is being watched.
03294         // The query to find out if it is watched is cached both in memcached and per-invocation,
03295         // and when it does have to be executed, it can be on a slave
03296         // If this is the user's newtalk page, we always update the timestamp
03297         $force = '';
03298         if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
03299             $force = 'force';
03300         }
03301 
03302         $this->getWatchedItem( $title )->resetNotificationTimestamp( $force, $oldid );
03303     }
03304 
03311     public function clearAllNotifications() {
03312         if ( wfReadOnly() ) {
03313             return;
03314         }
03315 
03316         // Do nothing if not allowed to edit the watchlist
03317         if ( !$this->isAllowed( 'editmywatchlist' ) ) {
03318             return;
03319         }
03320 
03321         global $wgUseEnotif, $wgShowUpdatedMarker;
03322         if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) {
03323             $this->setNewtalk( false );
03324             return;
03325         }
03326         $id = $this->getId();
03327         if ( $id != 0 ) {
03328             $dbw = wfGetDB( DB_MASTER );
03329             $dbw->update( 'watchlist',
03330                 array( /* SET */ 'wl_notificationtimestamp' => null ),
03331                 array( /* WHERE */ 'wl_user' => $id ),
03332                 __METHOD__
03333             );
03334             // We also need to clear here the "you have new message" notification for the own user_talk page;
03335             // it's cleared one page view later in WikiPage::doViewUpdates().
03336         }
03337     }
03338 
03352     protected function setCookie( $name, $value, $exp = 0, $secure = null, $params = array() ) {
03353         $params['secure'] = $secure;
03354         $this->getRequest()->response()->setcookie( $name, $value, $exp, $params );
03355     }
03356 
03366     protected function clearCookie( $name, $secure = null, $params = array() ) {
03367         $this->setCookie( $name, '', time() - 86400, $secure, $params );
03368     }
03369 
03378     public function setCookies( $request = null, $secure = null, $rememberMe = false ) {
03379         if ( $request === null ) {
03380             $request = $this->getRequest();
03381         }
03382 
03383         $this->load();
03384         if ( 0 == $this->mId ) {
03385             return;
03386         }
03387         if ( !$this->mToken ) {
03388             // When token is empty or NULL generate a new one and then save it to the database
03389             // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
03390             // Simply by setting every cell in the user_token column to NULL and letting them be
03391             // regenerated as users log back into the wiki.
03392             $this->setToken();
03393             $this->saveSettings();
03394         }
03395         $session = array(
03396             'wsUserID' => $this->mId,
03397             'wsToken' => $this->mToken,
03398             'wsUserName' => $this->getName()
03399         );
03400         $cookies = array(
03401             'UserID' => $this->mId,
03402             'UserName' => $this->getName(),
03403         );
03404         if ( $rememberMe ) {
03405             $cookies['Token'] = $this->mToken;
03406         } else {
03407             $cookies['Token'] = false;
03408         }
03409 
03410         wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
03411 
03412         foreach ( $session as $name => $value ) {
03413             $request->setSessionData( $name, $value );
03414         }
03415         foreach ( $cookies as $name => $value ) {
03416             if ( $value === false ) {
03417                 $this->clearCookie( $name );
03418             } else {
03419                 $this->setCookie( $name, $value, 0, $secure );
03420             }
03421         }
03422 
03430         if ( $request->getCheck( 'wpStickHTTPS' ) || $this->requiresHTTPS() ) {
03431             $this->setCookie(
03432                 'forceHTTPS',
03433                 'true',
03434                 $rememberMe ? 0 : null,
03435                 false,
03436                 array( 'prefix' => '' ) // no prefix
03437             );
03438         }
03439     }
03440 
03444     public function logout() {
03445         if ( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
03446             $this->doLogout();
03447         }
03448     }
03449 
03454     public function doLogout() {
03455         $this->clearInstanceCache( 'defaults' );
03456 
03457         $this->getRequest()->setSessionData( 'wsUserID', 0 );
03458 
03459         $this->clearCookie( 'UserID' );
03460         $this->clearCookie( 'Token' );
03461         $this->clearCookie( 'forceHTTPS', false, array( 'prefix' => '' ) );
03462 
03463         // Remember when user logged out, to prevent seeing cached pages
03464         $this->setCookie( 'LoggedOut', time(), time() + 86400 );
03465     }
03466 
03471     public function saveSettings() {
03472         global $wgAuth;
03473 
03474         $this->load();
03475         $this->loadPasswords();
03476         if ( wfReadOnly() ) {
03477             return;
03478         }
03479         if ( 0 == $this->mId ) {
03480             return;
03481         }
03482 
03483         $this->mTouched = self::newTouchedTimestamp();
03484         if ( !$wgAuth->allowSetLocalPassword() ) {
03485             $this->mPassword = self::getPasswordFactory()->newFromCiphertext( null );
03486         }
03487 
03488         $dbw = wfGetDB( DB_MASTER );
03489         $dbw->update( 'user',
03490             array( /* SET */
03491                 'user_name' => $this->mName,
03492                 'user_password' => $this->mPassword->toString(),
03493                 'user_newpassword' => $this->mNewpassword->toString(),
03494                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
03495                 'user_real_name' => $this->mRealName,
03496                 'user_email' => $this->mEmail,
03497                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
03498                 'user_touched' => $dbw->timestamp( $this->mTouched ),
03499                 'user_token' => strval( $this->mToken ),
03500                 'user_email_token' => $this->mEmailToken,
03501                 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
03502                 'user_password_expires' => $dbw->timestampOrNull( $this->mPasswordExpires ),
03503             ), array( /* WHERE */
03504                 'user_id' => $this->mId
03505             ), __METHOD__
03506         );
03507 
03508         $this->saveOptions();
03509 
03510         wfRunHooks( 'UserSaveSettings', array( $this ) );
03511         $this->clearSharedCache();
03512         $this->getUserPage()->invalidateCache();
03513     }
03514 
03519     public function idForName() {
03520         $s = trim( $this->getName() );
03521         if ( $s === '' ) {
03522             return 0;
03523         }
03524 
03525         $dbr = wfGetDB( DB_SLAVE );
03526         $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ );
03527         if ( $id === false ) {
03528             $id = 0;
03529         }
03530         return $id;
03531     }
03532 
03552     public static function createNew( $name, $params = array() ) {
03553         $user = new User;
03554         $user->load();
03555         $user->loadPasswords();
03556         $user->setToken(); // init token
03557         if ( isset( $params['options'] ) ) {
03558             $user->mOptions = $params['options'] + (array)$user->mOptions;
03559             unset( $params['options'] );
03560         }
03561         $dbw = wfGetDB( DB_MASTER );
03562         $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
03563 
03564         $fields = array(
03565             'user_id' => $seqVal,
03566             'user_name' => $name,
03567             'user_password' => $user->mPassword->toString(),
03568             'user_newpassword' => $user->mNewpassword->toString(),
03569             'user_newpass_time' => $dbw->timestampOrNull( $user->mNewpassTime ),
03570             'user_email' => $user->mEmail,
03571             'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
03572             'user_real_name' => $user->mRealName,
03573             'user_token' => strval( $user->mToken ),
03574             'user_registration' => $dbw->timestamp( $user->mRegistration ),
03575             'user_editcount' => 0,
03576             'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ),
03577         );
03578         foreach ( $params as $name => $value ) {
03579             $fields["user_$name"] = $value;
03580         }
03581         $dbw->insert( 'user', $fields, __METHOD__, array( 'IGNORE' ) );
03582         if ( $dbw->affectedRows() ) {
03583             $newUser = User::newFromId( $dbw->insertId() );
03584         } else {
03585             $newUser = null;
03586         }
03587         return $newUser;
03588     }
03589 
03616     public function addToDatabase() {
03617         $this->load();
03618         $this->loadPasswords();
03619         if ( !$this->mToken ) {
03620             $this->setToken(); // init token
03621         }
03622 
03623         $this->mTouched = self::newTouchedTimestamp();
03624 
03625         $dbw = wfGetDB( DB_MASTER );
03626         $inWrite = $dbw->writesOrCallbacksPending();
03627         $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
03628         $dbw->insert( 'user',
03629             array(
03630                 'user_id' => $seqVal,
03631                 'user_name' => $this->mName,
03632                 'user_password' => $this->mPassword->toString(),
03633                 'user_newpassword' => $this->mNewpassword->toString(),
03634                 'user_newpass_time' => $dbw->timestampOrNull( $this->mNewpassTime ),
03635                 'user_email' => $this->mEmail,
03636                 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
03637                 'user_real_name' => $this->mRealName,
03638                 'user_token' => strval( $this->mToken ),
03639                 'user_registration' => $dbw->timestamp( $this->mRegistration ),
03640                 'user_editcount' => 0,
03641                 'user_touched' => $dbw->timestamp( $this->mTouched ),
03642             ), __METHOD__,
03643             array( 'IGNORE' )
03644         );
03645         if ( !$dbw->affectedRows() ) {
03646             // The queries below cannot happen in the same REPEATABLE-READ snapshot.
03647             // Handle this by COMMIT, if possible, or by LOCK IN SHARE MODE otherwise.
03648             if ( $inWrite ) {
03649                 // Can't commit due to pending writes that may need atomicity.
03650                 // This may cause some lock contention unlike the case below.
03651                 $options = array( 'LOCK IN SHARE MODE' );
03652                 $flags = self::READ_LOCKING;
03653             } else {
03654                 // Often, this case happens early in views before any writes when
03655                 // using CentralAuth. It's should be OK to commit and break the snapshot.
03656                 $dbw->commit( __METHOD__, 'flush' );
03657                 $options = array();
03658                 $flags = 0;
03659             }
03660             $this->mId = $dbw->selectField( 'user', 'user_id',
03661                 array( 'user_name' => $this->mName ), __METHOD__, $options );
03662             $loaded = false;
03663             if ( $this->mId ) {
03664                 if ( $this->loadFromDatabase( $flags ) ) {
03665                     $loaded = true;
03666                 }
03667             }
03668             if ( !$loaded ) {
03669                 throw new MWException( __METHOD__ . ": hit a key conflict attempting " .
03670                     "to insert user '{$this->mName}' row, but it was not present in select!" );
03671             }
03672             return Status::newFatal( 'userexists' );
03673         }
03674         $this->mId = $dbw->insertId();
03675 
03676         // Clear instance cache other than user table data, which is already accurate
03677         $this->clearInstanceCache();
03678 
03679         $this->saveOptions();
03680         return Status::newGood();
03681     }
03682 
03688     public function spreadAnyEditBlock() {
03689         if ( $this->isLoggedIn() && $this->isBlocked() ) {
03690             return $this->spreadBlock();
03691         }
03692         return false;
03693     }
03694 
03700     protected function spreadBlock() {
03701         wfDebug( __METHOD__ . "()\n" );
03702         $this->load();
03703         if ( $this->mId == 0 ) {
03704             return false;
03705         }
03706 
03707         $userblock = Block::newFromTarget( $this->getName() );
03708         if ( !$userblock ) {
03709             return false;
03710         }
03711 
03712         return (bool)$userblock->doAutoblock( $this->getRequest()->getIP() );
03713     }
03714 
03719     public function isBlockedFromCreateAccount() {
03720         $this->getBlockedStatus();
03721         if ( $this->mBlock && $this->mBlock->prevents( 'createaccount' ) ) {
03722             return $this->mBlock;
03723         }
03724 
03725         # bug 13611: if the IP address the user is trying to create an account from is
03726         # blocked with createaccount disabled, prevent new account creation there even
03727         # when the user is logged in
03728         if ( $this->mBlockedFromCreateAccount === false && !$this->isAllowed( 'ipblock-exempt' ) ) {
03729             $this->mBlockedFromCreateAccount = Block::newFromTarget( null, $this->getRequest()->getIP() );
03730         }
03731         return $this->mBlockedFromCreateAccount instanceof Block
03732             && $this->mBlockedFromCreateAccount->prevents( 'createaccount' )
03733             ? $this->mBlockedFromCreateAccount
03734             : false;
03735     }
03736 
03741     public function isBlockedFromEmailuser() {
03742         $this->getBlockedStatus();
03743         return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
03744     }
03745 
03750     public function isAllowedToCreateAccount() {
03751         return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount();
03752     }
03753 
03759     public function getUserPage() {
03760         return Title::makeTitle( NS_USER, $this->getName() );
03761     }
03762 
03768     public function getTalkPage() {
03769         $title = $this->getUserPage();
03770         return $title->getTalkPage();
03771     }
03772 
03778     public function isNewbie() {
03779         return !$this->isAllowed( 'autoconfirmed' );
03780     }
03781 
03787     public function checkPassword( $password ) {
03788         global $wgAuth, $wgLegacyEncoding;
03789 
03790         $section = new ProfileSection( __METHOD__ );
03791 
03792         $this->loadPasswords();
03793 
03794         // Certain authentication plugins do NOT want to save
03795         // domain passwords in a mysql database, so we should
03796         // check this (in case $wgAuth->strict() is false).
03797         if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
03798             return true;
03799         } elseif ( $wgAuth->strict() ) {
03800             // Auth plugin doesn't allow local authentication
03801             return false;
03802         } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
03803             // Auth plugin doesn't allow local authentication for this user name
03804             return false;
03805         }
03806 
03807         $passwordFactory = self::getPasswordFactory();
03808         if ( !$this->mPassword->equals( $password ) ) {
03809             if ( $wgLegacyEncoding ) {
03810                 // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
03811                 // Check for this with iconv
03812                 $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
03813                 if ( $cp1252Password === $password || !$this->mPassword->equals( $cp1252Password ) ) {
03814                     return false;
03815                 }
03816             } else {
03817                 return false;
03818             }
03819         }
03820 
03821         if ( $passwordFactory->needsUpdate( $this->mPassword ) ) {
03822             $this->mPassword = $passwordFactory->newFromPlaintext( $password );
03823             $this->saveSettings();
03824         }
03825 
03826         return true;
03827     }
03828 
03837     public function checkTemporaryPassword( $plaintext ) {
03838         global $wgNewPasswordExpiry;
03839 
03840         $this->load();
03841         $this->loadPasswords();
03842         if ( $this->mNewpassword->equals( $plaintext ) ) {
03843             if ( is_null( $this->mNewpassTime ) ) {
03844                 return true;
03845             }
03846             $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry;
03847             return ( time() < $expiry );
03848         } else {
03849             return false;
03850         }
03851     }
03852 
03861     public function editToken( $salt = '', $request = null ) {
03862         wfDeprecated( __METHOD__, '1.19' );
03863         return $this->getEditToken( $salt, $request );
03864     }
03865 
03878     public function getEditToken( $salt = '', $request = null ) {
03879         if ( $request == null ) {
03880             $request = $this->getRequest();
03881         }
03882 
03883         if ( $this->isAnon() ) {
03884             return self::EDIT_TOKEN_SUFFIX;
03885         } else {
03886             $token = $request->getSessionData( 'wsEditToken' );
03887             if ( $token === null ) {
03888                 $token = MWCryptRand::generateHex( 32 );
03889                 $request->setSessionData( 'wsEditToken', $token );
03890             }
03891             if ( is_array( $salt ) ) {
03892                 $salt = implode( '|', $salt );
03893             }
03894             return md5( $token . $salt ) . self::EDIT_TOKEN_SUFFIX;
03895         }
03896     }
03897 
03905     public static function generateToken() {
03906         return MWCryptRand::generateHex( 32 );
03907     }
03908 
03920     public function matchEditToken( $val, $salt = '', $request = null ) {
03921         $sessionToken = $this->getEditToken( $salt, $request );
03922         if ( $val != $sessionToken ) {
03923             wfDebug( "User::matchEditToken: broken session data\n" );
03924         }
03925 
03926         return $val == $sessionToken;
03927     }
03928 
03938     public function matchEditTokenNoSuffix( $val, $salt = '', $request = null ) {
03939         $sessionToken = $this->getEditToken( $salt, $request );
03940         return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 );
03941     }
03942 
03950     public function sendConfirmationMail( $type = 'created' ) {
03951         global $wgLang;
03952         $expiration = null; // gets passed-by-ref and defined in next line.
03953         $token = $this->confirmationToken( $expiration );
03954         $url = $this->confirmationTokenUrl( $token );
03955         $invalidateURL = $this->invalidationTokenUrl( $token );
03956         $this->saveSettings();
03957 
03958         if ( $type == 'created' || $type === false ) {
03959             $message = 'confirmemail_body';
03960         } elseif ( $type === true ) {
03961             $message = 'confirmemail_body_changed';
03962         } else {
03963             // Messages: confirmemail_body_changed, confirmemail_body_set
03964             $message = 'confirmemail_body_' . $type;
03965         }
03966 
03967         return $this->sendMail( wfMessage( 'confirmemail_subject' )->text(),
03968             wfMessage( $message,
03969                 $this->getRequest()->getIP(),
03970                 $this->getName(),
03971                 $url,
03972                 $wgLang->timeanddate( $expiration, false ),
03973                 $invalidateURL,
03974                 $wgLang->date( $expiration, false ),
03975                 $wgLang->time( $expiration, false ) )->text() );
03976     }
03977 
03989     public function sendMail( $subject, $body, $from = null, $replyto = null ) {
03990         if ( is_null( $from ) ) {
03991             global $wgPasswordSender;
03992             $sender = new MailAddress( $wgPasswordSender,
03993                 wfMessage( 'emailsender' )->inContentLanguage()->text() );
03994         } else {
03995             $sender = MailAddress::newFromUser( $from );
03996         }
03997 
03998         $to = MailAddress::newFromUser( $this );
03999         return UserMailer::send( $to, $sender, $subject, $body, $replyto );
04000     }
04001 
04012     protected function confirmationToken( &$expiration ) {
04013         global $wgUserEmailConfirmationTokenExpiry;
04014         $now = time();
04015         $expires = $now + $wgUserEmailConfirmationTokenExpiry;
04016         $expiration = wfTimestamp( TS_MW, $expires );
04017         $this->load();
04018         $token = MWCryptRand::generateHex( 32 );
04019         $hash = md5( $token );
04020         $this->mEmailToken = $hash;
04021         $this->mEmailTokenExpires = $expiration;
04022         return $token;
04023     }
04024 
04030     protected function confirmationTokenUrl( $token ) {
04031         return $this->getTokenUrl( 'ConfirmEmail', $token );
04032     }
04033 
04039     protected function invalidationTokenUrl( $token ) {
04040         return $this->getTokenUrl( 'InvalidateEmail', $token );
04041     }
04042 
04057     protected function getTokenUrl( $page, $token ) {
04058         // Hack to bypass localization of 'Special:'
04059         $title = Title::makeTitle( NS_MAIN, "Special:$page/$token" );
04060         return $title->getCanonicalURL();
04061     }
04062 
04070     public function confirmEmail() {
04071         // Check if it's already confirmed, so we don't touch the database
04072         // and fire the ConfirmEmailComplete hook on redundant confirmations.
04073         if ( !$this->isEmailConfirmed() ) {
04074             $this->setEmailAuthenticationTimestamp( wfTimestampNow() );
04075             wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
04076         }
04077         return true;
04078     }
04079 
04087     public function invalidateEmail() {
04088         $this->load();
04089         $this->mEmailToken = null;
04090         $this->mEmailTokenExpires = null;
04091         $this->setEmailAuthenticationTimestamp( null );
04092         $this->mEmail = '';
04093         wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
04094         return true;
04095     }
04096 
04101     public function setEmailAuthenticationTimestamp( $timestamp ) {
04102         $this->load();
04103         $this->mEmailAuthenticated = $timestamp;
04104         wfRunHooks( 'UserSetEmailAuthenticationTimestamp', array( $this, &$this->mEmailAuthenticated ) );
04105     }
04106 
04112     public function canSendEmail() {
04113         global $wgEnableEmail, $wgEnableUserEmail;
04114         if ( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
04115             return false;
04116         }
04117         $canSend = $this->isEmailConfirmed();
04118         wfRunHooks( 'UserCanSendEmail', array( &$this, &$canSend ) );
04119         return $canSend;
04120     }
04121 
04127     public function canReceiveEmail() {
04128         return $this->isEmailConfirmed() && !$this->getOption( 'disablemail' );
04129     }
04130 
04141     public function isEmailConfirmed() {
04142         global $wgEmailAuthentication;
04143         $this->load();
04144         $confirmed = true;
04145         if ( wfRunHooks( 'EmailConfirmed', array( &$this, &$confirmed ) ) ) {
04146             if ( $this->isAnon() ) {
04147                 return false;
04148             }
04149             if ( !Sanitizer::validateEmail( $this->mEmail ) ) {
04150                 return false;
04151             }
04152             if ( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() ) {
04153                 return false;
04154             }
04155             return true;
04156         } else {
04157             return $confirmed;
04158         }
04159     }
04160 
04165     public function isEmailConfirmationPending() {
04166         global $wgEmailAuthentication;
04167         return $wgEmailAuthentication &&
04168             !$this->isEmailConfirmed() &&
04169             $this->mEmailToken &&
04170             $this->mEmailTokenExpires > wfTimestamp();
04171     }
04172 
04180     public function getRegistration() {
04181         if ( $this->isAnon() ) {
04182             return false;
04183         }
04184         $this->load();
04185         return $this->mRegistration;
04186     }
04187 
04194     public function getFirstEditTimestamp() {
04195         if ( $this->getId() == 0 ) {
04196             return false; // anons
04197         }
04198         $dbr = wfGetDB( DB_SLAVE );
04199         $time = $dbr->selectField( 'revision', 'rev_timestamp',
04200             array( 'rev_user' => $this->getId() ),
04201             __METHOD__,
04202             array( 'ORDER BY' => 'rev_timestamp ASC' )
04203         );
04204         if ( !$time ) {
04205             return false; // no edits
04206         }
04207         return wfTimestamp( TS_MW, $time );
04208     }
04209 
04216     public static function getGroupPermissions( $groups ) {
04217         global $wgGroupPermissions, $wgRevokePermissions;
04218         $rights = array();
04219         // grant every granted permission first
04220         foreach ( $groups as $group ) {
04221             if ( isset( $wgGroupPermissions[$group] ) ) {
04222                 $rights = array_merge( $rights,
04223                     // array_filter removes empty items
04224                     array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
04225             }
04226         }
04227         // now revoke the revoked permissions
04228         foreach ( $groups as $group ) {
04229             if ( isset( $wgRevokePermissions[$group] ) ) {
04230                 $rights = array_diff( $rights,
04231                     array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
04232             }
04233         }
04234         return array_unique( $rights );
04235     }
04236 
04243     public static function getGroupsWithPermission( $role ) {
04244         global $wgGroupPermissions;
04245         $allowedGroups = array();
04246         foreach ( array_keys( $wgGroupPermissions ) as $group ) {
04247             if ( self::groupHasPermission( $group, $role ) ) {
04248                 $allowedGroups[] = $group;
04249             }
04250         }
04251         return $allowedGroups;
04252     }
04253 
04266     public static function groupHasPermission( $group, $role ) {
04267         global $wgGroupPermissions, $wgRevokePermissions;
04268         return isset( $wgGroupPermissions[$group][$role] ) && $wgGroupPermissions[$group][$role]
04269             && !( isset( $wgRevokePermissions[$group][$role] ) && $wgRevokePermissions[$group][$role] );
04270     }
04271 
04279     public static function isEveryoneAllowed( $right ) {
04280         global $wgGroupPermissions, $wgRevokePermissions;
04281         static $cache = array();
04282 
04283         // Use the cached results, except in unit tests which rely on
04284         // being able change the permission mid-request
04285         if ( isset( $cache[$right] ) && !defined( 'MW_PHPUNIT_TEST' ) ) {
04286             return $cache[$right];
04287         }
04288 
04289         if ( !isset( $wgGroupPermissions['*'][$right] ) || !$wgGroupPermissions['*'][$right] ) {
04290             $cache[$right] = false;
04291             return false;
04292         }
04293 
04294         // If it's revoked anywhere, then everyone doesn't have it
04295         foreach ( $wgRevokePermissions as $rights ) {
04296             if ( isset( $rights[$right] ) && $rights[$right] ) {
04297                 $cache[$right] = false;
04298                 return false;
04299             }
04300         }
04301 
04302         // Allow extensions (e.g. OAuth) to say false
04303         if ( !wfRunHooks( 'UserIsEveryoneAllowed', array( $right ) ) ) {
04304             $cache[$right] = false;
04305             return false;
04306         }
04307 
04308         $cache[$right] = true;
04309         return true;
04310     }
04311 
04318     public static function getGroupName( $group ) {
04319         $msg = wfMessage( "group-$group" );
04320         return $msg->isBlank() ? $group : $msg->text();
04321     }
04322 
04330     public static function getGroupMember( $group, $username = '#' ) {
04331         $msg = wfMessage( "group-$group-member", $username );
04332         return $msg->isBlank() ? $group : $msg->text();
04333     }
04334 
04341     public static function getAllGroups() {
04342         global $wgGroupPermissions, $wgRevokePermissions;
04343         return array_diff(
04344             array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
04345             self::getImplicitGroups()
04346         );
04347     }
04348 
04353     public static function getAllRights() {
04354         if ( self::$mAllRights === false ) {
04355             global $wgAvailableRights;
04356             if ( count( $wgAvailableRights ) ) {
04357                 self::$mAllRights = array_unique( array_merge( self::$mCoreRights, $wgAvailableRights ) );
04358             } else {
04359                 self::$mAllRights = self::$mCoreRights;
04360             }
04361             wfRunHooks( 'UserGetAllRights', array( &self::$mAllRights ) );
04362         }
04363         return self::$mAllRights;
04364     }
04365 
04370     public static function getImplicitGroups() {
04371         global $wgImplicitGroups;
04372 
04373         $groups = $wgImplicitGroups;
04374         # Deprecated, use $wgImplictGroups instead
04375         wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) );
04376 
04377         return $groups;
04378     }
04379 
04386     public static function getGroupPage( $group ) {
04387         $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
04388         if ( $msg->exists() ) {
04389             $title = Title::newFromText( $msg->text() );
04390             if ( is_object( $title ) ) {
04391                 return $title;
04392             }
04393         }
04394         return false;
04395     }
04396 
04405     public static function makeGroupLinkHTML( $group, $text = '' ) {
04406         if ( $text == '' ) {
04407             $text = self::getGroupName( $group );
04408         }
04409         $title = self::getGroupPage( $group );
04410         if ( $title ) {
04411             return Linker::link( $title, htmlspecialchars( $text ) );
04412         } else {
04413             return $text;
04414         }
04415     }
04416 
04425     public static function makeGroupLinkWiki( $group, $text = '' ) {
04426         if ( $text == '' ) {
04427             $text = self::getGroupName( $group );
04428         }
04429         $title = self::getGroupPage( $group );
04430         if ( $title ) {
04431             $page = $title->getPrefixedText();
04432             return "[[$page|$text]]";
04433         } else {
04434             return $text;
04435         }
04436     }
04437 
04447     public static function changeableByGroup( $group ) {
04448         global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
04449 
04450         $groups = array(
04451             'add' => array(),
04452             'remove' => array(),
04453             'add-self' => array(),
04454             'remove-self' => array()
04455         );
04456 
04457         if ( empty( $wgAddGroups[$group] ) ) {
04458             // Don't add anything to $groups
04459         } elseif ( $wgAddGroups[$group] === true ) {
04460             // You get everything
04461             $groups['add'] = self::getAllGroups();
04462         } elseif ( is_array( $wgAddGroups[$group] ) ) {
04463             $groups['add'] = $wgAddGroups[$group];
04464         }
04465 
04466         // Same thing for remove
04467         if ( empty( $wgRemoveGroups[$group] ) ) {
04468         } elseif ( $wgRemoveGroups[$group] === true ) {
04469             $groups['remove'] = self::getAllGroups();
04470         } elseif ( is_array( $wgRemoveGroups[$group] ) ) {
04471             $groups['remove'] = $wgRemoveGroups[$group];
04472         }
04473 
04474         // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
04475         if ( empty( $wgGroupsAddToSelf['user'] ) || $wgGroupsAddToSelf['user'] !== true ) {
04476             foreach ( $wgGroupsAddToSelf as $key => $value ) {
04477                 if ( is_int( $key ) ) {
04478                     $wgGroupsAddToSelf['user'][] = $value;
04479                 }
04480             }
04481         }
04482 
04483         if ( empty( $wgGroupsRemoveFromSelf['user'] ) || $wgGroupsRemoveFromSelf['user'] !== true ) {
04484             foreach ( $wgGroupsRemoveFromSelf as $key => $value ) {
04485                 if ( is_int( $key ) ) {
04486                     $wgGroupsRemoveFromSelf['user'][] = $value;
04487                 }
04488             }
04489         }
04490 
04491         // Now figure out what groups the user can add to him/herself
04492         if ( empty( $wgGroupsAddToSelf[$group] ) ) {
04493         } elseif ( $wgGroupsAddToSelf[$group] === true ) {
04494             // No idea WHY this would be used, but it's there
04495             $groups['add-self'] = User::getAllGroups();
04496         } elseif ( is_array( $wgGroupsAddToSelf[$group] ) ) {
04497             $groups['add-self'] = $wgGroupsAddToSelf[$group];
04498         }
04499 
04500         if ( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
04501         } elseif ( $wgGroupsRemoveFromSelf[$group] === true ) {
04502             $groups['remove-self'] = User::getAllGroups();
04503         } elseif ( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
04504             $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
04505         }
04506 
04507         return $groups;
04508     }
04509 
04517     public function changeableGroups() {
04518         if ( $this->isAllowed( 'userrights' ) ) {
04519             // This group gives the right to modify everything (reverse-
04520             // compatibility with old "userrights lets you change
04521             // everything")
04522             // Using array_merge to make the groups reindexed
04523             $all = array_merge( User::getAllGroups() );
04524             return array(
04525                 'add' => $all,
04526                 'remove' => $all,
04527                 'add-self' => array(),
04528                 'remove-self' => array()
04529             );
04530         }
04531 
04532         // Okay, it's not so simple, we will have to go through the arrays
04533         $groups = array(
04534             'add' => array(),
04535             'remove' => array(),
04536             'add-self' => array(),
04537             'remove-self' => array()
04538         );
04539         $addergroups = $this->getEffectiveGroups();
04540 
04541         foreach ( $addergroups as $addergroup ) {
04542             $groups = array_merge_recursive(
04543                 $groups, $this->changeableByGroup( $addergroup )
04544             );
04545             $groups['add'] = array_unique( $groups['add'] );
04546             $groups['remove'] = array_unique( $groups['remove'] );
04547             $groups['add-self'] = array_unique( $groups['add-self'] );
04548             $groups['remove-self'] = array_unique( $groups['remove-self'] );
04549         }
04550         return $groups;
04551     }
04552 
04557     public function incEditCount() {
04558         if ( !$this->isAnon() ) {
04559             $dbw = wfGetDB( DB_MASTER );
04560             $dbw->update(
04561                 'user',
04562                 array( 'user_editcount=user_editcount+1' ),
04563                 array( 'user_id' => $this->getId() ),
04564                 __METHOD__
04565             );
04566 
04567             // Lazy initialization check...
04568             if ( $dbw->affectedRows() == 0 ) {
04569                 // Now here's a goddamn hack...
04570                 $dbr = wfGetDB( DB_SLAVE );
04571                 if ( $dbr !== $dbw ) {
04572                     // If we actually have a slave server, the count is
04573                     // at least one behind because the current transaction
04574                     // has not been committed and replicated.
04575                     $this->initEditCount( 1 );
04576                 } else {
04577                     // But if DB_SLAVE is selecting the master, then the
04578                     // count we just read includes the revision that was
04579                     // just added in the working transaction.
04580                     $this->initEditCount();
04581                 }
04582             }
04583         }
04584         // edit count in user cache too
04585         $this->invalidateCache();
04586     }
04587 
04594     protected function initEditCount( $add = 0 ) {
04595         // Pull from a slave to be less cruel to servers
04596         // Accuracy isn't the point anyway here
04597         $dbr = wfGetDB( DB_SLAVE );
04598         $count = (int)$dbr->selectField(
04599             'revision',
04600             'COUNT(rev_user)',
04601             array( 'rev_user' => $this->getId() ),
04602             __METHOD__
04603         );
04604         $count = $count + $add;
04605 
04606         $dbw = wfGetDB( DB_MASTER );
04607         $dbw->update(
04608             'user',
04609             array( 'user_editcount' => $count ),
04610             array( 'user_id' => $this->getId() ),
04611             __METHOD__
04612         );
04613 
04614         return $count;
04615     }
04616 
04623     public static function getRightDescription( $right ) {
04624         $key = "right-$right";
04625         $msg = wfMessage( $key );
04626         return $msg->isBlank() ? $right : $msg->text();
04627     }
04628 
04638     public static function crypt( $password, $salt = false ) {
04639         wfDeprecated( __METHOD__, '1.24' );
04640         $hash = self::getPasswordFactory()->newFromPlaintext( $password );
04641         return $hash->toString();
04642     }
04643 
04655     public static function comparePasswords( $hash, $password, $userId = false ) {
04656         wfDeprecated( __METHOD__, '1.24' );
04657 
04658         // Check for *really* old password hashes that don't even have a type
04659         // The old hash format was just an md5 hex hash, with no type information
04660         if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) {
04661             global $wgPasswordSalt;
04662             if ( $wgPasswordSalt ) {
04663                 $password = ":B:{$userId}:{$hash}";
04664             } else {
04665                 $password = ":A:{$hash}";
04666             }
04667         }
04668 
04669         $hash = self::getPasswordFactory()->newFromCiphertext( $hash );
04670         return $hash->equals( $password );
04671     }
04672 
04694     public function addNewUserLogEntry( $action = false, $reason = '' ) {
04695         global $wgUser, $wgNewUserLog;
04696         if ( empty( $wgNewUserLog ) ) {
04697             return true; // disabled
04698         }
04699 
04700         if ( $action === true ) {
04701             $action = 'byemail';
04702         } elseif ( $action === false ) {
04703             if ( $this->getName() == $wgUser->getName() ) {
04704                 $action = 'create';
04705             } else {
04706                 $action = 'create2';
04707             }
04708         }
04709 
04710         if ( $action === 'create' || $action === 'autocreate' ) {
04711             $performer = $this;
04712         } else {
04713             $performer = $wgUser;
04714         }
04715 
04716         $logEntry = new ManualLogEntry( 'newusers', $action );
04717         $logEntry->setPerformer( $performer );
04718         $logEntry->setTarget( $this->getUserPage() );
04719         $logEntry->setComment( $reason );
04720         $logEntry->setParameters( array(
04721             '4::userid' => $this->getId(),
04722         ) );
04723         $logid = $logEntry->insert();
04724 
04725         if ( $action !== 'autocreate' ) {
04726             $logEntry->publish( $logid );
04727         }
04728 
04729         return (int)$logid;
04730     }
04731 
04739     public function addNewUserLogEntryAutoCreate() {
04740         $this->addNewUserLogEntry( 'autocreate' );
04741 
04742         return true;
04743     }
04744 
04750     protected function loadOptions( $data = null ) {
04751         global $wgContLang;
04752 
04753         $this->load();
04754 
04755         if ( $this->mOptionsLoaded ) {
04756             return;
04757         }
04758 
04759         $this->mOptions = self::getDefaultOptions();
04760 
04761         if ( !$this->getId() ) {
04762             // For unlogged-in users, load language/variant options from request.
04763             // There's no need to do it for logged-in users: they can set preferences,
04764             // and handling of page content is done by $pageLang->getPreferredVariant() and such,
04765             // so don't override user's choice (especially when the user chooses site default).
04766             $variant = $wgContLang->getDefaultVariant();
04767             $this->mOptions['variant'] = $variant;
04768             $this->mOptions['language'] = $variant;
04769             $this->mOptionsLoaded = true;
04770             return;
04771         }
04772 
04773         // Maybe load from the object
04774         if ( !is_null( $this->mOptionOverrides ) ) {
04775             wfDebug( "User: loading options for user " . $this->getId() . " from override cache.\n" );
04776             foreach ( $this->mOptionOverrides as $key => $value ) {
04777                 $this->mOptions[$key] = $value;
04778             }
04779         } else {
04780             if ( !is_array( $data ) ) {
04781                 wfDebug( "User: loading options for user " . $this->getId() . " from database.\n" );
04782                 // Load from database
04783                 $dbr = wfGetDB( DB_SLAVE );
04784 
04785                 $res = $dbr->select(
04786                     'user_properties',
04787                     array( 'up_property', 'up_value' ),
04788                     array( 'up_user' => $this->getId() ),
04789                     __METHOD__
04790                 );
04791 
04792                 $this->mOptionOverrides = array();
04793                 $data = array();
04794                 foreach ( $res as $row ) {
04795                     $data[$row->up_property] = $row->up_value;
04796                 }
04797             }
04798             foreach ( $data as $property => $value ) {
04799                 $this->mOptionOverrides[$property] = $value;
04800                 $this->mOptions[$property] = $value;
04801             }
04802         }
04803 
04804         $this->mOptionsLoaded = true;
04805 
04806         wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
04807     }
04808 
04814     protected function saveOptions() {
04815         $this->loadOptions();
04816 
04817         // Not using getOptions(), to keep hidden preferences in database
04818         $saveOptions = $this->mOptions;
04819 
04820         // Allow hooks to abort, for instance to save to a global profile.
04821         // Reset options to default state before saving.
04822         if ( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) ) {
04823             return;
04824         }
04825 
04826         $userId = $this->getId();
04827 
04828         $insert_rows = array(); // all the new preference rows
04829         foreach ( $saveOptions as $key => $value ) {
04830             // Don't bother storing default values
04831             $defaultOption = self::getDefaultOption( $key );
04832             if ( ( $defaultOption === null && $value !== false && $value !== null )
04833                 || $value != $defaultOption
04834             ) {
04835                 $insert_rows[] = array(
04836                     'up_user' => $userId,
04837                     'up_property' => $key,
04838                     'up_value' => $value,
04839                 );
04840             }
04841         }
04842 
04843         $dbw = wfGetDB( DB_MASTER );
04844 
04845         $res = $dbw->select( 'user_properties',
04846             array( 'up_property', 'up_value' ), array( 'up_user' => $userId ), __METHOD__ );
04847 
04848         // Find prior rows that need to be removed or updated. These rows will
04849         // all be deleted (the later so that INSERT IGNORE applies the new values).
04850         $keysDelete = array();
04851         foreach ( $res as $row ) {
04852             if ( !isset( $saveOptions[$row->up_property] )
04853                 || strcmp( $saveOptions[$row->up_property], $row->up_value ) != 0
04854             ) {
04855                 $keysDelete[] = $row->up_property;
04856             }
04857         }
04858 
04859         if ( count( $keysDelete ) ) {
04860             // Do the DELETE by PRIMARY KEY for prior rows.
04861             // In the past a very large portion of calls to this function are for setting
04862             // 'rememberpassword' for new accounts (a preference that has since been removed).
04863             // Doing a blanket per-user DELETE for new accounts with no rows in the table
04864             // caused gap locks on [max user ID,+infinity) which caused high contention since
04865             // updates would pile up on each other as they are for higher (newer) user IDs.
04866             // It might not be necessary these days, but it shouldn't hurt either.
04867             $dbw->delete( 'user_properties',
04868                 array( 'up_user' => $userId, 'up_property' => $keysDelete ), __METHOD__ );
04869         }
04870         // Insert the new preference rows
04871         $dbw->insert( 'user_properties', $insert_rows, __METHOD__, array( 'IGNORE' ) );
04872     }
04873 
04879     public static function getPasswordFactory() {
04880         if ( self::$mPasswordFactory === null ) {
04881             self::$mPasswordFactory = new PasswordFactory();
04882             self::$mPasswordFactory->init( RequestContext::getMain()->getConfig() );
04883         }
04884 
04885         return self::$mPasswordFactory;
04886     }
04887 
04911     public static function passwordChangeInputAttribs() {
04912         global $wgMinimalPasswordLength;
04913 
04914         if ( $wgMinimalPasswordLength == 0 ) {
04915             return array();
04916         }
04917 
04918         # Note that the pattern requirement will always be satisfied if the
04919         # input is empty, so we need required in all cases.
04920         #
04921         # @todo FIXME: Bug 23769: This needs to not claim the password is required
04922         # if e-mail confirmation is being used.  Since HTML5 input validation
04923         # is b0rked anyway in some browsers, just return nothing.  When it's
04924         # re-enabled, fix this code to not output required for e-mail
04925         # registration.
04926         #$ret = array( 'required' );
04927         $ret = array();
04928 
04929         # We can't actually do this right now, because Opera 9.6 will print out
04930         # the entered password visibly in its error message!  When other
04931         # browsers add support for this attribute, or Opera fixes its support,
04932         # we can add support with a version check to avoid doing this on Opera
04933         # versions where it will be a problem.  Reported to Opera as
04934         # DSK-262266, but they don't have a public bug tracker for us to follow.
04935         /*
04936         if ( $wgMinimalPasswordLength > 1 ) {
04937             $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
04938             $ret['title'] = wfMessage( 'passwordtooshort' )
04939                 ->numParams( $wgMinimalPasswordLength )->text();
04940         }
04941         */
04942 
04943         return $ret;
04944     }
04945 
04951     public static function selectFields() {
04952         return array(
04953             'user_id',
04954             'user_name',
04955             'user_real_name',
04956             'user_email',
04957             'user_touched',
04958             'user_token',
04959             'user_email_authenticated',
04960             'user_email_token',
04961             'user_email_token_expires',
04962             'user_registration',
04963             'user_editcount',
04964         );
04965     }
04966 
04974     static function newFatalPermissionDeniedStatus( $permission ) {
04975         global $wgLang;
04976 
04977         $groups = array_map(
04978             array( 'User', 'makeGroupLinkWiki' ),
04979             User::getGroupsWithPermission( $permission )
04980         );
04981 
04982         if ( $groups ) {
04983             return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );
04984         } else {
04985             return Status::newFatal( 'badaccess-group0' );
04986         }
04987     }
04988 }