MediaWiki REL1_35
SessionManager.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
26use BagOStuff;
28use Config;
29use FauxRequest;
33use MWException;
34use Psr\Log\LoggerInterface;
35use User;
36use WebRequest;
37use Wikimedia\ObjectFactory;
38
54 private static $instance = null;
55
57 private static $globalSession = null;
58
60 private static $globalSessionRequest = null;
61
63 private $logger;
64
67
69 private $hookRunner;
70
72 private $config;
73
75 private $store;
76
78 private $sessionProviders = null;
79
81 private $varyCookies = null;
82
84 private $varyHeaders = null;
85
87 private $allSessionBackends = [];
88
90 private $allSessionIds = [];
91
93 private $preventUsers = [];
94
99 public static function singleton() {
100 if ( self::$instance === null ) {
101 self::$instance = new self();
102 }
103 return self::$instance;
104 }
105
114 public static function getGlobalSession() {
116 $id = '';
117 } else {
118 $id = session_id();
119 }
120
121 $request = \RequestContext::getMain()->getRequest();
122 if (
123 !self::$globalSession // No global session is set up yet
124 || self::$globalSessionRequest !== $request // The global WebRequest changed
125 || $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
126 ) {
127 self::$globalSessionRequest = $request;
128 if ( $id === '' ) {
129 // session_id() wasn't used, so fetch the Session from the WebRequest.
130 // We use $request->getSession() instead of $singleton->getSessionForRequest()
131 // because doing the latter would require a public
132 // "$request->getSessionId()" method that would confuse end
133 // users by returning SessionId|null where they'd expect it to
134 // be short for $request->getSession()->getId(), and would
135 // wind up being a duplicate of the code in
136 // $request->getSession() anyway.
137 self::$globalSession = $request->getSession();
138 } else {
139 // Someone used session_id(), so we need to follow suit.
140 // Note this overwrites whatever session might already be
141 // associated with $request with the one for $id.
142 self::$globalSession = self::singleton()->getSessionById( $id, true, $request )
143 ?: $request->getSession();
144 }
145 }
147 }
148
155 public function __construct( $options = [] ) {
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'
161 );
162 }
163 } else {
164 $this->config = MediaWikiServices::getInstance()->getMainConfig();
165 }
166
167 if ( isset( $options['logger'] ) ) {
168 if ( !$options['logger'] instanceof LoggerInterface ) {
169 throw new \InvalidArgumentException(
170 '$options[\'logger\'] must be an instance of LoggerInterface'
171 );
172 }
173 $this->setLogger( $options['logger'] );
174 } else {
175 $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
176 }
177
178 if ( isset( $options['hookContainer'] ) ) {
179 $this->setHookContainer( $options['hookContainer'] );
180 } else {
181 $this->setHookContainer( MediaWikiServices::getInstance()->getHookContainer() );
182 }
183
184 if ( isset( $options['store'] ) ) {
185 if ( !$options['store'] instanceof BagOStuff ) {
186 throw new \InvalidArgumentException(
187 '$options[\'store\'] must be an instance of BagOStuff'
188 );
189 }
190 $store = $options['store'];
191 } else {
192 $store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
193 }
194
195 $this->logger->debug( 'SessionManager using store ' . get_class( $store ) );
196 $this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store );
197
198 register_shutdown_function( [ $this, 'shutdown' ] );
199 }
200
201 public function setLogger( LoggerInterface $logger ) {
202 $this->logger = $logger;
203 }
204
210 $this->hookContainer = $hookContainer;
211 $this->hookRunner = new HookRunner( $hookContainer );
212 }
213
214 public function getSessionForRequest( WebRequest $request ) {
215 $info = $this->getSessionInfoForRequest( $request );
216
217 if ( !$info ) {
218 $session = $this->getEmptySession( $request );
219 } else {
220 $session = $this->getSessionFromInfo( $info, $request );
221 }
222 return $session;
223 }
224
225 public function getSessionById( $id, $create = false, WebRequest $request = null ) {
226 if ( !self::validateSessionId( $id ) ) {
227 throw new \InvalidArgumentException( 'Invalid session ID' );
228 }
229 if ( !$request ) {
230 $request = new FauxRequest;
231 }
232
233 $session = null;
234 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => $id, 'idIsSafe' => true ] );
235
236 // If we already have the backend loaded, use it directly
237 if ( isset( $this->allSessionBackends[$id] ) ) {
238 return $this->getSessionFromInfo( $info, $request );
239 }
240
241 // Test if the session is in storage, and if so try to load it.
242 $key = $this->store->makeKey( 'MWSession', $id );
243 if ( is_array( $this->store->get( $key ) ) ) {
244 $create = false; // If loading fails, don't bother creating because it probably will fail too.
245 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
246 $session = $this->getSessionFromInfo( $info, $request );
247 }
248 }
249
250 if ( $create && $session === null ) {
251 $ex = null;
252 try {
253 $session = $this->getEmptySessionInternal( $request, $id );
254 } catch ( \Exception $ex ) {
255 $this->logger->error( 'Failed to create empty session: {exception}',
256 [
257 'method' => __METHOD__,
258 'exception' => $ex,
259 ] );
260 $session = null;
261 }
262 }
263
264 return $session;
265 }
266
267 public function getEmptySession( WebRequest $request = null ) {
268 return $this->getEmptySessionInternal( $request );
269 }
270
277 private function getEmptySessionInternal( WebRequest $request = null, $id = null ) {
278 if ( $id !== null ) {
279 if ( !self::validateSessionId( $id ) ) {
280 throw new \InvalidArgumentException( 'Invalid session ID' );
281 }
282
283 $key = $this->store->makeKey( 'MWSession', $id );
284 if ( is_array( $this->store->get( $key ) ) ) {
285 throw new \InvalidArgumentException( 'Session ID already exists' );
286 }
287 }
288 if ( !$request ) {
289 $request = new FauxRequest;
290 }
291
292 $infos = [];
293 foreach ( $this->getProviders() as $provider ) {
294 $info = $provider->newSessionInfo( $id );
295 if ( !$info ) {
296 continue;
297 }
298 if ( $info->getProvider() !== $provider ) {
299 throw new \UnexpectedValueException(
300 "$provider returned an empty session info for a different provider: $info"
301 );
302 }
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
307 );
308 }
309 if ( !$info->isIdSafe() ) {
310 throw new \UnexpectedValueException(
311 "$provider returned empty session info with id flagged unsafe"
312 );
313 }
314 $compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
315 if ( $compare > 0 ) {
316 continue;
317 }
318 if ( $compare === 0 ) {
319 $infos[] = $info;
320 } else {
321 $infos = [ $info ];
322 }
323 }
324
325 // Make sure there's exactly one
326 if ( count( $infos ) > 1 ) {
327 throw new \UnexpectedValueException(
328 'Multiple empty sessions tied for top priority: ' . implode( ', ', $infos )
329 );
330 } elseif ( count( $infos ) < 1 ) {
331 throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
332 }
333
334 return $this->getSessionFromInfo( $infos[0], $request );
335 }
336
337 public function invalidateSessionsForUser( User $user ) {
338 $user->setToken();
339 $user->saveSettings();
340
341 foreach ( $this->getProviders() as $provider ) {
342 $provider->invalidateSessionsForUser( $user );
343 }
344 }
345
346 public function getVaryHeaders() {
347 // @codeCoverageIgnoreStart
348 if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
349 return [];
350 }
351 // @codeCoverageIgnoreEnd
352 if ( $this->varyHeaders === null ) {
353 $headers = [];
354 foreach ( $this->getProviders() as $provider ) {
355 foreach ( $provider->getVaryHeaders() as $header => $options ) {
356 # Note that the $options value returned has been deprecated
357 # and is ignored.
358 $headers[$header] = null;
359 }
360 }
361 $this->varyHeaders = $headers;
362 }
363 return $this->varyHeaders;
364 }
365
366 public function getVaryCookies() {
367 // @codeCoverageIgnoreStart
368 if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
369 return [];
370 }
371 // @codeCoverageIgnoreEnd
372 if ( $this->varyCookies === null ) {
373 $cookies = [];
374 foreach ( $this->getProviders() as $provider ) {
375 $cookies = array_merge( $cookies, $provider->getVaryCookies() );
376 }
377 $this->varyCookies = array_values( array_unique( $cookies ) );
378 }
379 return $this->varyCookies;
380 }
381
387 public static function validateSessionId( $id ) {
388 return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
389 }
390
405 public function preventSessionsForUser( $username ) {
406 $this->preventUsers[$username] = true;
407
408 // Instruct the session providers to kill any other sessions too.
409 foreach ( $this->getProviders() as $provider ) {
410 $provider->preventSessionsForUser( $username );
411 }
412 }
413
420 public function isUserSessionPrevented( $username ) {
421 return !empty( $this->preventUsers[$username] );
422 }
423
428 protected function getProviders() {
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] ) ) {
439 // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
440 throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
441 }
442 $this->sessionProviders[(string)$provider] = $provider;
443 }
444 }
446 }
447
458 public function getProvider( $name ) {
459 $providers = $this->getProviders();
460 return $providers[$name] ?? null;
461 }
462
467 public function shutdown() {
468 if ( $this->allSessionBackends ) {
469 $this->logger->debug( 'Saving all sessions on shutdown' );
470 if ( session_id() !== '' ) {
471 // @codeCoverageIgnoreStart
472 session_write_close();
473 }
474 // @codeCoverageIgnoreEnd
475 foreach ( $this->allSessionBackends as $backend ) {
476 $backend->shutdown();
477 }
478 }
479 }
480
486 private function getSessionInfoForRequest( WebRequest $request ) {
487 // Call all providers to fetch "the" session
488 $infos = [];
489 foreach ( $this->getProviders() as $provider ) {
490 $info = $provider->provideSessionInfo( $request );
491 if ( !$info ) {
492 continue;
493 }
494 if ( $info->getProvider() !== $provider ) {
495 throw new \UnexpectedValueException(
496 "$provider returned session info for a different provider: $info"
497 );
498 }
499 $infos[] = $info;
500 }
501
502 // Sort the SessionInfos. Then find the first one that can be
503 // successfully loaded, and then all the ones after it with the same
504 // priority.
505 usort( $infos, [ SessionInfo::class, 'compare' ] );
506 $retInfos = [];
507 while ( $infos ) {
508 $info = array_pop( $infos );
509 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
510 $retInfos[] = $info;
511 while ( $infos ) {
512 $info = array_pop( $infos );
513 if ( SessionInfo::compare( $retInfos[0], $info ) ) {
514 // We hit a lower priority, stop checking.
515 break;
516 }
517 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
518 // This is going to error out below, but we want to
519 // provide a complete list.
520 $retInfos[] = $info;
521 } else {
522 // Session load failed, so unpersist it from this request
523 $info->getProvider()->unpersistSession( $request );
524 }
525 }
526 } else {
527 // Session load failed, so unpersist it from this request
528 $info->getProvider()->unpersistSession( $request );
529 }
530 }
531
532 if ( count( $retInfos ) > 1 ) {
533 throw new SessionOverflowException(
534 $retInfos,
535 'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
536 );
537 }
538
539 return $retInfos ? $retInfos[0] : null;
540 }
541
549 private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
550 $key = $this->store->makeKey( 'MWSession', $info->getId() );
551 $blob = $this->store->get( $key );
552
553 // If we got data from the store and the SessionInfo says to force use,
554 // "fail" means to delete the data from the store and retry. Otherwise,
555 // "fail" is just return false.
556 if ( $info->forceUse() && $blob !== false ) {
557 $failHandler = function () use ( $key, &$info, $request ) {
558 $this->store->delete( $key );
559 return $this->loadSessionInfoFromStore( $info, $request );
560 };
561 } else {
562 $failHandler = function () {
563 return false;
564 };
565 }
566
567 $newParams = [];
568
569 if ( $blob !== false ) {
570 // Sanity check: blob must be an array, if it's saved at all
571 if ( !is_array( $blob ) ) {
572 $this->logger->warning( 'Session "{session}": Bad data', [
573 'session' => $info->__toString(),
574 ] );
575 $this->store->delete( $key );
576 return $failHandler();
577 }
578
579 // Sanity check: blob has data and metadata arrays
580 if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
581 !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
582 ) {
583 $this->logger->warning( 'Session "{session}": Bad data structure', [
584 'session' => $info->__toString(),
585 ] );
586 $this->store->delete( $key );
587 return $failHandler();
588 }
589
590 $data = $blob['data'];
591 $metadata = $blob['metadata'];
592
593 // Sanity check: metadata must be an array and must contain certain
594 // keys, if it's saved at all
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 )
599 ) {
600 $this->logger->warning( 'Session "{session}": Bad metadata', [
601 'session' => $info->__toString(),
602 ] );
603 $this->store->delete( $key );
604 return $failHandler();
605 }
606
607 // First, load the provider from metadata, or validate it against the metadata.
608 $provider = $info->getProvider();
609 if ( $provider === null ) {
610 $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
611 if ( !$provider ) {
612 $this->logger->warning(
613 'Session "{session}": Unknown provider ' . $metadata['provider'],
614 [
615 'session' => $info->__toString(),
616 ]
617 );
618 $this->store->delete( $key );
619 return $failHandler();
620 }
621 } elseif ( $metadata['provider'] !== (string)$provider ) {
622 $this->logger->warning( 'Session "{session}": Wrong provider ' .
623 $metadata['provider'] . ' !== ' . $provider,
624 [
625 'session' => $info->__toString(),
626 ] );
627 return $failHandler();
628 }
629
630 // Load provider metadata from metadata, or validate it against the metadata
631 $providerMetadata = $info->getProviderMetadata();
632 if ( isset( $metadata['providerMetadata'] ) ) {
633 if ( $providerMetadata === null ) {
634 $newParams['metadata'] = $metadata['providerMetadata'];
635 } else {
636 try {
637 $newProviderMetadata = $provider->mergeMetadata(
638 $metadata['providerMetadata'], $providerMetadata
639 );
640 if ( $newProviderMetadata !== $providerMetadata ) {
641 $newParams['metadata'] = $newProviderMetadata;
642 }
643 } catch ( MetadataMergeException $ex ) {
644 $this->logger->warning(
645 'Session "{session}": Metadata merge failed: {exception}',
646 [
647 'session' => $info->__toString(),
648 'exception' => $ex,
649 ] + $ex->getContext()
650 );
651 return $failHandler();
652 }
653 }
654 }
655
656 // Next, load the user from metadata, or validate it against the metadata.
657 $userInfo = $info->getUserInfo();
658 if ( !$userInfo ) {
659 // For loading, id is preferred to name.
660 try {
661 if ( $metadata['userId'] ) {
662 $userInfo = UserInfo::newFromId( $metadata['userId'] );
663 } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
664 $userInfo = UserInfo::newFromName( $metadata['userName'] );
665 } else {
666 $userInfo = UserInfo::newAnonymous();
667 }
668 } catch ( \InvalidArgumentException $ex ) {
669 $this->logger->error( 'Session "{session}": {exception}', [
670 'session' => $info->__toString(),
671 'exception' => $ex,
672 ] );
673 return $failHandler();
674 }
675 $newParams['userInfo'] = $userInfo;
676 } else {
677 // User validation passes if user ID matches, or if there
678 // is no saved ID and the names match.
679 if ( $metadata['userId'] ) {
680 if ( $metadata['userId'] !== $userInfo->getId() ) {
681 $this->logger->warning(
682 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
683 [
684 'session' => $info->__toString(),
685 'uid_a' => $metadata['userId'],
686 'uid_b' => $userInfo->getId(),
687 ] );
688 return $failHandler();
689 }
690
691 // If the user was renamed, probably best to fail here.
692 if ( $metadata['userName'] !== null &&
693 $userInfo->getName() !== $metadata['userName']
694 ) {
695 $this->logger->warning(
696 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
697 [
698 'session' => $info->__toString(),
699 'uname_a' => $metadata['userName'],
700 'uname_b' => $userInfo->getName(),
701 ] );
702 return $failHandler();
703 }
704
705 } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
706 if ( $metadata['userName'] !== $userInfo->getName() ) {
707 $this->logger->warning(
708 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
709 [
710 'session' => $info->__toString(),
711 'uname_a' => $metadata['userName'],
712 'uname_b' => $userInfo->getName(),
713 ] );
714 return $failHandler();
715 }
716 } elseif ( !$userInfo->isAnon() ) {
717 // Metadata specifies an anonymous user, but the passed-in
718 // user isn't anonymous.
719 $this->logger->warning(
720 'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
721 [
722 'session' => $info->__toString(),
723 ] );
724 return $failHandler();
725 }
726 }
727
728 // And if we have a token in the metadata, it must match the loaded/provided user.
729 if ( $metadata['userToken'] !== null &&
730 $userInfo->getToken() !== $metadata['userToken']
731 ) {
732 $this->logger->warning( 'Session "{session}": User token mismatch', [
733 'session' => $info->__toString(),
734 ] );
735 return $failHandler();
736 }
737 if ( !$userInfo->isVerified() ) {
738 $newParams['userInfo'] = $userInfo->verified();
739 }
740
741 if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
742 $newParams['remembered'] = true;
743 }
744 if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
745 $newParams['forceHTTPS'] = true;
746 }
747 if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) {
748 $newParams['persisted'] = true;
749 }
750
751 if ( !$info->isIdSafe() ) {
752 $newParams['idIsSafe'] = true;
753 }
754 } else {
755 // No metadata, so we can't load the provider if one wasn't given.
756 if ( $info->getProvider() === null ) {
757 $this->logger->warning(
758 'Session "{session}": Null provider and no metadata',
759 [
760 'session' => $info->__toString(),
761 ] );
762 return $failHandler();
763 }
764
765 // If no user was provided and no metadata, it must be anon.
766 if ( !$info->getUserInfo() ) {
767 if ( $info->getProvider()->canChangeUser() ) {
768 $newParams['userInfo'] = UserInfo::newAnonymous();
769 } else {
770 $this->logger->info(
771 'Session "{session}": No user provided and provider cannot set user',
772 [
773 'session' => $info->__toString(),
774 ] );
775 return $failHandler();
776 }
777 } elseif ( !$info->getUserInfo()->isVerified() ) {
778 // probably just a session timeout
779 $this->logger->info(
780 'Session "{session}": Unverified user provided and no metadata to auth it',
781 [
782 'session' => $info->__toString(),
783 ] );
784 return $failHandler();
785 }
786
787 $data = false;
788 $metadata = false;
789
790 if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
791 // The ID doesn't come from the user, so it should be safe
792 // (and if not, nothing we can do about it anyway)
793 $newParams['idIsSafe'] = true;
794 }
795 }
796
797 // Construct the replacement SessionInfo, if necessary
798 if ( $newParams ) {
799 $newParams['copyFrom'] = $info;
800 $info = new SessionInfo( $info->getPriority(), $newParams );
801 }
802
803 // Allow the provider to check the loaded SessionInfo
804 $providerMetadata = $info->getProviderMetadata();
805 if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
806 return $failHandler();
807 }
808 if ( $providerMetadata !== $info->getProviderMetadata() ) {
809 $info = new SessionInfo( $info->getPriority(), [
810 'metadata' => $providerMetadata,
811 'copyFrom' => $info,
812 ] );
813 }
814
815 // Give hooks a chance to abort. Combined with the SessionMetadata
816 // hook, this can allow for tying a session to an IP address or the
817 // like.
818 $reason = 'Hook aborted';
819 if ( !$this->hookRunner->onSessionCheckInfo(
820 $reason, $info, $request, $metadata, $data )
821 ) {
822 $this->logger->warning( 'Session "{session}": ' . $reason, [
823 'session' => $info->__toString(),
824 ] );
825 return $failHandler();
826 }
827
828 return true;
829 }
830
839 public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
840 // @codeCoverageIgnoreStart
841 if ( defined( 'MW_NO_SESSION' ) ) {
842 if ( MW_NO_SESSION === 'warn' ) {
843 // Undocumented safety case for converting existing entry points
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' ),
846 ] );
847 } else {
848 throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
849 }
850 }
851 // @codeCoverageIgnoreEnd
852
853 $id = $info->getId();
854
855 if ( !isset( $this->allSessionBackends[$id] ) ) {
856 if ( !isset( $this->allSessionIds[$id] ) ) {
857 $this->allSessionIds[$id] = new SessionId( $id );
858 }
859 $backend = new SessionBackend(
860 $this->allSessionIds[$id],
861 $info,
862 $this->store,
863 $this->logger,
864 $this->hookContainer,
865 $this->config->get( 'ObjectCacheSessionExpiry' )
866 );
867 $this->allSessionBackends[$id] = $backend;
868 $delay = $backend->delaySave();
869 } else {
870 $backend = $this->allSessionBackends[$id];
871 $delay = $backend->delaySave();
872 if ( $info->wasPersisted() ) {
873 $backend->persist();
874 }
875 if ( $info->wasRemembered() ) {
876 $backend->setRememberUser( true );
877 }
878 }
879
880 $request->setSessionId( $backend->getSessionId() );
881 $session = $backend->getSession( $request );
882
883 if ( !$info->isIdSafe() ) {
884 $session->resetId();
885 }
886
887 \Wikimedia\ScopedCallback::consume( $delay );
888 return $session;
889 }
890
896 public function deregisterSessionBackend( SessionBackend $backend ) {
897 $id = $backend->getId();
898 if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
899 $this->allSessionBackends[$id] !== $backend ||
900 $this->allSessionIds[$id] !== $backend->getSessionId()
901 ) {
902 throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
903 }
904
905 unset( $this->allSessionBackends[$id] );
906 // Explicitly do not unset $this->allSessionIds[$id]
907 }
908
914 public function changeBackendId( SessionBackend $backend ) {
915 $sessionId = $backend->getSessionId();
916 $oldId = (string)$sessionId;
917 if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
918 $this->allSessionBackends[$oldId] !== $backend ||
919 $this->allSessionIds[$oldId] !== $sessionId
920 ) {
921 throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
922 }
923
924 $newId = $this->generateSessionId();
925
926 unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
927 $sessionId->setId( $newId );
928 $this->allSessionBackends[$newId] = $backend;
929 $this->allSessionIds[$newId] = $sessionId;
930 }
931
936 public function generateSessionId() {
937 do {
938 $id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
939 $key = $this->store->makeKey( 'MWSession', $id );
940 } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
941 return $id;
942 }
943
949 public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
950 $handler->setManager( $this, $this->store, $this->logger );
951 }
952
958 public static function resetCache() {
959 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
960 // @codeCoverageIgnoreStart
961 throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
962 // @codeCoverageIgnoreEnd
963 }
964
965 self::$globalSession = null;
966 self::$globalSessionRequest = null;
967 }
968
971}
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:71
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.
MediaWiki exception.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
Subclass of UnexpectedValueException that can be annotated with additional data for debug logging.
Adapter for PHP's session handling.
static isEnabled()
Test whether the handler is installed and enabled.
setManager(SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger)
Set the manager, store, and logger.
This is the actual workhorse for Session.
getSessionId()
Fetch the SessionId object.
getId()
Returns the session ID.
Value object holding the session ID in a manner that can be globally updated.
Definition SessionId.php:40
Value object returned by SessionProvider.
forceUse()
Force use of this SessionInfo if validation fails.
getProviderMetadata()
Return provider metadata.
getId()
Return the session ID.
getProvider()
Return the provider.
isIdSafe()
Indicate whether the ID is "safe".
getUserInfo()
Return the user.
wasPersisted()
Return whether the session is persisted.
const MIN_PRIORITY
Minimum allowed priority.
getPriority()
Return the priority.
wasRemembered()
Return whether the user was remembered.
forceHTTPS()
Whether this session should only be used over HTTPS.
static compare( $a, $b)
Compare two SessionInfo objects by priority.
This serves as the entry point to the MediaWiki session handling system.
static resetCache()
Reset the internal caching for unit testing.
getVaryHeaders()
Return the HTTP headers that need varying on.
deregisterSessionBackend(SessionBackend $backend)
Deregister a SessionBackend.
invalidateSessionsForUser(User $user)
Invalidate sessions for a user.
static WebRequest null $globalSessionRequest
loadSessionInfoFromStore(SessionInfo &$info, WebRequest $request)
Load and verify the session info against the store.
setHookContainer(HookContainer $hookContainer)
getVaryCookies()
Return the list of cookies that need varying on.
setupPHPSessionHandler(PHPSessionHandler $handler)
Call setters on a PHPSessionHandler.
getEmptySessionInternal(WebRequest $request=null, $id=null)
static SessionManager null $instance
getEmptySession(WebRequest $request=null)
Create a new, empty session.
static getGlobalSession()
Get the "global" session.
getSessionInfoForRequest(WebRequest $request)
Fetch the SessionInfo(s) for a request.
preventSessionsForUser( $username)
Prevent future sessions for the user.
shutdown()
Save all active sessions on shutdown.
getSessionById( $id, $create=false, WebRequest $request=null)
Fetch a session by ID.
getProvider( $name)
Get a session provider by name.
static validateSessionId( $id)
Validate a session ID.
getProviders()
Get the available SessionProviders.
static singleton()
Get the global SessionManager.
changeBackendId(SessionBackend $backend)
Change a SessionBackend's ID.
generateSessionId()
Generate a new random session ID.
setLogger(LoggerInterface $logger)
getSessionFromInfo(SessionInfo $info, WebRequest $request)
Create a Session corresponding to the passed SessionInfo.
getSessionForRequest(WebRequest $request)
Fetch the session for a request (or a new empty session if none is attached to it)
isUserSessionPrevented( $username)
Test if a user is prevented.
OverflowException specific to the SessionManager, used when the request had multiple possible session...
A SessionProvider provides SessionInfo and support for Session.
setLogger(LoggerInterface $logger)
setConfig(Config $config)
Set configuration.
setManager(SessionManager $manager)
Set the session manager.
setHookContainer( $hookContainer)
Set the hook container.
Manages data for an an authenticated session.
Definition Session.php:48
static newFromName( $name, $verified=false)
Create an instance for a logged-in user by name.
Definition UserInfo.php:103
static newAnonymous()
Create an instance for an anonymous (i.e.
Definition UserInfo.php:75
static newFromId( $id, $verified=false)
Create an instance for a logged-in user by ID.
Definition UserInfo.php:85
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
saveSettings()
Save this user's settings into the database.
Definition User.php:3445
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition User.php:2529
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.
Definition Config.php:30
This exists to make IDEs happy, so they don't see the internal-but-required-to-be-public methods on S...
const MW_NO_SESSION
Definition load.php:32
A helper class for throttling authentication attempts.
$header