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