MediaWiki  master
SessionManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
27 use MWException;
29 use BagOStuff;
30 use CachedBagOStuff;
31 use Config;
32 use FauxRequest;
33 use User;
34 use WebRequest;
36 
50 final class SessionManager implements SessionManagerInterface {
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() {
107  if ( !PHPSessionHandler::isEnabled() ) {
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  }
138  return self::$globalSession;
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 }
getVaryCookies()
Return the list of cookies that need varying on.
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:36
getSessionId()
Fetch the SessionId object.
This is the actual workhorse for Session.
getProviders()
Get the available SessionProviders.
getUserInfo()
Return the user.
isUserSessionPrevented( $username)
Test if a user is prevented.
SessionBackend [] $allSessionBackends
saveSettings()
Save this user&#39;s settings into the database.
Definition: User.php:3977
changeBackendId(SessionBackend $backend)
Change a SessionBackend&#39;s ID.
getEmptySessionInternal(WebRequest $request=null, $id=null)
getPriority()
Return the priority.
getEmptySession(WebRequest $request=null)
Create a new, empty session.
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:78
static SessionManager null $instance
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:2840
static compare( $a, $b)
Compare two SessionInfo objects by priority.
static getInstance()
Returns the global default instance of the top level service locator.
shutdown()
Save all active sessions on shutdown.
A helper class for throttling authentication attempts.
getId()
Return the session ID.
Subclass of UnexpectedValueException that can be annotated with additional data for debug logging...
forceUse()
Force use of this SessionInfo if validation fails.
delaySave()
Delay automatic saving while multiple updates are being made.
getSessionFromInfo(SessionInfo $info, WebRequest $request)
Create a Session corresponding to the passed SessionInfo.
setupPHPSessionHandler(PHPSessionHandler $handler)
Call setters on a PHPSessionHandler.
preventSessionsForUser( $username)
Prevent future sessions for the user.
const MW_NO_SESSION
Definition: load.php:29
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, email address, options, last login time).
Definition: User.php:51
static resetCache()
Reset the internal caching for unit testing.
getSessionForRequest(WebRequest $request)
Fetch the session for a request (or a new empty session if none is attached to it) ...
static getMain()
Get the RequestContext object associated with the main request.
Interface for configuration instances.
Definition: Config.php:28
getSessionInfoForRequest(WebRequest $request)
Fetch the SessionInfo(s) for a request.
deregisterSessionBackend(SessionBackend $backend)
Deregister a SessionBackend.
getSessionById( $id, $create=false, WebRequest $request=null)
Fetch a session by ID.
getProvider()
Return the provider.
setLogger(LoggerInterface $logger)
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format...
Definition: MWCryptRand.php:36
static WebRequest null $globalSessionRequest
getId()
Returns the session ID.
getProviderMetadata()
Return provider metadata.
wasPersisted()
Return whether the session is persisted.
$header
Adapter for PHP&#39;s session handling.
static singleton()
Get the global SessionManager.
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:823
generateSessionId()
Generate a new random session ID.
static isEnabled()
Test whether the handler is installed and enabled.
static getGlobalSession()
Get the "global" session.
invalidateSessionsForUser(User $user)
Invalidate sessions for a user.
SessionProvider [] $sessionProviders
static newFromName( $name, $verified=false)
Create an instance for a logged-in user by name.
Definition: UserInfo.php:103
Value object holding the session ID in a manner that can be globally updated.
Definition: SessionId.php:38
getVaryHeaders()
Return the HTTP headers that need varying on.
static Session null $globalSession
This serves as the entry point to the MediaWiki session handling system.
OverflowException specific to the SessionManager, used when the request had multiple possible session...
This exists to make IDEs happy, so they don&#39;t see the internal-but-required-to-be-public methods on S...
setManager(SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger)
Set the manager, store, and logger.
getProvider( $name)
Get a session provider by name.
static validateSessionId( $id)
Validate a session ID.
forceHTTPS()
Whether this session should only be used over HTTPS.
Value object returned by SessionProvider.
Definition: SessionInfo.php:34
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
loadSessionInfoFromStore(SessionInfo &$info, WebRequest $request)
Load and verify the session info against the store.
wasRemembered()
Return whether the user was remembered.
isIdSafe()
Indicate whether the ID is "safe".