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