35use Psr\Log\LoggerInterface;
39use Wikimedia\ObjectFactory;
134 if ( self::$instance ===
null ) {
135 self::$instance =
new self();
153 $request = \RequestContext::getMain()->getRequest();
155 !self::$globalSession
156 || self::$globalSessionRequest !== $request
157 || $id !==
'' && self::$globalSession->getId() !== $id
159 self::$globalSessionRequest = $request;
169 self::$globalSession = $request->getSession();
174 self::$globalSession =
self::singleton()->getSessionById( $id,
true, $request )
175 ?: $request->getSession();
188 if ( isset( $options[
'config'] ) ) {
189 $this->config = $options[
'config'];
190 if ( !$this->config instanceof
Config ) {
191 throw new \InvalidArgumentException(
192 '$options[\'config\'] must be an instance of Config'
199 if ( isset( $options[
'logger'] ) ) {
200 if ( !$options[
'logger'] instanceof LoggerInterface ) {
201 throw new \InvalidArgumentException(
202 '$options[\'logger\'] must be an instance of LoggerInterface'
205 $this->setLogger( $options[
'logger'] );
207 $this->setLogger( \
MediaWiki\Logger\LoggerFactory::getInstance(
'session' ) );
210 if ( isset( $options[
'hookContainer'] ) ) {
211 $this->setHookContainer( $options[
'hookContainer'] );
216 if ( isset( $options[
'store'] ) ) {
217 if ( !$options[
'store'] instanceof
BagOStuff ) {
218 throw new \InvalidArgumentException(
219 '$options[\'store\'] must be an instance of BagOStuff'
222 $store = $options[
'store'];
224 $store = \ObjectCache::getInstance( $this->config->get(
'SessionCacheType' ) );
227 $this->logger->debug(
'SessionManager using store ' . get_class( $store ) );
229 $this->userNameUtils = MediawikiServices::getInstance()->getUserNameUtils();
231 register_shutdown_function( [ $this,
'shutdown' ] );
235 $this->logger = $logger;
243 $this->hookContainer = $hookContainer;
244 $this->hookRunner =
new HookRunner( $hookContainer );
248 $info = $this->getSessionInfoForRequest( $request );
251 $session = $this->getEmptySession( $request );
253 $session = $this->getSessionFromInfo( $info, $request );
259 if ( !self::validateSessionId( $id ) ) {
260 throw new \InvalidArgumentException(
'Invalid session ID' );
270 if ( isset( $this->allSessionBackends[$id] ) ) {
271 return $this->getSessionFromInfo( $info, $request );
275 $key = $this->store->makeKey(
'MWSession', $id );
276 if ( is_array( $this->store->get( $key ) ) ) {
278 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
279 $session = $this->getSessionFromInfo( $info, $request );
283 if ( $create && $session ===
null ) {
286 $session = $this->getEmptySessionInternal( $request, $id );
287 }
catch ( \Exception $ex ) {
288 $this->logger->error(
'Failed to create empty session: {exception}',
290 'method' => __METHOD__,
301 return $this->getEmptySessionInternal( $request );
311 if ( $id !==
null ) {
312 if ( !self::validateSessionId( $id ) ) {
313 throw new \InvalidArgumentException(
'Invalid session ID' );
316 $key = $this->store->makeKey(
'MWSession', $id );
317 if ( is_array( $this->store->get( $key ) ) ) {
318 throw new \InvalidArgumentException(
'Session ID already exists' );
326 foreach ( $this->getProviders() as $provider ) {
327 $info = $provider->newSessionInfo( $id );
331 if ( $info->getProvider() !== $provider ) {
332 throw new \UnexpectedValueException(
333 "$provider returned an empty session info for a different provider: $info"
336 if ( $id !==
null && $info->getId() !== $id ) {
337 throw new \UnexpectedValueException(
338 "$provider returned empty session info with a wrong id: " .
339 $info->getId() .
' != ' . $id
342 if ( !$info->isIdSafe() ) {
343 throw new \UnexpectedValueException(
344 "$provider returned empty session info with id flagged unsafe"
348 if ( $compare > 0 ) {
351 if ( $compare === 0 ) {
359 if ( count( $infos ) > 1 ) {
360 throw new \UnexpectedValueException(
361 'Multiple empty sessions tied for top priority: ' . implode(
', ', $infos )
363 } elseif ( count( $infos ) < 1 ) {
364 throw new \UnexpectedValueException(
'No provider could provide an empty session!' );
367 return $this->getSessionFromInfo( $infos[0], $request );
374 foreach ( $this->getProviders() as $provider ) {
375 $provider->invalidateSessionsForUser( $user );
381 if ( defined(
'MW_NO_SESSION' ) &&
MW_NO_SESSION !==
'warn' ) {
385 if ( $this->varyHeaders ===
null ) {
387 foreach ( $this->getProviders() as $provider ) {
388 foreach ( $provider->getVaryHeaders() as
$header => $options ) {
389 # Note that the $options value returned has been deprecated
394 $this->varyHeaders = $headers;
396 return $this->varyHeaders;
401 if ( defined(
'MW_NO_SESSION' ) &&
MW_NO_SESSION !==
'warn' ) {
405 if ( $this->varyCookies ===
null ) {
407 foreach ( $this->getProviders() as $provider ) {
408 $cookies = array_merge( $cookies, $provider->getVaryCookies() );
410 $this->varyCookies = array_values( array_unique( $cookies ) );
412 return $this->varyCookies;
421 return is_string( $id ) && preg_match(
'/^[a-zA-Z0-9_-]{32,}$/', $id );
438 $this->preventUsers[$username] =
true;
441 foreach ( $this->getProviders() as $provider ) {
442 $provider->preventSessionsForUser( $username );
453 return !empty( $this->preventUsers[$username] );
461 if ( $this->sessionProviders ===
null ) {
462 $this->sessionProviders = [];
463 foreach ( $this->config->get(
'SessionProviders' ) as $spec ) {
465 $provider = ObjectFactory::getObjectFromSpec( $spec );
470 $this->hookContainer,
473 if ( isset( $this->sessionProviders[(
string)$provider] ) ) {
475 throw new \UnexpectedValueException(
"Duplicate provider name \"$provider\"" );
477 $this->sessionProviders[(string)$provider] = $provider;
480 return $this->sessionProviders;
494 $providers = $this->getProviders();
495 return $providers[$name] ??
null;
503 if ( $this->allSessionBackends ) {
504 $this->logger->debug(
'Saving all sessions on shutdown' );
505 if ( session_id() !==
'' ) {
507 session_write_close();
510 foreach ( $this->allSessionBackends as $backend ) {
511 $backend->shutdown();
524 foreach ( $this->getProviders() as $provider ) {
525 $info = $provider->provideSessionInfo( $request );
529 if ( $info->getProvider() !== $provider ) {
530 throw new \UnexpectedValueException(
531 "$provider returned session info for a different provider: $info"
540 usort( $infos, [ SessionInfo::class,
'compare' ] );
543 $info = array_pop( $infos );
544 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
548 $info = array_pop( $infos );
553 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
559 $this->logUnpersist( $info, $request );
560 $info->getProvider()->unpersistSession( $request );
565 $this->logUnpersist( $info, $request );
566 $info->getProvider()->unpersistSession( $request );
570 if ( count( $retInfos ) > 1 ) {
573 'Multiple sessions for this request tied for top priority: ' . implode(
', ', $retInfos )
577 return $retInfos ? $retInfos[0] :
null;
588 $key = $this->store->makeKey(
'MWSession', $info->
getId() );
589 $blob = $this->store->get( $key );
595 $failHandler =
function () use ( $key, &$info, $request ) {
596 $this->store->delete( $key );
597 return $this->loadSessionInfoFromStore( $info, $request );
600 $failHandler =
static function () {
607 if (
$blob !==
false ) {
609 if ( !is_array(
$blob ) ) {
610 $this->logger->warning(
'Session "{session}": Bad data', [
613 $this->store->delete( $key );
614 return $failHandler();
618 if ( !isset(
$blob[
'data'] ) || !is_array(
$blob[
'data'] ) ||
619 !isset(
$blob[
'metadata'] ) || !is_array(
$blob[
'metadata'] )
621 $this->logger->warning(
'Session "{session}": Bad data structure', [
624 $this->store->delete( $key );
625 return $failHandler();
628 $data =
$blob[
'data'];
629 $metadata =
$blob[
'metadata'];
633 if ( !array_key_exists(
'userId', $metadata ) ||
634 !array_key_exists(
'userName', $metadata ) ||
635 !array_key_exists(
'userToken', $metadata ) ||
636 !array_key_exists(
'provider', $metadata )
638 $this->logger->warning(
'Session "{session}": Bad metadata', [
641 $this->store->delete( $key );
642 return $failHandler();
647 if ( $provider ===
null ) {
648 $newParams[
'provider'] = $provider = $this->getProvider( $metadata[
'provider'] );
650 $this->logger->warning(
651 'Session "{session}": Unknown provider ' . $metadata[
'provider'],
656 $this->store->delete( $key );
657 return $failHandler();
659 } elseif ( $metadata[
'provider'] !== (
string)$provider ) {
660 $this->logger->warning(
'Session "{session}": Wrong provider ' .
661 $metadata[
'provider'] .
' !== ' . $provider,
665 return $failHandler();
670 if ( isset( $metadata[
'providerMetadata'] ) ) {
671 if ( $providerMetadata ===
null ) {
672 $newParams[
'metadata'] = $metadata[
'providerMetadata'];
675 $newProviderMetadata = $provider->mergeMetadata(
676 $metadata[
'providerMetadata'], $providerMetadata
678 if ( $newProviderMetadata !== $providerMetadata ) {
679 $newParams[
'metadata'] = $newProviderMetadata;
682 $this->logger->warning(
683 'Session "{session}": Metadata merge failed: {exception}',
689 return $failHandler();
699 if ( $metadata[
'userId'] ) {
701 } elseif ( $metadata[
'userName'] !==
null ) {
706 }
catch ( \InvalidArgumentException $ex ) {
707 $this->logger->error(
'Session "{session}": {exception}', [
711 return $failHandler();
713 $newParams[
'userInfo'] = $userInfo;
717 if ( $metadata[
'userId'] ) {
718 if ( $metadata[
'userId'] !== $userInfo->getId() ) {
719 $this->logger->warning(
720 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
723 'uid_a' => $metadata[
'userId'],
724 'uid_b' => $userInfo->getId(),
726 return $failHandler();
730 if ( $metadata[
'userName'] !==
null &&
731 $userInfo->getName() !== $metadata[
'userName']
733 $this->logger->warning(
734 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
737 'uname_a' => $metadata[
'userName'],
738 'uname_b' => $userInfo->getName(),
740 return $failHandler();
743 } elseif ( $metadata[
'userName'] !==
null ) {
744 if ( $metadata[
'userName'] !== $userInfo->getName() ) {
745 $this->logger->warning(
746 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
749 'uname_a' => $metadata[
'userName'],
750 'uname_b' => $userInfo->getName(),
752 return $failHandler();
754 } elseif ( !$userInfo->isAnon() ) {
757 $this->logger->warning(
758 'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
762 return $failHandler();
767 if ( $metadata[
'userToken'] !==
null &&
768 $userInfo->getToken() !== $metadata[
'userToken']
770 $this->logger->warning(
'Session "{session}": User token mismatch', [
773 return $failHandler();
775 if ( !$userInfo->isVerified() ) {
776 $newParams[
'userInfo'] = $userInfo->verified();
779 if ( !empty( $metadata[
'remember'] ) && !$info->
wasRemembered() ) {
780 $newParams[
'remembered'] =
true;
782 if ( !empty( $metadata[
'forceHTTPS'] ) && !$info->
forceHTTPS() ) {
783 $newParams[
'forceHTTPS'] =
true;
785 if ( !empty( $metadata[
'persisted'] ) && !$info->
wasPersisted() ) {
786 $newParams[
'persisted'] =
true;
790 $newParams[
'idIsSafe'] =
true;
795 $this->logger->warning(
796 'Session "{session}": Null provider and no metadata',
800 return $failHandler();
809 'Session "{session}": No user provided and provider cannot set user',
813 return $failHandler();
818 'Session "{session}": Unverified user provided and no metadata to auth it',
822 return $failHandler();
831 $newParams[
'idIsSafe'] =
true;
837 $newParams[
'copyFrom'] = $info;
843 if ( !$info->
getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
844 return $failHandler();
848 'metadata' => $providerMetadata,
856 $reason =
'Hook aborted';
857 if ( !$this->hookRunner->onSessionCheckInfo(
858 $reason, $info, $request, $metadata, $data )
860 $this->logger->warning(
'Session "{session}": ' . $reason, [
863 return $failHandler();
879 if ( defined(
'MW_NO_SESSION' ) ) {
884 $this->logger->error(
'Sessions are supposed to be disabled for this entry point', [
885 'exception' =>
new \BadMethodCallException(
"Sessions are disabled for $ep entry point" ),
888 throw new \BadMethodCallException(
"Sessions are disabled for $ep entry point" );
893 $id = $info->
getId();
895 if ( !isset( $this->allSessionBackends[$id] ) ) {
896 if ( !isset( $this->allSessionIds[$id] ) ) {
897 $this->allSessionIds[$id] =
new SessionId( $id );
900 $this->allSessionIds[$id],
904 $this->hookContainer,
905 $this->config->get(
'ObjectCacheSessionExpiry' )
907 $this->allSessionBackends[$id] = $backend;
908 $delay = $backend->delaySave();
910 $backend = $this->allSessionBackends[$id];
911 $delay = $backend->delaySave();
916 $backend->setRememberUser(
true );
921 $session = $backend->getSession( $request );
927 \Wikimedia\ScopedCallback::consume( $delay );
937 $id = $backend->
getId();
938 if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
939 $this->allSessionBackends[$id] !== $backend ||
942 throw new \InvalidArgumentException(
'Backend was not registered with this SessionManager' );
945 unset( $this->allSessionBackends[$id] );
956 $oldId = (string)$sessionId;
957 if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
958 $this->allSessionBackends[$oldId] !== $backend ||
959 $this->allSessionIds[$oldId] !== $sessionId
961 throw new \InvalidArgumentException(
'Backend was not registered with this SessionManager' );
964 $newId = $this->generateSessionId();
966 unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
967 $sessionId->setId( $newId );
968 $this->allSessionBackends[$newId] = $backend;
969 $this->allSessionIds[$newId] = $sessionId;
979 $key = $this->store->makeKey(
'MWSession', $id );
980 }
while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
990 $handler->
setManager( $this, $this->store, $this->logger );
999 if ( !defined(
'MW_PHPUNIT_TEST' ) && !defined(
'MW_PARSER_TEST' ) ) {
1001 throw new MWException( __METHOD__ .
' may only be called from unit tests!' );
1005 self::$globalSession =
null;
1006 self::$globalSessionRequest =
null;
1011 'id' => $info->
getId(),
1014 'clientip' => $request->
getIP(),
1015 'userAgent' => $request->
getHeader(
'user-agent' ),
1019 $logData[
'user'] = $info->
getUserInfo()->getName();
1021 $logData[
'userVerified'] = $info->
getUserInfo()->isVerified();
1023 $this->logger->info(
'Failed to load session, unpersisting', $logData );
1038 $session = $session ?: self::getGlobalSession();
1039 $suspiciousIpExpiry = $this->config->get(
'SuspiciousIpExpiry' );
1041 if ( $suspiciousIpExpiry ===
false
1043 || !$session->isPersistent() || $session->getUser()->isAnon()
1050 $ip = $session->getRequest()->getIP();
1054 if ( $ip ===
'127.0.0.1' || $proxyLookup->isConfiguredProxy( $ip ) ) {
1057 $mwuser = $session->getRequest()->getCookie(
'mwuser-sessionId' );
1058 $now = \MWTimestamp::now( TS_UNIX );
1064 $data = $session->get(
'SessionManager-logPotentialSessionLeakage', [] )
1065 + [
'ip' =>
null,
'mwuser' =>
null,
'timestamp' => 0 ];
1069 ( $now - $data[
'timestamp'] > $suspiciousIpExpiry )
1071 $data[
'ip'] = $data[
'timestamp'] =
null;
1074 if ( $data[
'ip'] !== $ip || $data[
'mwuser'] !== $mwuser ) {
1075 $session->set(
'SessionManager-logPotentialSessionLeakage',
1076 [
'ip' => $ip,
'mwuser' => $mwuser,
'timestamp' => $now ] );
1079 $ipChanged = ( $data[
'ip'] && $data[
'ip'] !== $ip );
1080 $mwuserChanged = ( $data[
'mwuser'] && $data[
'mwuser'] !== $mwuser );
1081 $logLevel = $message =
null;
1089 $logLevel = LogLevel::INFO;
1090 $message =
'IP change within the same session';
1092 'oldIp' => $data[
'ip'],
1093 'oldIpRecorded' => $data[
'timestamp'],
1096 if ( $mwuserChanged ) {
1097 $logLevel = LogLevel::NOTICE;
1098 $message =
'mwuser change within the same session';
1100 'oldMwuser' => $data[
'mwuser'],
1101 'newMwuser' => $mwuser,
1104 if ( $ipChanged && $mwuserChanged ) {
1105 $logLevel = LogLevel::WARNING;
1106 $message =
'IP and mwuser change within the same session';
1110 'session' => $session->getId(),
1111 'user' => $session->getUser()->getName(),
1113 'userAgent' => $session->getRequest()->getHeader(
'user-agent' ),
1115 $logger = \MediaWiki\Logger\LoggerFactory::getInstance(
'session-ip' );
1116 $logger->log( $logLevel, $message, $logData );
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
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...
getIP()
Work out the IP address based on various globals For trusted proxies, use the XFF client IP (first of...
setSessionId(SessionId $sessionId)
Set the session for this request.
getHeader( $name, $flags=0)
Get a request header, or false if it isn't set.
Interface for configuration instances.