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