34use Psr\Log\LoggerInterface;
37use Wikimedia\ObjectFactory;
100 if ( self::$instance ===
null ) {
101 self::$instance =
new self();
121 $request = \RequestContext::getMain()->getRequest();
123 !self::$globalSession
124 || self::$globalSessionRequest !== $request
125 || $id !==
'' && self::$globalSession->getId() !== $id
127 self::$globalSessionRequest = $request;
137 self::$globalSession = $request->getSession();
142 self::$globalSession =
self::singleton()->getSessionById( $id,
true, $request )
143 ?: $request->getSession();
156 if ( isset( $options[
'config'] ) ) {
157 $this->config = $options[
'config'];
158 if ( !$this->config instanceof
Config ) {
159 throw new \InvalidArgumentException(
160 '$options[\'config\'] must be an instance of Config'
167 if ( isset( $options[
'logger'] ) ) {
168 if ( !$options[
'logger'] instanceof LoggerInterface ) {
169 throw new \InvalidArgumentException(
170 '$options[\'logger\'] must be an instance of LoggerInterface'
178 if ( isset( $options[
'hookContainer'] ) ) {
184 if ( isset( $options[
'store'] ) ) {
185 if ( !$options[
'store'] instanceof
BagOStuff ) {
186 throw new \InvalidArgumentException(
187 '$options[\'store\'] must be an instance of BagOStuff'
190 $store = $options[
'store'];
192 $store = \ObjectCache::getInstance( $this->config->get(
'SessionCacheType' ) );
195 $this->logger->debug(
'SessionManager using store ' . get_class(
$store ) );
198 register_shutdown_function( [ $this,
'shutdown' ] );
226 if ( !self::validateSessionId( $id ) ) {
227 throw new \InvalidArgumentException(
'Invalid session ID' );
237 if ( isset( $this->allSessionBackends[$id] ) ) {
242 $key = $this->store->makeKey(
'MWSession', $id );
243 if ( is_array( $this->store->get( $key ) ) ) {
250 if ( $create && $session ===
null ) {
254 }
catch ( \Exception $ex ) {
255 $this->logger->error(
'Failed to create empty session: {exception}',
257 'method' => __METHOD__,
278 if ( $id !==
null ) {
279 if ( !self::validateSessionId( $id ) ) {
280 throw new \InvalidArgumentException(
'Invalid session ID' );
283 $key = $this->store->makeKey(
'MWSession', $id );
284 if ( is_array( $this->store->get( $key ) ) ) {
285 throw new \InvalidArgumentException(
'Session ID already exists' );
294 $info = $provider->newSessionInfo( $id );
298 if ( $info->getProvider() !== $provider ) {
299 throw new \UnexpectedValueException(
300 "$provider returned an empty session info for a different provider: $info"
303 if ( $id !==
null && $info->getId() !== $id ) {
304 throw new \UnexpectedValueException(
305 "$provider returned empty session info with a wrong id: " .
306 $info->getId() .
' != ' . $id
309 if ( !$info->isIdSafe() ) {
310 throw new \UnexpectedValueException(
311 "$provider returned empty session info with id flagged unsafe"
315 if ( $compare > 0 ) {
318 if ( $compare === 0 ) {
326 if ( count( $infos ) > 1 ) {
327 throw new \UnexpectedValueException(
328 'Multiple empty sessions tied for top priority: ' . implode(
', ', $infos )
330 } elseif ( count( $infos ) < 1 ) {
331 throw new \UnexpectedValueException(
'No provider could provide an empty session!' );
342 $provider->invalidateSessionsForUser( $user );
348 if ( defined(
'MW_NO_SESSION' ) &&
MW_NO_SESSION !==
'warn' ) {
352 if ( $this->varyHeaders ===
null ) {
355 foreach ( $provider->getVaryHeaders() as
$header => $options ) {
356 # Note that the $options value returned has been deprecated
361 $this->varyHeaders = $headers;
368 if ( defined(
'MW_NO_SESSION' ) &&
MW_NO_SESSION !==
'warn' ) {
372 if ( $this->varyCookies ===
null ) {
375 $cookies = array_merge( $cookies, $provider->getVaryCookies() );
377 $this->varyCookies = array_values( array_unique( $cookies ) );
388 return is_string( $id ) && preg_match(
'/^[a-zA-Z0-9_-]{32,}$/', $id );
406 $this->preventUsers[$username] =
true;
410 $provider->preventSessionsForUser( $username );
421 return !empty( $this->preventUsers[$username] );
429 if ( $this->sessionProviders ===
null ) {
430 $this->sessionProviders = [];
431 foreach ( $this->config->get(
'SessionProviders' ) as $spec ) {
433 $provider = ObjectFactory::getObjectFromSpec( $spec );
434 $provider->setLogger( $this->logger );
435 $provider->setConfig( $this->config );
436 $provider->setManager( $this );
437 $provider->setHookContainer( $this->hookContainer );
438 if ( isset( $this->sessionProviders[(
string)$provider] ) ) {
440 throw new \UnexpectedValueException(
"Duplicate provider name \"$provider\"" );
442 $this->sessionProviders[(string)$provider] = $provider;
460 return $providers[$name] ??
null;
468 if ( $this->allSessionBackends ) {
469 $this->logger->debug(
'Saving all sessions on shutdown' );
470 if ( session_id() !==
'' ) {
472 session_write_close();
475 foreach ( $this->allSessionBackends as $backend ) {
476 $backend->shutdown();
490 $info = $provider->provideSessionInfo( $request );
494 if ( $info->getProvider() !== $provider ) {
495 throw new \UnexpectedValueException(
496 "$provider returned session info for a different provider: $info"
505 usort( $infos, [ SessionInfo::class,
'compare' ] );
508 $info = array_pop( $infos );
512 $info = array_pop( $infos );
523 $info->getProvider()->unpersistSession( $request );
528 $info->getProvider()->unpersistSession( $request );
532 if ( count( $retInfos ) > 1 ) {
535 'Multiple sessions for this request tied for top priority: ' . implode(
', ', $retInfos )
539 return $retInfos ? $retInfos[0] :
null;
550 $key = $this->store->makeKey(
'MWSession', $info->
getId() );
551 $blob = $this->store->get( $key );
557 $failHandler =
function () use ( $key, &$info, $request ) {
558 $this->store->delete( $key );
562 $failHandler =
function () {
569 if (
$blob !==
false ) {
571 if ( !is_array(
$blob ) ) {
572 $this->logger->warning(
'Session "{session}": Bad data', [
575 $this->store->delete( $key );
576 return $failHandler();
580 if ( !isset(
$blob[
'data'] ) || !is_array(
$blob[
'data'] ) ||
581 !isset(
$blob[
'metadata'] ) || !is_array(
$blob[
'metadata'] )
583 $this->logger->warning(
'Session "{session}": Bad data structure', [
586 $this->store->delete( $key );
587 return $failHandler();
590 $data =
$blob[
'data'];
591 $metadata =
$blob[
'metadata'];
595 if ( !array_key_exists(
'userId', $metadata ) ||
596 !array_key_exists(
'userName', $metadata ) ||
597 !array_key_exists(
'userToken', $metadata ) ||
598 !array_key_exists(
'provider', $metadata )
600 $this->logger->warning(
'Session "{session}": Bad metadata', [
603 $this->store->delete( $key );
604 return $failHandler();
609 if ( $provider ===
null ) {
610 $newParams[
'provider'] = $provider = $this->
getProvider( $metadata[
'provider'] );
612 $this->logger->warning(
613 'Session "{session}": Unknown provider ' . $metadata[
'provider'],
618 $this->store->delete( $key );
619 return $failHandler();
621 } elseif ( $metadata[
'provider'] !== (
string)$provider ) {
622 $this->logger->warning(
'Session "{session}": Wrong provider ' .
623 $metadata[
'provider'] .
' !== ' . $provider,
627 return $failHandler();
632 if ( isset( $metadata[
'providerMetadata'] ) ) {
633 if ( $providerMetadata ===
null ) {
634 $newParams[
'metadata'] = $metadata[
'providerMetadata'];
637 $newProviderMetadata = $provider->mergeMetadata(
638 $metadata[
'providerMetadata'], $providerMetadata
640 if ( $newProviderMetadata !== $providerMetadata ) {
641 $newParams[
'metadata'] = $newProviderMetadata;
644 $this->logger->warning(
645 'Session "{session}": Metadata merge failed: {exception}',
651 return $failHandler();
661 if ( $metadata[
'userId'] ) {
663 } elseif ( $metadata[
'userName'] !==
null ) {
668 }
catch ( \InvalidArgumentException $ex ) {
669 $this->logger->error(
'Session "{session}": {exception}', [
673 return $failHandler();
675 $newParams[
'userInfo'] = $userInfo;
679 if ( $metadata[
'userId'] ) {
680 if ( $metadata[
'userId'] !== $userInfo->getId() ) {
681 $this->logger->warning(
682 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
685 'uid_a' => $metadata[
'userId'],
686 'uid_b' => $userInfo->getId(),
688 return $failHandler();
692 if ( $metadata[
'userName'] !==
null &&
693 $userInfo->getName() !== $metadata[
'userName']
695 $this->logger->warning(
696 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
699 'uname_a' => $metadata[
'userName'],
700 'uname_b' => $userInfo->getName(),
702 return $failHandler();
705 } elseif ( $metadata[
'userName'] !==
null ) {
706 if ( $metadata[
'userName'] !== $userInfo->getName() ) {
707 $this->logger->warning(
708 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
711 'uname_a' => $metadata[
'userName'],
712 'uname_b' => $userInfo->getName(),
714 return $failHandler();
716 } elseif ( !$userInfo->isAnon() ) {
719 $this->logger->warning(
720 'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
724 return $failHandler();
729 if ( $metadata[
'userToken'] !==
null &&
730 $userInfo->getToken() !== $metadata[
'userToken']
732 $this->logger->warning(
'Session "{session}": User token mismatch', [
735 return $failHandler();
737 if ( !$userInfo->isVerified() ) {
738 $newParams[
'userInfo'] = $userInfo->verified();
741 if ( !empty( $metadata[
'remember'] ) && !$info->
wasRemembered() ) {
742 $newParams[
'remembered'] =
true;
744 if ( !empty( $metadata[
'forceHTTPS'] ) && !$info->
forceHTTPS() ) {
745 $newParams[
'forceHTTPS'] =
true;
747 if ( !empty( $metadata[
'persisted'] ) && !$info->
wasPersisted() ) {
748 $newParams[
'persisted'] =
true;
752 $newParams[
'idIsSafe'] =
true;
757 $this->logger->warning(
758 'Session "{session}": Null provider and no metadata',
762 return $failHandler();
771 'Session "{session}": No user provided and provider cannot set user',
775 return $failHandler();
780 'Session "{session}": Unverified user provided and no metadata to auth it',
784 return $failHandler();
793 $newParams[
'idIsSafe'] =
true;
799 $newParams[
'copyFrom'] = $info;
805 if ( !$info->
getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
806 return $failHandler();
810 'metadata' => $providerMetadata,
818 $reason =
'Hook aborted';
819 if ( !$this->hookRunner->onSessionCheckInfo(
820 $reason, $info, $request, $metadata, $data )
822 $this->logger->warning(
'Session "{session}": ' . $reason, [
825 return $failHandler();
841 if ( defined(
'MW_NO_SESSION' ) ) {
844 $this->logger->error(
'Sessions are supposed to be disabled for this entry point', [
845 'exception' =>
new \BadMethodCallException(
'Sessions are disabled for this entry point' ),
848 throw new \BadMethodCallException(
'Sessions are disabled for this entry point' );
853 $id = $info->
getId();
855 if ( !isset( $this->allSessionBackends[$id] ) ) {
856 if ( !isset( $this->allSessionIds[$id] ) ) {
857 $this->allSessionIds[$id] =
new SessionId( $id );
860 $this->allSessionIds[$id],
864 $this->hookContainer,
865 $this->config->get(
'ObjectCacheSessionExpiry' )
867 $this->allSessionBackends[$id] = $backend;
868 $delay = $backend->delaySave();
870 $backend = $this->allSessionBackends[$id];
871 $delay = $backend->delaySave();
876 $backend->setRememberUser(
true );
881 $session = $backend->getSession( $request );
887 \Wikimedia\ScopedCallback::consume( $delay );
897 $id = $backend->
getId();
898 if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
899 $this->allSessionBackends[$id] !== $backend ||
902 throw new \InvalidArgumentException(
'Backend was not registered with this SessionManager' );
905 unset( $this->allSessionBackends[$id] );
916 $oldId = (string)$sessionId;
917 if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
918 $this->allSessionBackends[$oldId] !== $backend ||
919 $this->allSessionIds[$oldId] !== $sessionId
921 throw new \InvalidArgumentException(
'Backend was not registered with this SessionManager' );
926 unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
927 $sessionId->setId( $newId );
928 $this->allSessionBackends[$newId] = $backend;
929 $this->allSessionIds[$newId] = $sessionId;
939 $key = $this->store->makeKey(
'MWSession', $id );
940 }
while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
950 $handler->
setManager( $this, $this->store, $this->logger );
959 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
961 throw new MWException( __METHOD__ .
' may only be called from unit tests!' );
965 self::$globalSession =
null;
966 self::$globalSessionRequest =
null;
Class representing a cache/ephemeral data store.
Wrapper around a BagOStuff that caches data in memory.
WebRequest clone which takes values from a provided array.
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
saveSettings()
Save this user's settings into the database.
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
setSessionId(SessionId $sessionId)
Set the session for this request.
Interface for configuration instances.