SessionBackend: skip isUserSessionPrevented check for anons
[lhc/web/wiklou.git] / includes / session / SessionManager.php
index f03260f..1aab12a 100644 (file)
@@ -28,8 +28,6 @@ use BagOStuff;
 use CachedBagOStuff;
 use Config;
 use FauxRequest;
-use Language;
-use Message;
 use User;
 use WebRequest;
 
@@ -68,13 +66,13 @@ final class SessionManager implements SessionManagerInterface {
        private $varyHeaders = null;
 
        /** @var SessionBackend[] */
-       private $allSessionBackends = array();
+       private $allSessionBackends = [];
 
        /** @var SessionId[] */
-       private $allSessionIds = array();
+       private $allSessionIds = [];
 
        /** @var string[] */
-       private $preventUsers = array();
+       private $preventUsers = [];
 
        /**
         * Get the global SessionManager
@@ -137,7 +135,7 @@ final class SessionManager implements SessionManagerInterface {
         *  - logger: LoggerInterface to use for logging. Defaults to the 'session' channel.
         *  - store: BagOStuff to store session data in.
         */
-       public function __construct( $options = array() ) {
+       public function __construct( $options = [] ) {
                if ( isset( $options['config'] ) ) {
                        $this->config = $options['config'];
                        if ( !$this->config instanceof Config ) {
@@ -169,11 +167,10 @@ final class SessionManager implements SessionManagerInterface {
                        $store = $options['store'];
                } else {
                        $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' ) );
+               register_shutdown_function( [ $this, 'shutdown' ] );
        }
 
        public function setLogger( LoggerInterface $logger ) {
@@ -205,7 +202,7 @@ final class SessionManager implements SessionManagerInterface {
                // of "no such ID"
                $key = wfMemcKey( 'MWSession', $id );
                if ( is_array( $this->store->get( $key ) ) ) {
-                       $info = new SessionInfo( SessionInfo::MIN_PRIORITY, array( 'id' => $id, 'idIsSafe' => true ) );
+                       $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => $id, 'idIsSafe' => true ] );
                        if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
                                $session = $this->getSessionFromInfo( $info, $request );
                        }
@@ -216,8 +213,11 @@ final class SessionManager implements SessionManagerInterface {
                        try {
                                $session = $this->getEmptySessionInternal( $request, $id );
                        } catch ( \Exception $ex ) {
-                               $this->logger->error( __METHOD__ . ': failed to create empty session: ' .
-                                       $ex->getMessage() );
+                               $this->logger->error( 'Failed to create empty session: {exception}',
+                                       [
+                                               'method' => __METHOD__,
+                                               'exception' => $ex,
+                               ] );
                                $session = null;
                        }
                }
@@ -250,7 +250,7 @@ final class SessionManager implements SessionManagerInterface {
                        $request = new FauxRequest;
                }
 
-               $infos = array();
+               $infos = [];
                foreach ( $this->getProviders() as $provider ) {
                        $info = $provider->newSessionInfo( $id );
                        if ( !$info ) {
@@ -279,7 +279,7 @@ final class SessionManager implements SessionManagerInterface {
                        if ( $compare === 0 ) {
                                $infos[] = $info;
                        } else {
-                               $infos = array( $info );
+                               $infos = [ $info ];
                        }
                }
 
@@ -297,11 +297,11 @@ final class SessionManager implements SessionManagerInterface {
 
        public function getVaryHeaders() {
                if ( $this->varyHeaders === null ) {
-                       $headers = array();
+                       $headers = [];
                        foreach ( $this->getProviders() as $provider ) {
                                foreach ( $provider->getVaryHeaders() as $header => $options ) {
                                        if ( !isset( $headers[$header] ) ) {
-                                               $headers[$header] = array();
+                                               $headers[$header] = [];
                                        }
                                        if ( is_array( $options ) ) {
                                                $headers[$header] = array_unique( array_merge( $headers[$header], $options ) );
@@ -315,7 +315,7 @@ final class SessionManager implements SessionManagerInterface {
 
        public function getVaryCookies() {
                if ( $this->varyCookies === null ) {
-                       $cookies = array();
+                       $cookies = [];
                        foreach ( $this->getProviders() as $provider ) {
                                $cookies = array_merge( $cookies, $provider->getVaryCookies() );
                        }
@@ -356,19 +356,21 @@ final class SessionManager implements SessionManagerInterface {
 
                // Try the local user from the slave DB
                $localId = User::idFromName( $user->getName() );
+               $flags = 0;
 
                // Fetch the user ID from the master, so that we don't try to create the user
                // when they already exist, due to replication lag
                // @codeCoverageIgnoreStart
                if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
                        $localId = User::idFromName( $user->getName(), User::READ_LATEST );
+                       $flags = User::READ_LATEST;
                }
                // @codeCoverageIgnoreEnd
 
                if ( $localId ) {
                        // User exists after all.
                        $user->setId( $localId );
-                       $user->loadFromId();
+                       $user->loadFromId( $flags );
                        return false;
                }
 
@@ -428,7 +430,7 @@ final class SessionManager implements SessionManagerInterface {
                // Give other extensions a chance to stop auto creation.
                $user->loadDefaults( $userName );
                $abortMessage = '';
-               if ( !\Hooks::run( 'AbortAutoAccount', array( $user, &$abortMessage ) ) ) {
+               if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortMessage ] ) ) {
                        // In this case we have no way to return the message to the user,
                        // but we can log it.
                        $logger->debug( __METHOD__ . ": denied by hook: $abortMessage" );
@@ -462,22 +464,41 @@ final class SessionManager implements SessionManagerInterface {
 
                // Checks passed, create the user...
                $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
-               $logger->info( __METHOD__ . ": creating new user ($userName) - from: $from" );
+               $logger->info( __METHOD__ . ': creating new user ({username}) - from: {url}',
+                       [
+                               'username' => $userName,
+                               'url' => $from,
+               ] );
 
                try {
                        // Insert the user into the local DB master
                        $status = $user->addToDatabase();
                        if ( !$status->isOK() ) {
                                // @codeCoverageIgnoreStart
-                               $logger->error( __METHOD__ . ': failed with message ' . $status->getWikiText() );
-                               $user->setId( 0 );
-                               $user->loadFromId();
+                               // double-check for a race condition (T70012)
+                               $id = User::idFromName( $user->getName(), User::READ_LATEST );
+                               if ( $id ) {
+                                       $logger->info( __METHOD__ . ': tried to autocreate existing user',
+                                               [
+                                                       'username' => $userName,
+                                               ] );
+                               } else {
+                                       $logger->error( __METHOD__ . ': failed with message ' . $status->getWikiText(),
+                                               [
+                                                       'username' => $userName,
+                                               ] );
+                               }
+                               $user->setId( $id );
+                               $user->loadFromId( User::READ_LATEST );
                                return false;
                                // @codeCoverageIgnoreEnd
                        }
                } catch ( \Exception $ex ) {
                        // @codeCoverageIgnoreStart
-                       $logger->error( __METHOD__ . ': failed with exception ' . $ex->getMessage() );
+                       $logger->error( __METHOD__ . ': failed with exception {exception}', [
+                               'exception' => $ex,
+                               'username' => $userName,
+                       ] );
                        // Do not keep throwing errors for a while
                        $cache->set( $backoffKey, 1, 600 );
                        // Bubble up error; which should normally trigger DB rollbacks
@@ -494,8 +515,8 @@ final class SessionManager implements SessionManagerInterface {
                }
 
                # Notify hooks (e.g. Newuserlog)
-               \Hooks::run( 'AuthPluginAutoCreate', array( $user ) );
-               \Hooks::run( 'LocalUserCreated', array( $user, true ) );
+               \Hooks::run( 'AuthPluginAutoCreate', [ $user ] );
+               \Hooks::run( 'LocalUserCreated', [ $user, true ] );
 
                $user->saveSettings();
 
@@ -542,7 +563,7 @@ final class SessionManager implements SessionManagerInterface {
         */
        protected function getProviders() {
                if ( $this->sessionProviders === null ) {
-                       $this->sessionProviders = array();
+                       $this->sessionProviders = [];
                        foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
                                $provider = \ObjectFactory::getObjectFromSpec( $spec );
                                $provider->setLogger( $this->logger );
@@ -597,7 +618,7 @@ final class SessionManager implements SessionManagerInterface {
         */
        private function getSessionInfoForRequest( WebRequest $request ) {
                // Call all providers to fetch "the" session
-               $infos = array();
+               $infos = [];
                foreach ( $this->getProviders() as $provider ) {
                        $info = $provider->provideSessionInfo( $request );
                        if ( !$info ) {
@@ -615,7 +636,7 @@ final class SessionManager implements SessionManagerInterface {
                // successfully loaded, and then all the ones after it with the same
                // priority.
                usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
-               $retInfos = array();
+               $retInfos = [];
                while ( $infos ) {
                        $info = array_pop( $infos );
                        if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
@@ -657,12 +678,14 @@ final class SessionManager implements SessionManagerInterface {
                $key = wfMemcKey( 'MWSession', $info->getId() );
                $blob = $this->store->get( $key );
 
-               $newParams = array();
+               $newParams = [];
 
                if ( $blob !== false ) {
                        // 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->logger->warning( 'Session "{session}": Bad data', [
+                                       'session' => $info,
+                               ] );
                                $this->store->delete( $key );
                                return false;
                        }
@@ -671,7 +694,9 @@ final class SessionManager implements SessionManagerInterface {
                        if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
                                !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
                        ) {
-                               $this->logger->warning( "Session $info: Bad data structure" );
+                               $this->logger->warning( 'Session "{session}": Bad data structure', [
+                                       'session' => $info,
+                               ] );
                                $this->store->delete( $key );
                                return false;
                        }
@@ -686,7 +711,9 @@ final class SessionManager implements SessionManagerInterface {
                                !array_key_exists( 'userToken', $metadata ) ||
                                !array_key_exists( 'provider', $metadata )
                        ) {
-                               $this->logger->warning( "Session $info: Bad metadata" );
+                               $this->logger->warning( 'Session "{session}": Bad metadata', [
+                                       'session' => $info,
+                               ] );
                                $this->store->delete( $key );
                                return false;
                        }
@@ -696,13 +723,21 @@ final class SessionManager implements SessionManagerInterface {
                        if ( $provider === null ) {
                                $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
                                if ( !$provider ) {
-                                       $this->logger->warning( "Session $info: Unknown provider, " . $metadata['provider'] );
+                                       $this->logger->warning(
+                                               'Session "{session}": Unknown provider ' . $metadata['provider'],
+                                               [
+                                                       'session' => $info,
+                                               ]
+                                       );
                                        $this->store->delete( $key );
                                        return false;
                                }
                        } elseif ( $metadata['provider'] !== (string)$provider ) {
-                               $this->logger->warning( "Session $info: Wrong provider, " .
-                                       $metadata['provider'] . ' !== ' . $provider );
+                               $this->logger->warning( 'Session "{session}": Wrong provider ' .
+                                       $metadata['provider'] . ' !== ' . $provider,
+                                       [
+                                               'session' => $info,
+                               ] );
                                return false;
                        }
 
@@ -719,8 +754,14 @@ final class SessionManager implements SessionManagerInterface {
                                                if ( $newProviderMetadata !== $providerMetadata ) {
                                                        $newParams['metadata'] = $newProviderMetadata;
                                                }
-                                       } catch ( \UnexpectedValueException $ex ) {
-                                               $this->logger->warning( "Session $info: Metadata merge failed: " . $ex->getMessage() );
+                                       } catch ( MetadataMergeException $ex ) {
+                                               $this->logger->warning(
+                                                       'Session "{session}": Metadata merge failed: {exception}',
+                                                       [
+                                                               'session' => $info,
+                                                               'exception' => $ex,
+                                                       ] + $ex->getContext()
+                                               );
                                                return false;
                                        }
                                }
@@ -739,7 +780,10 @@ final class SessionManager implements SessionManagerInterface {
                                                $userInfo = UserInfo::newAnonymous();
                                        }
                                } catch ( \InvalidArgumentException $ex ) {
-                                       $this->logger->error( "Session $info: " . $ex->getMessage() );
+                                       $this->logger->error( 'Session "{session}": {exception}', [
+                                               'session' => $info,
+                                               'exception' => $ex,
+                                       ] );
                                        return false;
                                }
                                $newParams['userInfo'] = $userInfo;
@@ -748,8 +792,13 @@ final class SessionManager implements SessionManagerInterface {
                                // is no saved ID and the names match.
                                if ( $metadata['userId'] ) {
                                        if ( $metadata['userId'] !== $userInfo->getId() ) {
-                                               $this->logger->warning( "Session $info: User ID mismatch, " .
-                                                       $metadata['userId'] . ' !== ' . $userInfo->getId() );
+                                               $this->logger->warning(
+                                                       'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
+                                                       [
+                                                               'session' => $info,
+                                                               'uid_a' => $metadata['userId'],
+                                                               'uid_b' => $userInfo->getId(),
+                                               ] );
                                                return false;
                                        }
 
@@ -757,24 +806,35 @@ final class SessionManager implements SessionManagerInterface {
                                        if ( $metadata['userName'] !== null &&
                                                $userInfo->getName() !== $metadata['userName']
                                        ) {
-                                               $this->logger->warning( "Session $info: User ID matched but name didn't (rename?), " .
-                                                       $metadata['userName'] . ' !== ' . $userInfo->getName() );
+                                               $this->logger->warning(
+                                                       'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
+                                                       [
+                                                               'session' => $info,
+                                                               'uname_a' => $metadata['userName'],
+                                                               'uname_b' => $userInfo->getName(),
+                                               ] );
                                                return false;
                                        }
 
                                } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
                                        if ( $metadata['userName'] !== $userInfo->getName() ) {
-                                               $this->logger->warning( "Session $info: User name mismatch, " .
-                                                       $metadata['userName'] . ' !== ' . $userInfo->getName() );
+                                               $this->logger->warning(
+                                                       'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
+                                                       [
+                                                               'session' => $info,
+                                                               'uname_a' => $metadata['userName'],
+                                                               'uname_b' => $userInfo->getName(),
+                                               ] );
                                                return false;
                                        }
                                } elseif ( !$userInfo->isAnon() ) {
                                        // Metadata specifies an anonymous user, but the passed-in
                                        // user isn't anonymous.
                                        $this->logger->warning(
-                                               "Session $info: Metadata has an anonymous user, " .
-                                                       'but a non-anon user was provided'
-                                       );
+                                               'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
+                                               [
+                                                       'session' => $info,
+                                       ] );
                                        return false;
                                }
                        }
@@ -783,7 +843,9 @@ final class SessionManager implements SessionManagerInterface {
                        if ( $metadata['userToken'] !== null &&
                                $userInfo->getToken() !== $metadata['userToken']
                        ) {
-                               $this->logger->warning( "Session $info: User token mismatch" );
+                               $this->logger->warning( 'Session "{session}": User token mismatch', [
+                                       'session' => $info,
+                               ] );
                                return false;
                        }
                        if ( !$userInfo->isVerified() ) {
@@ -806,7 +868,11 @@ final class SessionManager implements SessionManagerInterface {
                } else {
                        // No metadata, so we can't load the provider if one wasn't given.
                        if ( $info->getProvider() === null ) {
-                               $this->logger->warning( "Session $info: Null provider and no metadata" );
+                               $this->logger->warning(
+                                       'Session "{session}": Null provider and no metadata',
+                                       [
+                                               'session' => $info,
+                               ] );
                                return false;
                        }
 
@@ -816,14 +882,18 @@ final class SessionManager implements SessionManagerInterface {
                                        $newParams['userInfo'] = UserInfo::newAnonymous();
                                } else {
                                        $this->logger->info(
-                                               "Session $info: No user provided and provider cannot set user"
-                                       );
+                                               'Session "{session}": No user provided and provider cannot set user',
+                                               [
+                                                       'session' => $info,
+                                       ] );
                                        return false;
                                }
                        } elseif ( !$info->getUserInfo()->isVerified() ) {
                                $this->logger->warning(
-                                       "Session $info: Unverified user provided and no metadata to auth it"
-                               );
+                                       'Session "{session}": Unverified user provided and no metadata to auth it',
+                                       [
+                                               'session' => $info,
+                               ] );
                                return false;
                        }
 
@@ -849,10 +919,10 @@ final class SessionManager implements SessionManagerInterface {
                        return false;
                }
                if ( $providerMetadata !== $info->getProviderMetadata() ) {
-                       $info = new SessionInfo( $info->getPriority(), array(
+                       $info = new SessionInfo( $info->getPriority(), [
                                'metadata' => $providerMetadata,
                                'copyFrom' => $info,
-                       ) );
+                       ] );
                }
 
                // Give hooks a chance to abort. Combined with the SessionMetadata
@@ -861,9 +931,11 @@ final class SessionManager implements SessionManagerInterface {
                $reason = 'Hook aborted';
                if ( !\Hooks::run(
                        'SessionCheckInfo',
-                       array( &$reason, $info, $request, $metadata, $data )
+                       [ &$reason, $info, $request, $metadata, $data ]
                ) ) {
-                       $this->logger->warning( "Session $info: $reason" );
+                       $this->logger->warning( 'Session "{session}": ' . $reason, [
+                               'session' => $info,
+                       ] );
                        return false;
                }
 
@@ -879,6 +951,15 @@ final class SessionManager implements SessionManagerInterface {
         * @return Session
         */
        public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
+               if ( defined( 'MW_NO_SESSION' ) ) {
+                       if ( MW_NO_SESSION === 'warn' ) {
+                               // Undocumented safety case for converting existing entry points
+                               $this->logger->error( 'Sessions are supposed to be disabled for this entry point' );
+                       } else {
+                               throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
+                       }
+               }
+
                $id = $info->getId();
 
                if ( !isset( $this->allSessionBackends[$id] ) ) {