28 use Psr\Log\LoggerInterface;
35 use Wikimedia\ObjectFactory;
92 if ( self::$instance ===
null ) {
93 self::$instance =
new self();
115 !self::$globalSession
116 || self::$globalSessionRequest !== $request
117 || $id !==
'' && self::$globalSession->getId() !== $id
119 self::$globalSessionRequest = $request;
129 self::$globalSession = $request->getSession();
134 self::$globalSession =
self::singleton()->getSessionById( $id,
true, $request )
135 ?: $request->getSession();
148 if ( isset( $options[
'config'] ) ) {
149 $this->config = $options[
'config'];
150 if ( !$this->config instanceof
Config ) {
151 throw new \InvalidArgumentException(
152 '$options[\'config\'] must be an instance of Config'
159 if ( isset( $options[
'logger'] ) ) {
160 if ( !$options[
'logger'] instanceof LoggerInterface ) {
161 throw new \InvalidArgumentException(
162 '$options[\'logger\'] must be an instance of LoggerInterface'
170 if ( isset( $options[
'store'] ) ) {
171 if ( !$options[
'store'] instanceof
BagOStuff ) {
172 throw new \InvalidArgumentException(
173 '$options[\'store\'] must be an instance of BagOStuff'
176 $store = $options[
'store'];
180 $this->logger->debug(
'SessionManager using store ' . get_class(
$store ) );
183 register_shutdown_function( [ $this,
'shutdown' ] );
202 if ( !self::validateSessionId( $id ) ) {
203 throw new \InvalidArgumentException(
'Invalid session ID' );
213 if ( isset( $this->allSessionBackends[$id] ) ) {
218 $key = $this->store->makeKey(
'MWSession', $id );
219 if ( is_array( $this->store->get( $key ) ) ) {
226 if ( $create && $session ===
null ) {
230 }
catch ( \Exception $ex ) {
231 $this->logger->error(
'Failed to create empty session: {exception}',
233 'method' => __METHOD__,
254 if ( $id !==
null ) {
255 if ( !self::validateSessionId( $id ) ) {
256 throw new \InvalidArgumentException(
'Invalid session ID' );
259 $key = $this->store->makeKey(
'MWSession', $id );
260 if ( is_array( $this->store->get( $key ) ) ) {
261 throw new \InvalidArgumentException(
'Session ID already exists' );
270 $info = $provider->newSessionInfo( $id );
274 if ( $info->getProvider() !== $provider ) {
275 throw new \UnexpectedValueException(
276 "$provider returned an empty session info for a different provider: $info"
279 if ( $id !==
null && $info->getId() !== $id ) {
280 throw new \UnexpectedValueException(
281 "$provider returned empty session info with a wrong id: " .
282 $info->getId() .
' != ' . $id
285 if ( !$info->isIdSafe() ) {
286 throw new \UnexpectedValueException(
287 "$provider returned empty session info with id flagged unsafe"
291 if ( $compare > 0 ) {
294 if ( $compare === 0 ) {
302 if ( count( $infos ) > 1 ) {
303 throw new \UnexpectedValueException(
304 'Multiple empty sessions tied for top priority: ' . implode(
', ', $infos )
306 } elseif ( count( $infos ) < 1 ) {
307 throw new \UnexpectedValueException(
'No provider could provide an empty session!' );
318 $provider->invalidateSessionsForUser( $user );
325 if ( defined(
'MW_NO_SESSION' ) &&
MW_NO_SESSION !==
'warn' ) {
329 if ( $this->varyHeaders ===
null ) {
332 foreach ( $provider->getVaryHeaders() as
$header => $options ) {
333 # Note that the $options value returned has been deprecated
338 $this->varyHeaders = $headers;
346 if ( defined(
'MW_NO_SESSION' ) &&
MW_NO_SESSION !==
'warn' ) {
350 if ( $this->varyCookies ===
null ) {
353 $cookies = array_merge( $cookies, $provider->getVaryCookies() );
355 $this->varyCookies = array_values( array_unique( $cookies ) );
366 return is_string( $id ) && preg_match(
'/^[a-zA-Z0-9_-]{32,}$/', $id );
384 $this->preventUsers[$username] =
true;
388 $provider->preventSessionsForUser( $username );
399 return !empty( $this->preventUsers[$username] );
407 if ( $this->sessionProviders ===
null ) {
408 $this->sessionProviders = [];
409 foreach ( $this->config->get(
'SessionProviders' ) as $spec ) {
410 $provider = ObjectFactory::getObjectFromSpec( $spec );
411 $provider->setLogger( $this->logger );
412 $provider->setConfig( $this->config );
413 $provider->setManager( $this );
414 if ( isset( $this->sessionProviders[(
string)$provider] ) ) {
416 throw new \UnexpectedValueException(
"Duplicate provider name \"$provider\"" );
418 $this->sessionProviders[(string)$provider] = $provider;
436 return $providers[$name] ??
null;
444 if ( $this->allSessionBackends ) {
445 $this->logger->debug(
'Saving all sessions on shutdown' );
446 if ( session_id() !==
'' ) {
448 session_write_close();
451 foreach ( $this->allSessionBackends as $backend ) {
452 $backend->shutdown();
466 $info = $provider->provideSessionInfo( $request );
470 if ( $info->getProvider() !== $provider ) {
471 throw new \UnexpectedValueException(
472 "$provider returned session info for a different provider: $info"
481 usort( $infos,
'MediaWiki\\Session\\SessionInfo::compare' );
484 $info = array_pop( $infos );
488 $info = array_pop( $infos );
499 $info->getProvider()->unpersistSession( $request );
504 $info->getProvider()->unpersistSession( $request );
508 if ( count( $retInfos ) > 1 ) {
511 'Multiple sessions for this request tied for top priority: ' . implode(
', ', $retInfos )
515 return $retInfos ? $retInfos[0] :
null;
526 $key = $this->store->makeKey(
'MWSession', $info->
getId() );
527 $blob = $this->store->get( $key );
533 $failHandler =
function () use ( $key, &$info, $request ) {
534 $this->store->delete( $key );
538 $failHandler =
function () {
545 if (
$blob !==
false ) {
547 if ( !is_array(
$blob ) ) {
548 $this->logger->warning(
'Session "{session}": Bad data', [
551 $this->store->delete( $key );
552 return $failHandler();
556 if ( !isset(
$blob[
'data'] ) || !is_array(
$blob[
'data'] ) ||
557 !isset(
$blob[
'metadata'] ) || !is_array(
$blob[
'metadata'] )
559 $this->logger->warning(
'Session "{session}": Bad data structure', [
562 $this->store->delete( $key );
563 return $failHandler();
566 $data =
$blob[
'data'];
567 $metadata =
$blob[
'metadata'];
571 if ( !array_key_exists(
'userId', $metadata ) ||
572 !array_key_exists(
'userName', $metadata ) ||
573 !array_key_exists(
'userToken', $metadata ) ||
574 !array_key_exists(
'provider', $metadata )
576 $this->logger->warning(
'Session "{session}": Bad metadata', [
579 $this->store->delete( $key );
580 return $failHandler();
585 if ( $provider ===
null ) {
586 $newParams[
'provider'] = $provider = $this->
getProvider( $metadata[
'provider'] );
588 $this->logger->warning(
589 'Session "{session}": Unknown provider ' . $metadata[
'provider'],
594 $this->store->delete( $key );
595 return $failHandler();
597 } elseif ( $metadata[
'provider'] !== (
string)$provider ) {
598 $this->logger->warning(
'Session "{session}": Wrong provider ' .
599 $metadata[
'provider'] .
' !== ' . $provider,
603 return $failHandler();
608 if ( isset( $metadata[
'providerMetadata'] ) ) {
609 if ( $providerMetadata ===
null ) {
610 $newParams[
'metadata'] = $metadata[
'providerMetadata'];
613 $newProviderMetadata = $provider->mergeMetadata(
614 $metadata[
'providerMetadata'], $providerMetadata
616 if ( $newProviderMetadata !== $providerMetadata ) {
617 $newParams[
'metadata'] = $newProviderMetadata;
620 $this->logger->warning(
621 'Session "{session}": Metadata merge failed: {exception}',
627 return $failHandler();
637 if ( $metadata[
'userId'] ) {
639 } elseif ( $metadata[
'userName'] !==
null ) {
644 }
catch ( \InvalidArgumentException $ex ) {
645 $this->logger->error(
'Session "{session}": {exception}', [
649 return $failHandler();
651 $newParams[
'userInfo'] = $userInfo;
655 if ( $metadata[
'userId'] ) {
656 if ( $metadata[
'userId'] !== $userInfo->getId() ) {
657 $this->logger->warning(
658 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
661 'uid_a' => $metadata[
'userId'],
662 'uid_b' => $userInfo->
getId(),
664 return $failHandler();
668 if ( $metadata[
'userName'] !==
null &&
669 $userInfo->getName() !== $metadata[
'userName']
671 $this->logger->warning(
672 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
675 'uname_a' => $metadata[
'userName'],
676 'uname_b' => $userInfo->getName(),
678 return $failHandler();
681 } elseif ( $metadata[
'userName'] !==
null ) {
682 if ( $metadata[
'userName'] !== $userInfo->getName() ) {
683 $this->logger->warning(
684 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
687 'uname_a' => $metadata[
'userName'],
688 'uname_b' => $userInfo->getName(),
690 return $failHandler();
692 } elseif ( !$userInfo->isAnon() ) {
695 $this->logger->warning(
696 'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
700 return $failHandler();
705 if ( $metadata[
'userToken'] !==
null &&
706 $userInfo->getToken() !== $metadata[
'userToken']
708 $this->logger->warning(
'Session "{session}": User token mismatch', [
711 return $failHandler();
713 if ( !$userInfo->isVerified() ) {
714 $newParams[
'userInfo'] = $userInfo->verified();
717 if ( !empty( $metadata[
'remember'] ) && !$info->
wasRemembered() ) {
718 $newParams[
'remembered'] =
true;
720 if ( !empty( $metadata[
'forceHTTPS'] ) && !$info->
forceHTTPS() ) {
721 $newParams[
'forceHTTPS'] =
true;
723 if ( !empty( $metadata[
'persisted'] ) && !$info->
wasPersisted() ) {
724 $newParams[
'persisted'] =
true;
728 $newParams[
'idIsSafe'] =
true;
733 $this->logger->warning(
734 'Session "{session}": Null provider and no metadata',
738 return $failHandler();
747 'Session "{session}": No user provided and provider cannot set user',
751 return $failHandler();
756 'Session "{session}": Unverified user provided and no metadata to auth it',
760 return $failHandler();
769 $newParams[
'idIsSafe'] =
true;
775 $newParams[
'copyFrom'] = $info;
781 if ( !$info->
getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
782 return $failHandler();
786 'metadata' => $providerMetadata,
794 $reason =
'Hook aborted';
797 [ &$reason, $info, $request, $metadata, $data ]
799 $this->logger->warning(
'Session "{session}": ' . $reason, [
802 return $failHandler();
818 if ( defined(
'MW_NO_SESSION' ) ) {
822 $this->logger->error(
'Sessions are supposed to be disabled for this entry point', [
823 'exception' =>
new \BadMethodCallException(
'Sessions are disabled for this entry point' ),
826 throw new \BadMethodCallException(
'Sessions are disabled for this entry point' );
831 $id = $info->
getId();
833 if ( !isset( $this->allSessionBackends[$id] ) ) {
834 if ( !isset( $this->allSessionIds[$id] ) ) {
835 $this->allSessionIds[$id] =
new SessionId( $id );
838 $this->allSessionIds[$id],
842 $this->config->get(
'ObjectCacheSessionExpiry' )
844 $this->allSessionBackends[$id] = $backend;
847 $backend = $this->allSessionBackends[$id];
848 $delay = $backend->delaySave();
853 $backend->setRememberUser(
true );
858 $session = $backend->getSession( $request );
864 \Wikimedia\ScopedCallback::consume( $delay );
874 $id = $backend->
getId();
875 if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
876 $this->allSessionBackends[$id] !== $backend ||
879 throw new \InvalidArgumentException(
'Backend was not registered with this SessionManager' );
882 unset( $this->allSessionBackends[$id] );
893 $oldId = (string)$sessionId;
894 if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
895 $this->allSessionBackends[$oldId] !== $backend ||
896 $this->allSessionIds[$oldId] !== $sessionId
898 throw new \InvalidArgumentException(
'Backend was not registered with this SessionManager' );
903 unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
904 $sessionId->setId( $newId );
905 $this->allSessionBackends[$newId] = $backend;
906 $this->allSessionIds[$newId] = $sessionId;
916 $key = $this->store->makeKey(
'MWSession', $id );
917 }
while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
927 $handler->
setManager( $this, $this->store, $this->logger );
935 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
937 throw new MWException( __METHOD__ .
' may only be called from unit tests!' );
941 self::$globalSession =
null;
942 self::$globalSessionRequest =
null;