use Psr\Log\LoggerInterface;
use BagOStuff;
+use CachedBagOStuff;
use Config;
use FauxRequest;
use Language;
/** @var Config */
private $config;
- /** @var BagOStuff|null */
- private $tempStore;
-
- /** @var BagOStuff|null */
- private $permStore;
+ /** @var CachedBagOStuff|null */
+ private $store;
/** @var SessionProvider[] */
private $sessionProviders = null;
$this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
}
- $this->tempStore = new \HashBagOStuff;
if ( isset( $options['store'] ) ) {
if ( !$options['store'] instanceof BagOStuff ) {
throw new \InvalidArgumentException(
'$options[\'store\'] must be an instance of BagOStuff'
);
}
- $this->permStore = $options['store'];
+ $store = $options['store'];
} else {
- $this->permStore = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
- $this->permStore->setLogger( $this->logger );
+ $store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
+ $store->setLogger( $this->logger );
}
+ $this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store );
register_shutdown_function( array( $this, 'shutdown' ) );
}
// Test this here to provide a better log message for the common case
// of "no such ID"
$key = wfMemcKey( 'MWSession', $id );
- $existing = $this->tempStore->get( $key );
- if ( $existing === false ) {
- $existing = $this->permStore->get( $key );
- if ( $existing !== false ) {
- $this->tempStore->set( $key, $existing );
- }
- }
- if ( is_array( $existing ) ) {
+ if ( is_array( $this->store->get( $key ) ) ) {
$info = new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => $id, 'idIsSafe' => true ) );
if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
$session = $this->getSessionFromInfo( $info, $request );
}
$key = wfMemcKey( 'MWSession', $id );
- $existing = $this->tempStore->get( $key );
- if ( $existing === false ) {
- $existing = $this->permStore->get( $key );
- if ( $existing !== false ) {
- $this->tempStore->set( $key, $existing );
- }
- }
- if ( is_array( $existing ) ) {
+ if ( is_array( $this->store->get( $key ) ) ) {
throw new \InvalidArgumentException( 'Session ID already exists' );
}
}
public function preventSessionsForUser( $username ) {
$this->preventUsers[$username] = true;
- // Reset the user's token to kill existing sessions
- $user = User::newFromName( $username );
- if ( $user && $user->getToken( false ) ) {
- $user->setToken();
- $user->saveSettings();
- }
-
// Instruct the session providers to kill any other sessions too.
foreach ( $this->getProviders() as $provider ) {
$provider->preventSessionsForUser( $username );
*/
private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
$key = wfMemcKey( 'MWSession', $info->getId() );
- $blob = $this->tempStore->get( $key );
- if ( $blob === false ) {
- $blob = $this->permStore->get( $key );
- if ( $blob !== false ) {
- $this->tempStore->set( $key, $blob );
- }
- }
+ $blob = $this->store->get( $key );
$newParams = array();
// Sanity check: blob must be an array, if it's saved at all
if ( !is_array( $blob ) ) {
$this->logger->warning( "Session $info: Bad data" );
- $this->tempStore->delete( $key );
- $this->permStore->delete( $key );
+ $this->store->delete( $key );
return false;
}
!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
) {
$this->logger->warning( "Session $info: Bad data structure" );
- $this->tempStore->delete( $key );
- $this->permStore->delete( $key );
+ $this->store->delete( $key );
return false;
}
!array_key_exists( 'provider', $metadata )
) {
$this->logger->warning( "Session $info: Bad metadata" );
- $this->tempStore->delete( $key );
- $this->permStore->delete( $key );
+ $this->store->delete( $key );
return false;
}
$newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
if ( !$provider ) {
$this->logger->warning( "Session $info: Unknown provider, " . $metadata['provider'] );
- $this->tempStore->delete( $key );
- $this->permStore->delete( $key );
+ $this->store->delete( $key );
return false;
}
} elseif ( $metadata['provider'] !== (string)$provider ) {
$backend = new SessionBackend(
$this->allSessionIds[$id],
$info,
- $this->tempStore,
- $this->permStore,
+ $this->store,
$this->logger,
$this->config->get( 'ObjectCacheSessionExpiry' )
);
do {
$id = wfBaseConvert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
$key = wfMemcKey( 'MWSession', $id );
- } while ( isset( $this->allSessionIds[$id] ) ||
- is_array( $this->tempStore->get( $key ) ) || is_array( $this->permStore->get( $key ) )
- );
+ } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
return $id;
}
* @param PHPSessionHandler $handler
*/
public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
- $handler->setManager( $this, $this->permStore, $this->logger );
+ $handler->setManager( $this, $this->store, $this->logger );
}
/**
*/
const TOKEN_LENGTH = 32;
+ /**
+ * @const string An invalid value for user_token
+ */
+ const INVALID_TOKEN = '*** INVALID ***';
+
/**
* Global constant made accessible as class constants so that autoloader
* magic can be used.
return $this->getName();
}
+ /**
+ * Test if it's safe to load this User object. You should typically check this before using
+ * $wgUser or RequestContext::getUser in a method that might be called before the system has
+ * been fully initialized. If the object is unsafe, you should use an anonymous user:
+ * \code
+ * $user = $wgUser->isSafeToLoad() ? $wgUser : new User;
+ * \endcode
+ *
+ * @since 1.27
+ * @return bool
+ */
+ public function isSafeToLoad() {
+ global $wgFullyInitialised;
+ return $wgFullyInitialised || $this->mLoadedItems === true || $this->mFrom !== 'session';
+ }
+
/**
* Load the user table data for this object from the source given by mFrom.
*
$this->queryFlagsUsed = $flags;
// If this is called too early, things are likely to break.
- if ( $this->mFrom === 'session' && empty( $wgFullyInitialised ) ) {
+ if ( !$wgFullyInitialised && $this->mFrom === 'session' ) {
\MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
->warning( 'User::loadFromSession called before the end of Setup.php', array(
'exception' => new Exception( 'User::loadFromSession called before the end of Setup.php' ),
$user = self::newFromRow( $row );
// A user is considered to exist as a non-system user if it has a
- // password set, or a temporary password set, or an email set.
+ // password set, or a temporary password set, or an email set, or a
+ // non-invalid token.
$passwordFactory = new PasswordFactory();
$passwordFactory->init( RequestContext::getMain()->getConfig() );
try {
$newpassword = PasswordFactory::newInvalidPassword();
}
if ( !$password instanceof InvalidPassword || !$newpassword instanceof InvalidPassword
- || $user->mEmail
+ || $user->mEmail || $user->mToken !== self::INVALID_TOKEN
) {
// User exists. Steal it?
if ( !$options['steal'] ) {
__METHOD__
);
$user->invalidateEmail();
+ $user->mToken = self::INVALID_TOKEN;
$user->saveSettings();
+ SessionManager::singleton()->preventSessionsForUser( $user->getName() );
}
- SessionManager::singleton()->preventSessionsForUser( $user->getName() );
-
return $user;
}
# We only need to worry about passing the IP address to the Block generator if the
# user is not immune to autoblocks/hardblocks, and they are the current user so we
# know which IP address they're actually coming from
- if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->equals( $wgUser ) ) {
- $ip = $this->getRequest()->getIP();
- } else {
- $ip = null;
+ $ip = null;
+ if ( !$this->isAllowed( 'ipblock-exempt' ) ) {
+ // $wgUser->getName() only works after the end of Setup.php. Until
+ // then, assume it's a logged-out user.
+ $globalUserName = $wgUser->isSafeToLoad()
+ ? $wgUser->getName()
+ : IP::sanitizeIP( $wgUser->getRequest()->getIP() );
+ if ( $this->getName() === $globalUserName ) {
+ $ip = $this->getRequest()->getIP();
+ }
}
// User/IP blocking
$keys = array();
$id = $this->getId();
$userLimit = false;
+ $isNewbie = $this->isNewbie();
- if ( isset( $limits['anon'] ) && $id == 0 ) {
- $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
- }
-
- if ( isset( $limits['user'] ) && $id != 0 ) {
- $userLimit = $limits['user'];
- }
- if ( $this->isNewbie() ) {
- if ( isset( $limits['newbie'] ) && $id != 0 ) {
+ if ( $id == 0 ) {
+ // limits for anons
+ if ( isset( $limits['anon'] ) ) {
+ $keys[wfMemcKey( 'limiter', $action, 'anon' )] = $limits['anon'];
+ }
+ } else {
+ // limits for logged-in users
+ if ( isset( $limits['user'] ) ) {
+ $userLimit = $limits['user'];
+ }
+ // limits for newbie logged-in users
+ if ( $isNewbie && isset( $limits['newbie'] ) ) {
$keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $limits['newbie'];
}
+ }
+
+ // limits for anons and for newbie logged-in users
+ if ( $isNewbie ) {
+ // ip-based limits
if ( isset( $limits['ip'] ) ) {
$ip = $this->getRequest()->getIP();
$keys["mediawiki:limiter:$action:ip:$ip"] = $limits['ip'];
}
+ // subnet-based limits
if ( isset( $limits['subnet'] ) ) {
$ip = $this->getRequest()->getIP();
- $matches = array();
- $subnet = false;
- if ( IP::isIPv6( $ip ) ) {
- $parts = IP::parseRange( "$ip/64" );
- $subnet = $parts[0];
- } elseif ( preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $ip, $matches ) ) {
- // IPv4
- $subnet = $matches[1];
- }
+ $subnet = IP::getSubnet( $ip );
if ( $subnet !== false ) {
$keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
}
}
}
+
// Check for group-specific permissions
- // If more than one group applies, use the group with the highest limit
+ // If more than one group applies, use the group with the highest limit ratio (max/period)
foreach ( $this->getGroups() as $group ) {
if ( isset( $limits[$group] ) ) {
if ( $userLimit === false
}
}
}
+
// Set the user limit key
if ( $userLimit !== false ) {
list( $max, $period ) = $userLimit;
$keys[wfMemcKey( 'limiter', $action, 'user', $id )] = $userLimit;
}
+ // ip-based limits for all ping-limitable users
+ if ( isset( $limits['ip-all'] ) ) {
+ $ip = $this->getRequest()->getIP();
+ // ignore if user limit is more permissive
+ if ( $isNewbie || $userLimit === false
+ || $limits['ip-all'][0] / $limits['ip-all'][1] > $userLimit[0] / $userLimit[1] ) {
+ $keys["mediawiki:limiter:$action:ip-all:$ip"] = $limits['ip-all'];
+ }
+ }
+
+ // subnet-based limits for all ping-limitable users
+ if ( isset( $limits['subnet-all'] ) ) {
+ $ip = $this->getRequest()->getIP();
+ $subnet = IP::getSubnet( $ip );
+ if ( $subnet !== false ) {
+ // ignore if user limit is more permissive
+ if ( $isNewbie || $userLimit === false
+ || $limits['ip-all'][0] / $limits['ip-all'][1]
+ > $userLimit[0] / $userLimit[1] ) {
+ $keys["mediawiki:limiter:$action:subnet-all:$subnet"] = $limits['subnet-all'];
+ }
+ }
+ }
+
$cache = ObjectCache::getLocalClusterInstance();
$triggered = false;
$this->setToken();
}
- // If the user doesn't have a token, return null to indicate that.
- // Otherwise, hmac the version with the secret if we have a version.
if ( !$this->mToken ) {
+ // The user doesn't have a token, return null to indicate that.
return null;
+ } elseif ( $this->mToken === self::INVALID_TOKEN ) {
+ // We return a random value here so existing token checks are very
+ // likely to fail.
+ return MWCryptRand::generateHex( self::TOKEN_LENGTH );
} elseif ( $wgAuthenticationTokenVersion === null ) {
+ // $wgAuthenticationTokenVersion not in use, so return the raw secret
return $this->mToken;
} else {
+ // $wgAuthenticationTokenVersion in use, so hmac it.
$ret = MWCryptHash::hmac( $wgAuthenticationTokenVersion, $this->mToken, false );
// The raw hash can be overly long. Shorten it up.
*/
public function setToken( $token = false ) {
$this->load();
- if ( !$token ) {
+ if ( $this->mToken === self::INVALID_TOKEN ) {
+ \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
+ ->debug( __METHOD__ . ": Ignoring attempt to set token for system user \"$this\"" );
+ } elseif ( !$token ) {
$this->mToken = MWCryptRand::generateHex( self::TOKEN_LENGTH );
} else {
$this->mToken = $token;
$manager = \TestingAccessWrapper::newFromObject( $this->getManager() );
$this->assertSame( $this->config, $manager->config );
$this->assertSame( $this->logger, $manager->logger );
- $this->assertSame( $this->store, $manager->permStore );
+ $this->assertSame( $this->store, $manager->store );
$manager = \TestingAccessWrapper::newFromObject( new SessionManager() );
$this->assertSame( \RequestContext::getMain()->getConfig(), $manager->config );
$manager = \TestingAccessWrapper::newFromObject( new SessionManager( array(
'config' => $this->config,
) ) );
- $this->assertSame( \ObjectCache::$instances['testSessionStore'], $manager->permStore );
+ $this->assertSame( \ObjectCache::$instances['testSessionStore'], $manager->store );
foreach ( array(
'config' => '$options[\'config\'] must be an instance of Config',
public function testGetSessionById() {
$manager = $this->getManager();
-
- // Disable the in-process cache so our $this->store->setSession() takes effect.
- \TestingAccessWrapper::newFromObject( $manager )->tempStore = new \EmptyBagOStuff;
-
try {
$manager->getSessionById( 'bad' );
$this->fail( 'Expected exception not thrown' );
'Bar' => array( 'X', 'Bar1', 3 => 'Bar2' ),
'Quux' => array( 'Quux' ),
'Baz' => array(),
- 'Quux' => array( 'Quux' ),
);
$this->assertEquals( $expect, $manager->getVaryHeaders() );
$that = $this;
- \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
+ \ObjectCache::$instances[__METHOD__] = new TestBagOStuff();
$this->setMwGlobals( array( 'wgMainCacheType' => __METHOD__ ) );
$this->stashMwGlobals( array( 'wgGroupPermissions' ) );
$this->objectCacheDef( $provider1 ),
) );
- $user = User::newFromName( 'UTSysop' );
- $token = $user->getToken( true );
-
$this->assertFalse( $manager->isUserSessionPrevented( 'UTSysop' ) );
$manager->preventSessionsForUser( 'UTSysop' );
- $this->assertNotEquals( $token, User::newFromName( 'UTSysop' )->getToken() );
$this->assertTrue( $manager->isUserSessionPrevented( 'UTSysop' ) );
}
$manager->setLogger( $logger );
$request = new \FauxRequest();
- // Disable the in-process cache so our $this->store->setSession() takes effect.
- \TestingAccessWrapper::newFromObject( $manager )->tempStore = new \EmptyBagOStuff;
-
// TestingAccessWrapper can't handle methods with reference arguments, sigh.
$rClass = new \ReflectionClass( $manager );
$rMethod = $rClass->getMethod( 'loadSessionInfoFromStore' );