MediaWiki REL1_34
SessionManager.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
27use MWException;
28use Psr\Log\LoggerInterface;
29use BagOStuff;
31use Config;
32use FauxRequest;
33use User;
34use WebRequest;
35use Wikimedia\ObjectFactory;
36
52 private static $instance = null;
53
55 private static $globalSession = null;
56
58 private static $globalSessionRequest = null;
59
61 private $logger;
62
64 private $config;
65
67 private $store;
68
70 private $sessionProviders = null;
71
73 private $varyCookies = null;
74
76 private $varyHeaders = null;
77
79 private $allSessionBackends = [];
80
82 private $allSessionIds = [];
83
85 private $preventUsers = [];
86
91 public static function singleton() {
92 if ( self::$instance === null ) {
93 self::$instance = new self();
94 }
95 return self::$instance;
96 }
97
106 public static function getGlobalSession() {
108 $id = '';
109 } else {
110 $id = session_id();
111 }
112
113 $request = \RequestContext::getMain()->getRequest();
114 if (
115 !self::$globalSession // No global session is set up yet
116 || self::$globalSessionRequest !== $request // The global WebRequest changed
117 || $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
118 ) {
119 self::$globalSessionRequest = $request;
120 if ( $id === '' ) {
121 // session_id() wasn't used, so fetch the Session from the WebRequest.
122 // We use $request->getSession() instead of $singleton->getSessionForRequest()
123 // because doing the latter would require a public
124 // "$request->getSessionId()" method that would confuse end
125 // users by returning SessionId|null where they'd expect it to
126 // be short for $request->getSession()->getId(), and would
127 // wind up being a duplicate of the code in
128 // $request->getSession() anyway.
129 self::$globalSession = $request->getSession();
130 } else {
131 // Someone used session_id(), so we need to follow suit.
132 // Note this overwrites whatever session might already be
133 // associated with $request with the one for $id.
134 self::$globalSession = self::singleton()->getSessionById( $id, true, $request )
135 ?: $request->getSession();
136 }
137 }
139 }
140
147 public function __construct( $options = [] ) {
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'
153 );
154 }
155 } else {
156 $this->config = MediaWikiServices::getInstance()->getMainConfig();
157 }
158
159 if ( isset( $options['logger'] ) ) {
160 if ( !$options['logger'] instanceof LoggerInterface ) {
161 throw new \InvalidArgumentException(
162 '$options[\'logger\'] must be an instance of LoggerInterface'
163 );
164 }
165 $this->setLogger( $options['logger'] );
166 } else {
167 $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
168 }
169
170 if ( isset( $options['store'] ) ) {
171 if ( !$options['store'] instanceof BagOStuff ) {
172 throw new \InvalidArgumentException(
173 '$options[\'store\'] must be an instance of BagOStuff'
174 );
175 }
176 $store = $options['store'];
177 } else {
178 $store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
179 }
180 $this->logger->debug( 'SessionManager using store ' . get_class( $store ) );
181 $this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store );
182
183 register_shutdown_function( [ $this, 'shutdown' ] );
184 }
185
186 public function setLogger( LoggerInterface $logger ) {
187 $this->logger = $logger;
188 }
189
190 public function getSessionForRequest( WebRequest $request ) {
191 $info = $this->getSessionInfoForRequest( $request );
192
193 if ( !$info ) {
194 $session = $this->getEmptySession( $request );
195 } else {
196 $session = $this->getSessionFromInfo( $info, $request );
197 }
198 return $session;
199 }
200
201 public function getSessionById( $id, $create = false, WebRequest $request = null ) {
202 if ( !self::validateSessionId( $id ) ) {
203 throw new \InvalidArgumentException( 'Invalid session ID' );
204 }
205 if ( !$request ) {
206 $request = new FauxRequest;
207 }
208
209 $session = null;
210 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => $id, 'idIsSafe' => true ] );
211
212 // If we already have the backend loaded, use it directly
213 if ( isset( $this->allSessionBackends[$id] ) ) {
214 return $this->getSessionFromInfo( $info, $request );
215 }
216
217 // Test if the session is in storage, and if so try to load it.
218 $key = $this->store->makeKey( 'MWSession', $id );
219 if ( is_array( $this->store->get( $key ) ) ) {
220 $create = false; // If loading fails, don't bother creating because it probably will fail too.
221 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
222 $session = $this->getSessionFromInfo( $info, $request );
223 }
224 }
225
226 if ( $create && $session === null ) {
227 $ex = null;
228 try {
229 $session = $this->getEmptySessionInternal( $request, $id );
230 } catch ( \Exception $ex ) {
231 $this->logger->error( 'Failed to create empty session: {exception}',
232 [
233 'method' => __METHOD__,
234 'exception' => $ex,
235 ] );
236 $session = null;
237 }
238 }
239
240 return $session;
241 }
242
243 public function getEmptySession( WebRequest $request = null ) {
244 return $this->getEmptySessionInternal( $request );
245 }
246
253 private function getEmptySessionInternal( WebRequest $request = null, $id = null ) {
254 if ( $id !== null ) {
255 if ( !self::validateSessionId( $id ) ) {
256 throw new \InvalidArgumentException( 'Invalid session ID' );
257 }
258
259 $key = $this->store->makeKey( 'MWSession', $id );
260 if ( is_array( $this->store->get( $key ) ) ) {
261 throw new \InvalidArgumentException( 'Session ID already exists' );
262 }
263 }
264 if ( !$request ) {
265 $request = new FauxRequest;
266 }
267
268 $infos = [];
269 foreach ( $this->getProviders() as $provider ) {
270 $info = $provider->newSessionInfo( $id );
271 if ( !$info ) {
272 continue;
273 }
274 if ( $info->getProvider() !== $provider ) {
275 throw new \UnexpectedValueException(
276 "$provider returned an empty session info for a different provider: $info"
277 );
278 }
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
283 );
284 }
285 if ( !$info->isIdSafe() ) {
286 throw new \UnexpectedValueException(
287 "$provider returned empty session info with id flagged unsafe"
288 );
289 }
290 $compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
291 if ( $compare > 0 ) {
292 continue;
293 }
294 if ( $compare === 0 ) {
295 $infos[] = $info;
296 } else {
297 $infos = [ $info ];
298 }
299 }
300
301 // Make sure there's exactly one
302 if ( count( $infos ) > 1 ) {
303 throw new \UnexpectedValueException(
304 'Multiple empty sessions tied for top priority: ' . implode( ', ', $infos )
305 );
306 } elseif ( count( $infos ) < 1 ) {
307 throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
308 }
309
310 return $this->getSessionFromInfo( $infos[0], $request );
311 }
312
313 public function invalidateSessionsForUser( User $user ) {
314 $user->setToken();
315 $user->saveSettings();
316
317 foreach ( $this->getProviders() as $provider ) {
318 $provider->invalidateSessionsForUser( $user );
319 }
320 }
321
322 public function getVaryHeaders() {
323 // @codeCoverageIgnoreStart
324 // @phan-suppress-next-line PhanUndeclaredConstant
325 if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
326 return [];
327 }
328 // @codeCoverageIgnoreEnd
329 if ( $this->varyHeaders === null ) {
330 $headers = [];
331 foreach ( $this->getProviders() as $provider ) {
332 foreach ( $provider->getVaryHeaders() as $header => $options ) {
333 # Note that the $options value returned has been deprecated
334 # and is ignored.
335 $headers[$header] = null;
336 }
337 }
338 $this->varyHeaders = $headers;
339 }
340 return $this->varyHeaders;
341 }
342
343 public function getVaryCookies() {
344 // @codeCoverageIgnoreStart
345 // @phan-suppress-next-line PhanUndeclaredConstant
346 if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
347 return [];
348 }
349 // @codeCoverageIgnoreEnd
350 if ( $this->varyCookies === null ) {
351 $cookies = [];
352 foreach ( $this->getProviders() as $provider ) {
353 $cookies = array_merge( $cookies, $provider->getVaryCookies() );
354 }
355 $this->varyCookies = array_values( array_unique( $cookies ) );
356 }
357 return $this->varyCookies;
358 }
359
365 public static function validateSessionId( $id ) {
366 return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
367 }
368
383 public function preventSessionsForUser( $username ) {
384 $this->preventUsers[$username] = true;
385
386 // Instruct the session providers to kill any other sessions too.
387 foreach ( $this->getProviders() as $provider ) {
388 $provider->preventSessionsForUser( $username );
389 }
390 }
391
398 public function isUserSessionPrevented( $username ) {
399 return !empty( $this->preventUsers[$username] );
400 }
401
406 protected function getProviders() {
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] ) ) {
415 // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
416 throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
417 }
418 $this->sessionProviders[(string)$provider] = $provider;
419 }
420 }
422 }
423
434 public function getProvider( $name ) {
435 $providers = $this->getProviders();
436 return $providers[$name] ?? null;
437 }
438
443 public function shutdown() {
444 if ( $this->allSessionBackends ) {
445 $this->logger->debug( 'Saving all sessions on shutdown' );
446 if ( session_id() !== '' ) {
447 // @codeCoverageIgnoreStart
448 session_write_close();
449 }
450 // @codeCoverageIgnoreEnd
451 foreach ( $this->allSessionBackends as $backend ) {
452 $backend->shutdown();
453 }
454 }
455 }
456
462 private function getSessionInfoForRequest( WebRequest $request ) {
463 // Call all providers to fetch "the" session
464 $infos = [];
465 foreach ( $this->getProviders() as $provider ) {
466 $info = $provider->provideSessionInfo( $request );
467 if ( !$info ) {
468 continue;
469 }
470 if ( $info->getProvider() !== $provider ) {
471 throw new \UnexpectedValueException(
472 "$provider returned session info for a different provider: $info"
473 );
474 }
475 $infos[] = $info;
476 }
477
478 // Sort the SessionInfos. Then find the first one that can be
479 // successfully loaded, and then all the ones after it with the same
480 // priority.
481 usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
482 $retInfos = [];
483 while ( $infos ) {
484 $info = array_pop( $infos );
485 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
486 $retInfos[] = $info;
487 while ( $infos ) {
488 $info = array_pop( $infos );
489 if ( SessionInfo::compare( $retInfos[0], $info ) ) {
490 // We hit a lower priority, stop checking.
491 break;
492 }
493 if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
494 // This is going to error out below, but we want to
495 // provide a complete list.
496 $retInfos[] = $info;
497 } else {
498 // Session load failed, so unpersist it from this request
499 $info->getProvider()->unpersistSession( $request );
500 }
501 }
502 } else {
503 // Session load failed, so unpersist it from this request
504 $info->getProvider()->unpersistSession( $request );
505 }
506 }
507
508 if ( count( $retInfos ) > 1 ) {
509 throw new SessionOverflowException(
510 $retInfos,
511 'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
512 );
513 }
514
515 return $retInfos ? $retInfos[0] : null;
516 }
517
525 private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
526 $key = $this->store->makeKey( 'MWSession', $info->getId() );
527 $blob = $this->store->get( $key );
528
529 // If we got data from the store and the SessionInfo says to force use,
530 // "fail" means to delete the data from the store and retry. Otherwise,
531 // "fail" is just return false.
532 if ( $info->forceUse() && $blob !== false ) {
533 $failHandler = function () use ( $key, &$info, $request ) {
534 $this->store->delete( $key );
535 return $this->loadSessionInfoFromStore( $info, $request );
536 };
537 } else {
538 $failHandler = function () {
539 return false;
540 };
541 }
542
543 $newParams = [];
544
545 if ( $blob !== false ) {
546 // Sanity check: blob must be an array, if it's saved at all
547 if ( !is_array( $blob ) ) {
548 $this->logger->warning( 'Session "{session}": Bad data', [
549 'session' => $info,
550 ] );
551 $this->store->delete( $key );
552 return $failHandler();
553 }
554
555 // Sanity check: blob has data and metadata arrays
556 if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
557 !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
558 ) {
559 $this->logger->warning( 'Session "{session}": Bad data structure', [
560 'session' => $info,
561 ] );
562 $this->store->delete( $key );
563 return $failHandler();
564 }
565
566 $data = $blob['data'];
567 $metadata = $blob['metadata'];
568
569 // Sanity check: metadata must be an array and must contain certain
570 // keys, if it's saved at all
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 )
575 ) {
576 $this->logger->warning( 'Session "{session}": Bad metadata', [
577 'session' => $info,
578 ] );
579 $this->store->delete( $key );
580 return $failHandler();
581 }
582
583 // First, load the provider from metadata, or validate it against the metadata.
584 $provider = $info->getProvider();
585 if ( $provider === null ) {
586 $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
587 if ( !$provider ) {
588 $this->logger->warning(
589 'Session "{session}": Unknown provider ' . $metadata['provider'],
590 [
591 'session' => $info,
592 ]
593 );
594 $this->store->delete( $key );
595 return $failHandler();
596 }
597 } elseif ( $metadata['provider'] !== (string)$provider ) {
598 $this->logger->warning( 'Session "{session}": Wrong provider ' .
599 $metadata['provider'] . ' !== ' . $provider,
600 [
601 'session' => $info,
602 ] );
603 return $failHandler();
604 }
605
606 // Load provider metadata from metadata, or validate it against the metadata
607 $providerMetadata = $info->getProviderMetadata();
608 if ( isset( $metadata['providerMetadata'] ) ) {
609 if ( $providerMetadata === null ) {
610 $newParams['metadata'] = $metadata['providerMetadata'];
611 } else {
612 try {
613 $newProviderMetadata = $provider->mergeMetadata(
614 $metadata['providerMetadata'], $providerMetadata
615 );
616 if ( $newProviderMetadata !== $providerMetadata ) {
617 $newParams['metadata'] = $newProviderMetadata;
618 }
619 } catch ( MetadataMergeException $ex ) {
620 $this->logger->warning(
621 'Session "{session}": Metadata merge failed: {exception}',
622 [
623 'session' => $info,
624 'exception' => $ex,
625 ] + $ex->getContext()
626 );
627 return $failHandler();
628 }
629 }
630 }
631
632 // Next, load the user from metadata, or validate it against the metadata.
633 $userInfo = $info->getUserInfo();
634 if ( !$userInfo ) {
635 // For loading, id is preferred to name.
636 try {
637 if ( $metadata['userId'] ) {
638 $userInfo = UserInfo::newFromId( $metadata['userId'] );
639 } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
640 $userInfo = UserInfo::newFromName( $metadata['userName'] );
641 } else {
642 $userInfo = UserInfo::newAnonymous();
643 }
644 } catch ( \InvalidArgumentException $ex ) {
645 $this->logger->error( 'Session "{session}": {exception}', [
646 'session' => $info,
647 'exception' => $ex,
648 ] );
649 return $failHandler();
650 }
651 $newParams['userInfo'] = $userInfo;
652 } else {
653 // User validation passes if user ID matches, or if there
654 // is no saved ID and the names match.
655 if ( $metadata['userId'] ) {
656 if ( $metadata['userId'] !== $userInfo->getId() ) {
657 $this->logger->warning(
658 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
659 [
660 'session' => $info,
661 'uid_a' => $metadata['userId'],
662 'uid_b' => $userInfo->getId(),
663 ] );
664 return $failHandler();
665 }
666
667 // If the user was renamed, probably best to fail here.
668 if ( $metadata['userName'] !== null &&
669 $userInfo->getName() !== $metadata['userName']
670 ) {
671 $this->logger->warning(
672 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
673 [
674 'session' => $info,
675 'uname_a' => $metadata['userName'],
676 'uname_b' => $userInfo->getName(),
677 ] );
678 return $failHandler();
679 }
680
681 } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
682 if ( $metadata['userName'] !== $userInfo->getName() ) {
683 $this->logger->warning(
684 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
685 [
686 'session' => $info,
687 'uname_a' => $metadata['userName'],
688 'uname_b' => $userInfo->getName(),
689 ] );
690 return $failHandler();
691 }
692 } elseif ( !$userInfo->isAnon() ) {
693 // Metadata specifies an anonymous user, but the passed-in
694 // user isn't anonymous.
695 $this->logger->warning(
696 'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
697 [
698 'session' => $info,
699 ] );
700 return $failHandler();
701 }
702 }
703
704 // And if we have a token in the metadata, it must match the loaded/provided user.
705 if ( $metadata['userToken'] !== null &&
706 $userInfo->getToken() !== $metadata['userToken']
707 ) {
708 $this->logger->warning( 'Session "{session}": User token mismatch', [
709 'session' => $info,
710 ] );
711 return $failHandler();
712 }
713 if ( !$userInfo->isVerified() ) {
714 $newParams['userInfo'] = $userInfo->verified();
715 }
716
717 if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
718 $newParams['remembered'] = true;
719 }
720 if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
721 $newParams['forceHTTPS'] = true;
722 }
723 if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) {
724 $newParams['persisted'] = true;
725 }
726
727 if ( !$info->isIdSafe() ) {
728 $newParams['idIsSafe'] = true;
729 }
730 } else {
731 // No metadata, so we can't load the provider if one wasn't given.
732 if ( $info->getProvider() === null ) {
733 $this->logger->warning(
734 'Session "{session}": Null provider and no metadata',
735 [
736 'session' => $info,
737 ] );
738 return $failHandler();
739 }
740
741 // If no user was provided and no metadata, it must be anon.
742 if ( !$info->getUserInfo() ) {
743 if ( $info->getProvider()->canChangeUser() ) {
744 $newParams['userInfo'] = UserInfo::newAnonymous();
745 } else {
746 $this->logger->info(
747 'Session "{session}": No user provided and provider cannot set user',
748 [
749 'session' => $info,
750 ] );
751 return $failHandler();
752 }
753 } elseif ( !$info->getUserInfo()->isVerified() ) {
754 // probably just a session timeout
755 $this->logger->info(
756 'Session "{session}": Unverified user provided and no metadata to auth it',
757 [
758 'session' => $info,
759 ] );
760 return $failHandler();
761 }
762
763 $data = false;
764 $metadata = false;
765
766 if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
767 // The ID doesn't come from the user, so it should be safe
768 // (and if not, nothing we can do about it anyway)
769 $newParams['idIsSafe'] = true;
770 }
771 }
772
773 // Construct the replacement SessionInfo, if necessary
774 if ( $newParams ) {
775 $newParams['copyFrom'] = $info;
776 $info = new SessionInfo( $info->getPriority(), $newParams );
777 }
778
779 // Allow the provider to check the loaded SessionInfo
780 $providerMetadata = $info->getProviderMetadata();
781 if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
782 return $failHandler();
783 }
784 if ( $providerMetadata !== $info->getProviderMetadata() ) {
785 $info = new SessionInfo( $info->getPriority(), [
786 'metadata' => $providerMetadata,
787 'copyFrom' => $info,
788 ] );
789 }
790
791 // Give hooks a chance to abort. Combined with the SessionMetadata
792 // hook, this can allow for tying a session to an IP address or the
793 // like.
794 $reason = 'Hook aborted';
795 if ( !\Hooks::run(
796 'SessionCheckInfo',
797 [ &$reason, $info, $request, $metadata, $data ]
798 ) ) {
799 $this->logger->warning( 'Session "{session}": ' . $reason, [
800 'session' => $info,
801 ] );
802 return $failHandler();
803 }
804
805 return true;
806 }
807
816 public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
817 // @codeCoverageIgnoreStart
818 if ( defined( 'MW_NO_SESSION' ) ) {
819 // @phan-suppress-next-line PhanUndeclaredConstant
820 if ( MW_NO_SESSION === 'warn' ) {
821 // Undocumented safety case for converting existing entry points
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' ),
824 ] );
825 } else {
826 throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
827 }
828 }
829 // @codeCoverageIgnoreEnd
830
831 $id = $info->getId();
832
833 if ( !isset( $this->allSessionBackends[$id] ) ) {
834 if ( !isset( $this->allSessionIds[$id] ) ) {
835 $this->allSessionIds[$id] = new SessionId( $id );
836 }
837 $backend = new SessionBackend(
838 $this->allSessionIds[$id],
839 $info,
840 $this->store,
841 $this->logger,
842 $this->config->get( 'ObjectCacheSessionExpiry' )
843 );
844 $this->allSessionBackends[$id] = $backend;
845 $delay = $backend->delaySave();
846 } else {
847 $backend = $this->allSessionBackends[$id];
848 $delay = $backend->delaySave();
849 if ( $info->wasPersisted() ) {
850 $backend->persist();
851 }
852 if ( $info->wasRemembered() ) {
853 $backend->setRememberUser( true );
854 }
855 }
856
857 $request->setSessionId( $backend->getSessionId() );
858 $session = $backend->getSession( $request );
859
860 if ( !$info->isIdSafe() ) {
861 $session->resetId();
862 }
863
864 \Wikimedia\ScopedCallback::consume( $delay );
865 return $session;
866 }
867
873 public function deregisterSessionBackend( SessionBackend $backend ) {
874 $id = $backend->getId();
875 if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
876 $this->allSessionBackends[$id] !== $backend ||
877 $this->allSessionIds[$id] !== $backend->getSessionId()
878 ) {
879 throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
880 }
881
882 unset( $this->allSessionBackends[$id] );
883 // Explicitly do not unset $this->allSessionIds[$id]
884 }
885
891 public function changeBackendId( SessionBackend $backend ) {
892 $sessionId = $backend->getSessionId();
893 $oldId = (string)$sessionId;
894 if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
895 $this->allSessionBackends[$oldId] !== $backend ||
896 $this->allSessionIds[$oldId] !== $sessionId
897 ) {
898 throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
899 }
900
901 $newId = $this->generateSessionId();
902
903 unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
904 $sessionId->setId( $newId );
905 $this->allSessionBackends[$newId] = $backend;
906 $this->allSessionIds[$newId] = $sessionId;
907 }
908
913 public function generateSessionId() {
914 do {
915 $id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
916 $key = $this->store->makeKey( 'MWSession', $id );
917 } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
918 return $id;
919 }
920
926 public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
927 $handler->setManager( $this, $this->store, $this->logger );
928 }
929
934 public static function resetCache() {
935 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
936 // @codeCoverageIgnoreStart
937 throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
938 // @codeCoverageIgnoreEnd
939 }
940
941 self::$globalSession = null;
942 self::$globalSessionRequest = null;
943 }
944
947}
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:63
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.
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:38
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.
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.
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:51
saveSettings()
Save this user's settings into the database.
Definition User.php:4027
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition User.php:2889
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:28
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:29
A helper class for throttling authentication attempts.
$header