28 use Psr\Log\LoggerInterface;
35 use Wikimedia\ObjectFactory;
93 if ( self::$instance ===
null ) {
94 self::$instance =
new self();
116 !self::$globalSession
117 || self::$globalSessionRequest !==
$request
118 || $id !==
'' && self::$globalSession->getId() !== $id
120 self::$globalSessionRequest =
$request;
130 self::$globalSession =
$request->getSession();
149 if ( isset(
$options[
'config'] ) ) {
151 if ( !$this->config instanceof
Config ) {
152 throw new \InvalidArgumentException(
153 '$options[\'config\'] must be an instance of Config'
160 if ( isset(
$options[
'logger'] ) ) {
161 if ( !
$options[
'logger'] instanceof LoggerInterface ) {
162 throw new \InvalidArgumentException(
163 '$options[\'logger\'] must be an instance of LoggerInterface'
173 throw new \InvalidArgumentException(
174 '$options[\'store\'] must be an instance of BagOStuff'
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!' );
315 $user->saveSettings();
319 $authUser->resetAuthToken();
323 $provider->invalidateSessionsForUser(
$user );
329 if ( defined(
'MW_NO_SESSION' ) &&
MW_NO_SESSION !==
'warn' ) {
333 if ( $this->varyHeaders ===
null ) {
337 if ( !isset( $headers[
$header] ) ) {
345 $this->varyHeaders = $headers;
352 if ( defined(
'MW_NO_SESSION' ) &&
MW_NO_SESSION !==
'warn' ) {
356 if ( $this->varyCookies ===
null ) {
359 $cookies = array_merge( $cookies, $provider->getVaryCookies() );
361 $this->varyCookies = array_values( array_unique( $cookies ) );
372 return is_string( $id ) && preg_match(
'/^[a-zA-Z0-9_-]{32,}$/', $id );
390 return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser(
392 \
MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION,
411 $provider->preventSessionsForUser(
$username );
422 return !empty( $this->preventUsers[
$username] );
430 if ( $this->sessionProviders ===
null ) {
431 $this->sessionProviders = [];
432 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 if ( isset( $this->sessionProviders[(
string)$provider] ) ) {
438 throw new \UnexpectedValueException(
"Duplicate provider name \"$provider\"" );
440 $this->sessionProviders[(
string)$provider] = $provider;
458 return isset( $providers[
$name] ) ? $providers[
$name] :
null;
466 if ( $this->allSessionBackends ) {
467 $this->logger->debug(
'Saving all sessions on shutdown' );
468 if ( session_id() !==
'' ) {
470 session_write_close();
473 foreach ( $this->allSessionBackends
as $backend ) {
474 $backend->shutdown();
488 $info = $provider->provideSessionInfo(
$request );
492 if ( $info->getProvider() !== $provider ) {
493 throw new \UnexpectedValueException(
494 "$provider returned session info for a different provider: $info"
503 usort( $infos,
'MediaWiki\\Session\\SessionInfo::compare' );
506 $info = array_pop( $infos );
510 $info = array_pop( $infos );
521 $info->getProvider()->unpersistSession(
$request );
526 $info->getProvider()->unpersistSession(
$request );
530 if ( count( $retInfos ) > 1 ) {
531 $ex = new \OverflowException(
532 'Multiple sessions for this request tied for top priority: ' . implode(
', ', $retInfos )
534 $ex->sessionInfos = $retInfos;
538 return $retInfos ? $retInfos[0] :
null;
549 $key = $this->
store->makeKey(
'MWSession', $info->
getId() );
556 $failHandler =
function ()
use ( $key, &$info,
$request ) {
557 $this->
store->delete( $key );
561 $failHandler =
function () {
568 if (
$blob !==
false ) {
570 if ( !is_array(
$blob ) ) {
571 $this->logger->warning(
'Session "{session}": Bad data', [
574 $this->
store->delete( $key );
575 return $failHandler();
579 if ( !isset(
$blob[
'data'] ) || !is_array(
$blob[
'data'] ) ||
580 !isset(
$blob[
'metadata'] ) || !is_array(
$blob[
'metadata'] )
582 $this->logger->warning(
'Session "{session}": Bad data structure', [
585 $this->
store->delete( $key );
586 return $failHandler();
589 $data =
$blob[
'data'];
590 $metadata =
$blob[
'metadata'];
594 if ( !array_key_exists(
'userId', $metadata ) ||
595 !array_key_exists(
'userName', $metadata ) ||
596 !array_key_exists(
'userToken', $metadata ) ||
597 !array_key_exists(
'provider', $metadata )
599 $this->logger->warning(
'Session "{session}": Bad metadata', [
602 $this->
store->delete( $key );
603 return $failHandler();
608 if ( $provider ===
null ) {
609 $newParams[
'provider'] = $provider = $this->
getProvider( $metadata[
'provider'] );
611 $this->logger->warning(
612 'Session "{session}": Unknown provider ' . $metadata[
'provider'],
617 $this->
store->delete( $key );
618 return $failHandler();
620 } elseif ( $metadata[
'provider'] !== (
string)$provider ) {
621 $this->logger->warning(
'Session "{session}": Wrong provider ' .
622 $metadata[
'provider'] .
' !== ' . $provider,
626 return $failHandler();
631 if ( isset( $metadata[
'providerMetadata'] ) ) {
632 if ( $providerMetadata ===
null ) {
633 $newParams[
'metadata'] = $metadata[
'providerMetadata'];
636 $newProviderMetadata = $provider->mergeMetadata(
637 $metadata[
'providerMetadata'], $providerMetadata
639 if ( $newProviderMetadata !== $providerMetadata ) {
640 $newParams[
'metadata'] = $newProviderMetadata;
643 $this->logger->warning(
644 'Session "{session}": Metadata merge failed: {exception}',
650 return $failHandler();
660 if ( $metadata[
'userId'] ) {
662 } elseif ( $metadata[
'userName'] !==
null ) {
667 }
catch ( \InvalidArgumentException $ex ) {
668 $this->logger->error(
'Session "{session}": {exception}', [
672 return $failHandler();
674 $newParams[
'userInfo'] = $userInfo;
678 if ( $metadata[
'userId'] ) {
679 if ( $metadata[
'userId'] !== $userInfo->getId() ) {
680 $this->logger->warning(
681 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
684 'uid_a' => $metadata[
'userId'],
685 'uid_b' => $userInfo->
getId(),
687 return $failHandler();
691 if ( $metadata[
'userName'] !==
null &&
692 $userInfo->getName() !== $metadata[
'userName']
694 $this->logger->warning(
695 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
698 'uname_a' => $metadata[
'userName'],
699 'uname_b' => $userInfo->getName(),
701 return $failHandler();
704 } elseif ( $metadata[
'userName'] !==
null ) {
705 if ( $metadata[
'userName'] !== $userInfo->getName() ) {
706 $this->logger->warning(
707 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
710 'uname_a' => $metadata[
'userName'],
711 'uname_b' => $userInfo->getName(),
713 return $failHandler();
715 } elseif ( !$userInfo->isAnon() ) {
718 $this->logger->warning(
719 'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
723 return $failHandler();
728 if ( $metadata[
'userToken'] !==
null &&
729 $userInfo->getToken() !== $metadata[
'userToken']
731 $this->logger->warning(
'Session "{session}": User token mismatch', [
734 return $failHandler();
736 if ( !$userInfo->isVerified() ) {
737 $newParams[
'userInfo'] = $userInfo->verified();
740 if ( !empty( $metadata[
'remember'] ) && !$info->
wasRemembered() ) {
741 $newParams[
'remembered'] =
true;
743 if ( !empty( $metadata[
'forceHTTPS'] ) && !$info->
forceHTTPS() ) {
744 $newParams[
'forceHTTPS'] =
true;
746 if ( !empty( $metadata[
'persisted'] ) && !$info->
wasPersisted() ) {
747 $newParams[
'persisted'] =
true;
751 $newParams[
'idIsSafe'] =
true;
756 $this->logger->warning(
757 'Session "{session}": Null provider and no metadata',
761 return $failHandler();
770 'Session "{session}": No user provided and provider cannot set user',
774 return $failHandler();
779 'Session "{session}": Unverified user provided and no metadata to auth it',
783 return $failHandler();
792 $newParams[
'idIsSafe'] =
true;
798 $newParams[
'copyFrom'] = $info;
805 return $failHandler();
809 'metadata' => $providerMetadata,
817 $reason =
'Hook aborted';
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->config->get(
'ObjectCacheSessionExpiry' )
866 $this->allSessionBackends[$id] = $backend;
867 $delay = $backend->delaySave();
869 $backend = $this->allSessionBackends[$id];
870 $delay = $backend->delaySave();
875 $backend->setRememberUser(
true );
879 $request->setSessionId( $backend->getSessionId() );
880 $session = $backend->getSession(
$request );
886 \Wikimedia\ScopedCallback::consume( $delay );
896 $id = $backend->
getId();
897 if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
898 $this->allSessionBackends[$id] !== $backend ||
901 throw new \InvalidArgumentException(
'Backend was not registered with this SessionManager' );
904 unset( $this->allSessionBackends[$id] );
915 $oldId = (
string)$sessionId;
916 if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
917 $this->allSessionBackends[$oldId] !== $backend ||
918 $this->allSessionIds[$oldId] !== $sessionId
920 throw new \InvalidArgumentException(
'Backend was not registered with this SessionManager' );
925 unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
926 $sessionId->setId( $newId );
927 $this->allSessionBackends[$newId] = $backend;
928 $this->allSessionIds[$newId] = $sessionId;
938 $key = $this->
store->makeKey(
'MWSession', $id );
939 }
while ( isset( $this->allSessionIds[$id] ) || is_array( $this->
store->get( $key ) ) );
957 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
959 throw new MWException( __METHOD__ .
' may only be called from unit tests!' );
963 self::$globalSession =
null;
964 self::$globalSessionRequest =
null;