Merge "Add 'autocomplete' option to HTMLTextField"
[lhc/web/wiklou.git] / includes / session / SessionManager.php
index a364045..b8e480f 100644 (file)
@@ -35,8 +35,15 @@ use WebRequest;
 /**
  * This serves as the entry point to the MediaWiki session handling system.
  *
+ * Most methods here are for internal use by session handling code. Other callers
+ * should only use getGlobalSession and the methods of SessionManagerInterface;
+ * the rest of the functionality is exposed via MediaWiki\Session\Session methods.
+ *
+ * To provide custom session handling, implement a MediaWiki\Session\SessionProvider.
+ *
  * @ingroup Session
  * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
  */
 final class SessionManager implements SessionManagerInterface {
        /** @var SessionManager|null */
@@ -301,6 +308,20 @@ final class SessionManager implements SessionManagerInterface {
                return $this->getSessionFromInfo( $infos[0], $request );
        }
 
+       public function invalidateSessionsForUser( User $user ) {
+               $user->setToken();
+               $user->saveSettings();
+
+               $authUser = \MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ] );
+               if ( $authUser ) {
+                       $authUser->resetAuthToken();
+               }
+
+               foreach ( $this->getProviders() as $provider ) {
+                       $provider->invalidateSessionsForUser( $user );
+               }
+       }
+
        public function getVaryHeaders() {
                // @codeCoverageIgnoreStart
                if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
@@ -357,196 +378,18 @@ final class SessionManager implements SessionManagerInterface {
        /**
         * Auto-create the given user, if necessary
         * @private Don't call this yourself. Let Setup.php do it for you at the right time.
-        * @note This more properly belongs in AuthManager, but we need it now.
-        *  When AuthManager comes, this will be deprecated and will pass-through
-        *  to the corresponding AuthManager method.
+        * @deprecated since 1.27, use MediaWiki\Auth\AuthManager::autoCreateUser instead
         * @param User $user User to auto-create
         * @return bool Success
+        * @codeCoverageIgnore
         */
        public static function autoCreateUser( User $user ) {
-               global $wgAuth;
-
-               $logger = self::singleton()->logger;
-
-               // Much of this code is based on that in CentralAuth
-
-               // 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( $flags );
-                       return false;
-               }
-
-               // Denied by AuthPlugin? But ignore AuthPlugin itself.
-               if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) {
-                       $logger->debug( __METHOD__ . ': denied by AuthPlugin' );
-                       $user->setId( 0 );
-                       $user->loadFromId();
-                       return false;
-               }
-
-               // Wiki is read-only?
-               if ( wfReadOnly() ) {
-                       $logger->debug( __METHOD__ . ': denied by wfReadOnly()' );
-                       $user->setId( 0 );
-                       $user->loadFromId();
-                       return false;
-               }
-
-               $userName = $user->getName();
-
-               // Check the session, if we tried to create this user already there's
-               // no point in retrying.
-               $session = self::getGlobalSession();
-               $reason = $session->get( 'MWSession::AutoCreateBlacklist' );
-               if ( $reason ) {
-                       $logger->debug( __METHOD__ . ": blacklisted in session ($reason)" );
-                       $user->setId( 0 );
-                       $user->loadFromId();
-                       return false;
-               }
-
-               // Is the IP user able to create accounts?
-               $anon = new User;
-               if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
-                       || $anon->isBlockedFromCreateAccount()
-               ) {
-                       // Blacklist the user to avoid repeated DB queries subsequently
-                       $logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' );
-                       $session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 );
-                       $session->persist();
-                       $user->setId( 0 );
-                       $user->loadFromId();
-                       return false;
-               }
-
-               // Check for validity of username
-               if ( !User::isCreatableName( $userName ) ) {
-                       $logger->debug( __METHOD__ . ': Invalid username, blacklisting' );
-                       $session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 );
-                       $session->persist();
-                       $user->setId( 0 );
-                       $user->loadFromId();
-                       return false;
-               }
-
-               // Give other extensions a chance to stop auto creation.
-               $user->loadDefaults( $userName );
-               $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" );
-                       $session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 );
-                       $session->persist();
-                       $user->setId( 0 );
-                       $user->loadFromId();
-                       return false;
-               }
-
-               // Make sure the name has not been changed
-               if ( $user->getName() !== $userName ) {
-                       $user->setId( 0 );
-                       $user->loadFromId();
-                       throw new \UnexpectedValueException(
-                               'AbortAutoAccount hook tried to change the user name'
-                       );
-               }
-
-               // Ignore warnings about master connections/writes...hard to avoid here
-               \Profiler::instance()->getTransactionProfiler()->resetExpectations();
-
-               $cache = \ObjectCache::getLocalClusterInstance();
-               $backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) );
-               if ( $cache->get( $backoffKey ) ) {
-                       $logger->debug( __METHOD__ . ': denied by prior creation attempt failures' );
-                       $user->setId( 0 );
-                       $user->loadFromId();
-                       return false;
-               }
-
-               // Checks passed, create the user...
-               $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
-               $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
-                               // 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( false, false, 'en' ),
-                                               [
-                                                       'username' => $userName,
-                                               ]
-                                       );
-                               }
-                               $user->setId( $id );
-                               $user->loadFromId( User::READ_LATEST );
-                               return false;
-                               // @codeCoverageIgnoreEnd
-                       }
-               } catch ( \Exception $ex ) {
-                       // @codeCoverageIgnoreStart
-                       $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
-                       throw $ex;
-                       // @codeCoverageIgnoreEnd
-               }
-
-               # Notify AuthPlugin
-               // @codeCoverageIgnoreStart
-               $tmpUser = $user;
-               $wgAuth->initUser( $tmpUser, true );
-               if ( $tmpUser !== $user ) {
-                       $logger->warning( __METHOD__ . ': ' .
-                               get_class( $wgAuth ) . '::initUser() replaced the user object' );
-               }
-               // @codeCoverageIgnoreEnd
-
-               # Notify hooks (e.g. Newuserlog)
-               \Hooks::run( 'AuthPluginAutoCreate', [ $user ] );
-               \Hooks::run( 'LocalUserCreated', [ $user, true ] );
-
-               $user->saveSettings();
-
-               # Update user count
-               \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
-
-               # Watch user's userpage and talk page
-               $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
-
-               return true;
+               wfDeprecated( __METHOD__, '1.27' );
+               return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser(
+                       $user,
+                       \MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION,
+                       false
+               )->isGood();
        }
 
        /**
@@ -704,6 +547,20 @@ final class SessionManager implements SessionManagerInterface {
                $key = wfMemcKey( 'MWSession', $info->getId() );
                $blob = $this->store->get( $key );
 
+               // If we got data from the store and the SessionInfo says to force use,
+               // "fail" means to delete the data from the store and retry. Otherwise,
+               // "fail" is just return false.
+               if ( $info->forceUse() && $blob !== false ) {
+                       $failHandler = function () use ( $key, &$info, $request ) {
+                               $this->store->delete( $key );
+                               return $this->loadSessionInfoFromStore( $info, $request );
+                       };
+               } else {
+                       $failHandler = function () {
+                               return false;
+                       };
+               }
+
                $newParams = [];
 
                if ( $blob !== false ) {
@@ -713,7 +570,7 @@ final class SessionManager implements SessionManagerInterface {
                                        'session' => $info,
                                ] );
                                $this->store->delete( $key );
-                               return false;
+                               return $failHandler();
                        }
 
                        // Sanity check: blob has data and metadata arrays
@@ -724,7 +581,7 @@ final class SessionManager implements SessionManagerInterface {
                                        'session' => $info,
                                ] );
                                $this->store->delete( $key );
-                               return false;
+                               return $failHandler();
                        }
 
                        $data = $blob['data'];
@@ -741,7 +598,7 @@ final class SessionManager implements SessionManagerInterface {
                                        'session' => $info,
                                ] );
                                $this->store->delete( $key );
-                               return false;
+                               return $failHandler();
                        }
 
                        // First, load the provider from metadata, or validate it against the metadata.
@@ -756,7 +613,7 @@ final class SessionManager implements SessionManagerInterface {
                                                ]
                                        );
                                        $this->store->delete( $key );
-                                       return false;
+                                       return $failHandler();
                                }
                        } elseif ( $metadata['provider'] !== (string)$provider ) {
                                $this->logger->warning( 'Session "{session}": Wrong provider ' .
@@ -764,7 +621,7 @@ final class SessionManager implements SessionManagerInterface {
                                        [
                                                'session' => $info,
                                ] );
-                               return false;
+                               return $failHandler();
                        }
 
                        // Load provider metadata from metadata, or validate it against the metadata
@@ -788,7 +645,7 @@ final class SessionManager implements SessionManagerInterface {
                                                                'exception' => $ex,
                                                        ] + $ex->getContext()
                                                );
-                                               return false;
+                                               return $failHandler();
                                        }
                                }
                        }
@@ -810,7 +667,7 @@ final class SessionManager implements SessionManagerInterface {
                                                'session' => $info,
                                                'exception' => $ex,
                                        ] );
-                                       return false;
+                                       return $failHandler();
                                }
                                $newParams['userInfo'] = $userInfo;
                        } else {
@@ -825,7 +682,7 @@ final class SessionManager implements SessionManagerInterface {
                                                                'uid_a' => $metadata['userId'],
                                                                'uid_b' => $userInfo->getId(),
                                                ] );
-                                               return false;
+                                               return $failHandler();
                                        }
 
                                        // If the user was renamed, probably best to fail here.
@@ -839,7 +696,7 @@ final class SessionManager implements SessionManagerInterface {
                                                                'uname_a' => $metadata['userName'],
                                                                'uname_b' => $userInfo->getName(),
                                                ] );
-                                               return false;
+                                               return $failHandler();
                                        }
 
                                } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
@@ -851,7 +708,7 @@ final class SessionManager implements SessionManagerInterface {
                                                                'uname_a' => $metadata['userName'],
                                                                'uname_b' => $userInfo->getName(),
                                                ] );
-                                               return false;
+                                               return $failHandler();
                                        }
                                } elseif ( !$userInfo->isAnon() ) {
                                        // Metadata specifies an anonymous user, but the passed-in
@@ -861,7 +718,7 @@ final class SessionManager implements SessionManagerInterface {
                                                [
                                                        'session' => $info,
                                        ] );
-                                       return false;
+                                       return $failHandler();
                                }
                        }
 
@@ -872,7 +729,7 @@ final class SessionManager implements SessionManagerInterface {
                                $this->logger->warning( 'Session "{session}": User token mismatch', [
                                        'session' => $info,
                                ] );
-                               return false;
+                               return $failHandler();
                        }
                        if ( !$userInfo->isVerified() ) {
                                $newParams['userInfo'] = $userInfo->verified();
@@ -899,7 +756,7 @@ final class SessionManager implements SessionManagerInterface {
                                        [
                                                'session' => $info,
                                ] );
-                               return false;
+                               return $failHandler();
                        }
 
                        // If no user was provided and no metadata, it must be anon.
@@ -912,7 +769,7 @@ final class SessionManager implements SessionManagerInterface {
                                                [
                                                        'session' => $info,
                                        ] );
-                                       return false;
+                                       return $failHandler();
                                }
                        } elseif ( !$info->getUserInfo()->isVerified() ) {
                                $this->logger->warning(
@@ -920,7 +777,7 @@ final class SessionManager implements SessionManagerInterface {
                                        [
                                                'session' => $info,
                                ] );
-                               return false;
+                               return $failHandler();
                        }
 
                        $data = false;
@@ -942,7 +799,7 @@ final class SessionManager implements SessionManagerInterface {
                // Allow the provider to check the loaded SessionInfo
                $providerMetadata = $info->getProviderMetadata();
                if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
-                       return false;
+                       return $failHandler();
                }
                if ( $providerMetadata !== $info->getProviderMetadata() ) {
                        $info = new SessionInfo( $info->getPriority(), [
@@ -962,16 +819,16 @@ final class SessionManager implements SessionManagerInterface {
                        $this->logger->warning( 'Session "{session}": ' . $reason, [
                                'session' => $info,
                        ] );
-                       return false;
+                       return $failHandler();
                }
 
                return true;
        }
 
        /**
-        * Create a session corresponding to the passed SessionInfo
+        * Create a Session corresponding to the passed SessionInfo
         * @private For use by a SessionProvider that needs to specially create its
-        *  own session.
+        *  own Session. Most session providers won't need this.
         * @param SessionInfo $info
         * @param WebRequest $request
         * @return Session
@@ -1023,7 +880,7 @@ final class SessionManager implements SessionManagerInterface {
                        $session->resetId();
                }
 
-               \ScopedCallback::consume( $delay );
+               \Wikimedia\ScopedCallback::consume( $delay );
                return $session;
        }
 
@@ -1074,7 +931,7 @@ final class SessionManager implements SessionManagerInterface {
         */
        public function generateSessionId() {
                do {
-                       $id = wfBaseConvert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
+                       $id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
                        $key = wfMemcKey( 'MWSession', $id );
                } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
                return $id;
@@ -1091,6 +948,7 @@ final class SessionManager implements SessionManagerInterface {
 
        /**
         * Reset the internal caching for unit testing
+        * @protected Unit tests only
         */
        public static function resetCache() {
                if ( !defined( 'MW_PHPUNIT_TEST' ) ) {