MediaWiki  master
SessionManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
26 use BagOStuff;
27 use CachedBagOStuff;
28 use Config;
29 use FauxRequest;
33 use MWException;
34 use Psr\Log\LoggerInterface;
35 use User;
36 use WebRequest;
37 use Wikimedia\ObjectFactory;
38 
52 final class SessionManager implements SessionManagerInterface {
54  private static $instance = null;
55 
57  private static $globalSession = null;
58 
60  private static $globalSessionRequest = null;
61 
63  private $logger;
64 
66  private $hookContainer;
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() {
115  if ( !PHPSessionHandler::isEnabled() ) {
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  }
146  return self::$globalSession;
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  // @phan-suppress-next-line PhanUndeclaredConstant
349  if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
350  return [];
351  }
352  // @codeCoverageIgnoreEnd
353  if ( $this->varyHeaders === null ) {
354  $headers = [];
355  foreach ( $this->getProviders() as $provider ) {
356  foreach ( $provider->getVaryHeaders() as $header => $options ) {
357  # Note that the $options value returned has been deprecated
358  # and is ignored.
359  $headers[$header] = null;
360  }
361  }
362  $this->varyHeaders = $headers;
363  }
364  return $this->varyHeaders;
365  }
366 
367  public function getVaryCookies() {
368  // @codeCoverageIgnoreStart
369  // @phan-suppress-next-line PhanUndeclaredConstant
370  if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
371  return [];
372  }
373  // @codeCoverageIgnoreEnd
374  if ( $this->varyCookies === null ) {
375  $cookies = [];
376  foreach ( $this->getProviders() as $provider ) {
377  $cookies = array_merge( $cookies, $provider->getVaryCookies() );
378  }
379  $this->varyCookies = array_values( array_unique( $cookies ) );
380  }
381  return $this->varyCookies;
382  }
383 
389  public static function validateSessionId( $id ) {
390  return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
391  }
392 
407  public function preventSessionsForUser( $username ) {
408  $this->preventUsers[$username] = true;
409 
410  // Instruct the session providers to kill any other sessions too.
411  foreach ( $this->getProviders() as $provider ) {
412  $provider->preventSessionsForUser( $username );
413  }
414  }
415 
422  public function isUserSessionPrevented( $username ) {
423  return !empty( $this->preventUsers[$username] );
424  }
425 
430  protected function getProviders() {
431  if ( $this->sessionProviders === null ) {
432  $this->sessionProviders = [];
433  foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
435  $provider = ObjectFactory::getObjectFromSpec( $spec );
436  $provider->setLogger( $this->logger );
437  $provider->setConfig( $this->config );
438  $provider->setManager( $this );
439  $provider->setHookContainer( $this->hookContainer );
440  if ( isset( $this->sessionProviders[(string)$provider] ) ) {
441  // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
442  throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
443  }
444  $this->sessionProviders[(string)$provider] = $provider;
445  }
446  }
448  }
449 
460  public function getProvider( $name ) {
461  $providers = $this->getProviders();
462  return $providers[$name] ?? null;
463  }
464 
469  public function shutdown() {
470  if ( $this->allSessionBackends ) {
471  $this->logger->debug( 'Saving all sessions on shutdown' );
472  if ( session_id() !== '' ) {
473  // @codeCoverageIgnoreStart
474  session_write_close();
475  }
476  // @codeCoverageIgnoreEnd
477  foreach ( $this->allSessionBackends as $backend ) {
478  $backend->shutdown();
479  }
480  }
481  }
482 
488  private function getSessionInfoForRequest( WebRequest $request ) {
489  // Call all providers to fetch "the" session
490  $infos = [];
491  foreach ( $this->getProviders() as $provider ) {
492  $info = $provider->provideSessionInfo( $request );
493  if ( !$info ) {
494  continue;
495  }
496  if ( $info->getProvider() !== $provider ) {
497  throw new \UnexpectedValueException(
498  "$provider returned session info for a different provider: $info"
499  );
500  }
501  $infos[] = $info;
502  }
503 
504  // Sort the SessionInfos. Then find the first one that can be
505  // successfully loaded, and then all the ones after it with the same
506  // priority.
507  usort( $infos, [ SessionInfo::class, 'compare' ] );
508  $retInfos = [];
509  while ( $infos ) {
510  $info = array_pop( $infos );
511  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
512  $retInfos[] = $info;
513  while ( $infos ) {
514  $info = array_pop( $infos );
515  if ( SessionInfo::compare( $retInfos[0], $info ) ) {
516  // We hit a lower priority, stop checking.
517  break;
518  }
519  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
520  // This is going to error out below, but we want to
521  // provide a complete list.
522  $retInfos[] = $info;
523  } else {
524  // Session load failed, so unpersist it from this request
525  $info->getProvider()->unpersistSession( $request );
526  }
527  }
528  } else {
529  // Session load failed, so unpersist it from this request
530  $info->getProvider()->unpersistSession( $request );
531  }
532  }
533 
534  if ( count( $retInfos ) > 1 ) {
535  throw new SessionOverflowException(
536  $retInfos,
537  'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
538  );
539  }
540 
541  return $retInfos ? $retInfos[0] : null;
542  }
543 
551  private function loadSessionInfoFromStore( SessionInfo &$info, WebRequest $request ) {
552  $key = $this->store->makeKey( 'MWSession', $info->getId() );
553  $blob = $this->store->get( $key );
554 
555  // If we got data from the store and the SessionInfo says to force use,
556  // "fail" means to delete the data from the store and retry. Otherwise,
557  // "fail" is just return false.
558  if ( $info->forceUse() && $blob !== false ) {
559  $failHandler = function () use ( $key, &$info, $request ) {
560  $this->store->delete( $key );
561  return $this->loadSessionInfoFromStore( $info, $request );
562  };
563  } else {
564  $failHandler = function () {
565  return false;
566  };
567  }
568 
569  $newParams = [];
570 
571  if ( $blob !== false ) {
572  // Sanity check: blob must be an array, if it's saved at all
573  if ( !is_array( $blob ) ) {
574  $this->logger->warning( 'Session "{session}": Bad data', [
575  'session' => $info,
576  ] );
577  $this->store->delete( $key );
578  return $failHandler();
579  }
580 
581  // Sanity check: blob has data and metadata arrays
582  if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
583  !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
584  ) {
585  $this->logger->warning( 'Session "{session}": Bad data structure', [
586  'session' => $info,
587  ] );
588  $this->store->delete( $key );
589  return $failHandler();
590  }
591 
592  $data = $blob['data'];
593  $metadata = $blob['metadata'];
594 
595  // Sanity check: metadata must be an array and must contain certain
596  // keys, if it's saved at all
597  if ( !array_key_exists( 'userId', $metadata ) ||
598  !array_key_exists( 'userName', $metadata ) ||
599  !array_key_exists( 'userToken', $metadata ) ||
600  !array_key_exists( 'provider', $metadata )
601  ) {
602  $this->logger->warning( 'Session "{session}": Bad metadata', [
603  'session' => $info,
604  ] );
605  $this->store->delete( $key );
606  return $failHandler();
607  }
608 
609  // First, load the provider from metadata, or validate it against the metadata.
610  $provider = $info->getProvider();
611  if ( $provider === null ) {
612  $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
613  if ( !$provider ) {
614  $this->logger->warning(
615  'Session "{session}": Unknown provider ' . $metadata['provider'],
616  [
617  'session' => $info,
618  ]
619  );
620  $this->store->delete( $key );
621  return $failHandler();
622  }
623  } elseif ( $metadata['provider'] !== (string)$provider ) {
624  $this->logger->warning( 'Session "{session}": Wrong provider ' .
625  $metadata['provider'] . ' !== ' . $provider,
626  [
627  'session' => $info,
628  ] );
629  return $failHandler();
630  }
631 
632  // Load provider metadata from metadata, or validate it against the metadata
633  $providerMetadata = $info->getProviderMetadata();
634  if ( isset( $metadata['providerMetadata'] ) ) {
635  if ( $providerMetadata === null ) {
636  $newParams['metadata'] = $metadata['providerMetadata'];
637  } else {
638  try {
639  $newProviderMetadata = $provider->mergeMetadata(
640  $metadata['providerMetadata'], $providerMetadata
641  );
642  if ( $newProviderMetadata !== $providerMetadata ) {
643  $newParams['metadata'] = $newProviderMetadata;
644  }
645  } catch ( MetadataMergeException $ex ) {
646  $this->logger->warning(
647  'Session "{session}": Metadata merge failed: {exception}',
648  [
649  'session' => $info,
650  'exception' => $ex,
651  ] + $ex->getContext()
652  );
653  return $failHandler();
654  }
655  }
656  }
657 
658  // Next, load the user from metadata, or validate it against the metadata.
659  $userInfo = $info->getUserInfo();
660  if ( !$userInfo ) {
661  // For loading, id is preferred to name.
662  try {
663  if ( $metadata['userId'] ) {
664  $userInfo = UserInfo::newFromId( $metadata['userId'] );
665  } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
666  $userInfo = UserInfo::newFromName( $metadata['userName'] );
667  } else {
668  $userInfo = UserInfo::newAnonymous();
669  }
670  } catch ( \InvalidArgumentException $ex ) {
671  $this->logger->error( 'Session "{session}": {exception}', [
672  'session' => $info,
673  'exception' => $ex,
674  ] );
675  return $failHandler();
676  }
677  $newParams['userInfo'] = $userInfo;
678  } else {
679  // User validation passes if user ID matches, or if there
680  // is no saved ID and the names match.
681  if ( $metadata['userId'] ) {
682  if ( $metadata['userId'] !== $userInfo->getId() ) {
683  $this->logger->warning(
684  'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
685  [
686  'session' => $info,
687  'uid_a' => $metadata['userId'],
688  'uid_b' => $userInfo->getId(),
689  ] );
690  return $failHandler();
691  }
692 
693  // If the user was renamed, probably best to fail here.
694  if ( $metadata['userName'] !== null &&
695  $userInfo->getName() !== $metadata['userName']
696  ) {
697  $this->logger->warning(
698  'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
699  [
700  'session' => $info,
701  'uname_a' => $metadata['userName'],
702  'uname_b' => $userInfo->getName(),
703  ] );
704  return $failHandler();
705  }
706 
707  } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
708  if ( $metadata['userName'] !== $userInfo->getName() ) {
709  $this->logger->warning(
710  'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
711  [
712  'session' => $info,
713  'uname_a' => $metadata['userName'],
714  'uname_b' => $userInfo->getName(),
715  ] );
716  return $failHandler();
717  }
718  } elseif ( !$userInfo->isAnon() ) {
719  // Metadata specifies an anonymous user, but the passed-in
720  // user isn't anonymous.
721  $this->logger->warning(
722  'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
723  [
724  'session' => $info,
725  ] );
726  return $failHandler();
727  }
728  }
729 
730  // And if we have a token in the metadata, it must match the loaded/provided user.
731  if ( $metadata['userToken'] !== null &&
732  $userInfo->getToken() !== $metadata['userToken']
733  ) {
734  $this->logger->warning( 'Session "{session}": User token mismatch', [
735  'session' => $info,
736  ] );
737  return $failHandler();
738  }
739  if ( !$userInfo->isVerified() ) {
740  $newParams['userInfo'] = $userInfo->verified();
741  }
742 
743  if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
744  $newParams['remembered'] = true;
745  }
746  if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
747  $newParams['forceHTTPS'] = true;
748  }
749  if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) {
750  $newParams['persisted'] = true;
751  }
752 
753  if ( !$info->isIdSafe() ) {
754  $newParams['idIsSafe'] = true;
755  }
756  } else {
757  // No metadata, so we can't load the provider if one wasn't given.
758  if ( $info->getProvider() === null ) {
759  $this->logger->warning(
760  'Session "{session}": Null provider and no metadata',
761  [
762  'session' => $info,
763  ] );
764  return $failHandler();
765  }
766 
767  // If no user was provided and no metadata, it must be anon.
768  if ( !$info->getUserInfo() ) {
769  if ( $info->getProvider()->canChangeUser() ) {
770  $newParams['userInfo'] = UserInfo::newAnonymous();
771  } else {
772  $this->logger->info(
773  'Session "{session}": No user provided and provider cannot set user',
774  [
775  'session' => $info,
776  ] );
777  return $failHandler();
778  }
779  } elseif ( !$info->getUserInfo()->isVerified() ) {
780  // probably just a session timeout
781  $this->logger->info(
782  'Session "{session}": Unverified user provided and no metadata to auth it',
783  [
784  'session' => $info,
785  ] );
786  return $failHandler();
787  }
788 
789  $data = false;
790  $metadata = false;
791 
792  if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
793  // The ID doesn't come from the user, so it should be safe
794  // (and if not, nothing we can do about it anyway)
795  $newParams['idIsSafe'] = true;
796  }
797  }
798 
799  // Construct the replacement SessionInfo, if necessary
800  if ( $newParams ) {
801  $newParams['copyFrom'] = $info;
802  $info = new SessionInfo( $info->getPriority(), $newParams );
803  }
804 
805  // Allow the provider to check the loaded SessionInfo
806  $providerMetadata = $info->getProviderMetadata();
807  if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
808  return $failHandler();
809  }
810  if ( $providerMetadata !== $info->getProviderMetadata() ) {
811  $info = new SessionInfo( $info->getPriority(), [
812  'metadata' => $providerMetadata,
813  'copyFrom' => $info,
814  ] );
815  }
816 
817  // Give hooks a chance to abort. Combined with the SessionMetadata
818  // hook, this can allow for tying a session to an IP address or the
819  // like.
820  $reason = 'Hook aborted';
821  if ( !$this->hookRunner->onSessionCheckInfo(
822  $reason, $info, $request, $metadata, $data )
823  ) {
824  $this->logger->warning( 'Session "{session}": ' . $reason, [
825  'session' => $info,
826  ] );
827  return $failHandler();
828  }
829 
830  return true;
831  }
832 
841  public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
842  // @codeCoverageIgnoreStart
843  if ( defined( 'MW_NO_SESSION' ) ) {
844  // @phan-suppress-next-line PhanUndeclaredConstant
845  if ( MW_NO_SESSION === 'warn' ) {
846  // Undocumented safety case for converting existing entry points
847  $this->logger->error( 'Sessions are supposed to be disabled for this entry point', [
848  'exception' => new \BadMethodCallException( 'Sessions are disabled for this entry point' ),
849  ] );
850  } else {
851  throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
852  }
853  }
854  // @codeCoverageIgnoreEnd
855 
856  $id = $info->getId();
857 
858  if ( !isset( $this->allSessionBackends[$id] ) ) {
859  if ( !isset( $this->allSessionIds[$id] ) ) {
860  $this->allSessionIds[$id] = new SessionId( $id );
861  }
862  $backend = new SessionBackend(
863  $this->allSessionIds[$id],
864  $info,
865  $this->store,
866  $this->logger,
867  $this->hookContainer,
868  $this->config->get( 'ObjectCacheSessionExpiry' )
869  );
870  $this->allSessionBackends[$id] = $backend;
871  $delay = $backend->delaySave();
872  } else {
873  $backend = $this->allSessionBackends[$id];
874  $delay = $backend->delaySave();
875  if ( $info->wasPersisted() ) {
876  $backend->persist();
877  }
878  if ( $info->wasRemembered() ) {
879  $backend->setRememberUser( true );
880  }
881  }
882 
883  $request->setSessionId( $backend->getSessionId() );
884  $session = $backend->getSession( $request );
885 
886  if ( !$info->isIdSafe() ) {
887  $session->resetId();
888  }
889 
890  \Wikimedia\ScopedCallback::consume( $delay );
891  return $session;
892  }
893 
899  public function deregisterSessionBackend( SessionBackend $backend ) {
900  $id = $backend->getId();
901  if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
902  $this->allSessionBackends[$id] !== $backend ||
903  $this->allSessionIds[$id] !== $backend->getSessionId()
904  ) {
905  throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
906  }
907 
908  unset( $this->allSessionBackends[$id] );
909  // Explicitly do not unset $this->allSessionIds[$id]
910  }
911 
917  public function changeBackendId( SessionBackend $backend ) {
918  $sessionId = $backend->getSessionId();
919  $oldId = (string)$sessionId;
920  if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
921  $this->allSessionBackends[$oldId] !== $backend ||
922  $this->allSessionIds[$oldId] !== $sessionId
923  ) {
924  throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
925  }
926 
927  $newId = $this->generateSessionId();
928 
929  unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
930  $sessionId->setId( $newId );
931  $this->allSessionBackends[$newId] = $backend;
932  $this->allSessionIds[$newId] = $sessionId;
933  }
934 
939  public function generateSessionId() {
940  do {
941  $id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
942  $key = $this->store->makeKey( 'MWSession', $id );
943  } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
944  return $id;
945  }
946 
952  public function setupPHPSessionHandler( PHPSessionHandler $handler ) {
953  $handler->setManager( $this, $this->store, $this->logger );
954  }
955 
961  public static function resetCache() {
962  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
963  // @codeCoverageIgnoreStart
964  throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
965  // @codeCoverageIgnoreEnd
966  }
967 
968  self::$globalSession = null;
969  self::$globalSessionRequest = null;
970  }
971 
974 }
MediaWiki\Session\SessionManager\isUserSessionPrevented
isUserSessionPrevented( $username)
Test if a user is prevented.
Definition: SessionManager.php:422
MediaWiki\Session\UserInfo\newAnonymous
static newAnonymous()
Create an instance for an anonymous (i.e.
Definition: UserInfo.php:75
MediaWiki\Session\SessionManager\getEmptySessionInternal
getEmptySessionInternal(WebRequest $request=null, $id=null)
Definition: SessionManager.php:277
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
MediaWiki\Session\SessionInfo\forceHTTPS
forceHTTPS()
Whether this session should only be used over HTTPS.
Definition: SessionInfo.php:280
MediaWiki\Session\SessionManager\loadSessionInfoFromStore
loadSessionInfoFromStore(SessionInfo &$info, WebRequest $request)
Load and verify the session info against the store.
Definition: SessionManager.php:551
MediaWiki\Session\SessionManager\$hookContainer
HookContainer $hookContainer
Definition: SessionManager.php:66
MW_NO_SESSION
const MW_NO_SESSION
Definition: load.php:32
MediaWiki\Session\SessionManager\generateSessionId
generateSessionId()
Generate a new random session ID.
Definition: SessionManager.php:939
MediaWiki\Session\SessionManager\getVaryCookies
getVaryCookies()
Return the list of cookies that need varying on.
Definition: SessionManager.php:367
MediaWiki\Session\SessionProvider\setLogger
setLogger(LoggerInterface $logger)
Definition: SessionProvider.php:112
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:152
MediaWiki\Session\SessionManager\$globalSessionRequest
static WebRequest null $globalSessionRequest
Definition: SessionManager.php:60
MediaWiki\Session\SessionManager\getProviders
getProviders()
Get the available SessionProviders.
Definition: SessionManager.php:430
MediaWiki\Session\SessionBackend\getId
getId()
Returns the session ID.
Definition: SessionBackend.php:233
MediaWiki\Session\SessionManager\setupPHPSessionHandler
setupPHPSessionHandler(PHPSessionHandler $handler)
Call setters on a PHPSessionHandler.
Definition: SessionManager.php:952
MediaWiki\Session\SessionInfo\compare
static compare( $a, $b)
Compare two SessionInfo objects by priority.
Definition: SessionInfo.php:296
WebRequest\setSessionId
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:840
MediaWiki\Session\SessionManager\$varyHeaders
array $varyHeaders
Definition: SessionManager.php:84
MediaWiki\Session\SessionManager\preventSessionsForUser
preventSessionsForUser( $username)
Prevent future sessions for the user.
Definition: SessionManager.php:407
MediaWiki\Session\SessionManager\$instance
static SessionManager null $instance
Definition: SessionManager.php:54
MediaWiki\Session\MetadataMergeException
Subclass of UnexpectedValueException that can be annotated with additional data for debug logging.
Definition: MetadataMergeException.php:35
MediaWiki\Session\SessionManager\getSessionById
getSessionById( $id, $create=false, WebRequest $request=null)
Fetch a session by ID.
Definition: SessionManager.php:225
MediaWiki\Session\PHPSessionHandler\isEnabled
static isEnabled()
Test whether the handler is installed and enabled.
Definition: PHPSessionHandler.php:103
MediaWiki\Session\SessionInfo\getPriority
getPriority()
Return the priority.
Definition: SessionInfo.php:227
MediaWiki\Session\SessionInfo\getId
getId()
Return the session ID.
Definition: SessionInfo.php:188
MediaWiki\Session\SessionManager\$allSessionBackends
SessionBackend[] $allSessionBackends
Definition: SessionManager.php:87
MediaWiki\Session\SessionInfo\forceUse
forceUse()
Force use of this SessionInfo if validation fails.
Definition: SessionInfo.php:219
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:70
MediaWiki\Session\SessionOverflowException
OverflowException specific to the SessionManager, used when the request had multiple possible session...
Definition: SessionOverflowException.php:11
MediaWiki\Session\MetadataMergeException\getContext
getContext()
Get context data.
Definition: MetadataMergeException.php:59
MediaWiki\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:183
Config
Interface for configuration instances.
Definition: Config.php:28
MWException
MediaWiki exception.
Definition: MWException.php:26
MediaWiki\Session\SessionManager\$config
Config $config
Definition: SessionManager.php:72
MediaWiki\Session\SessionManager\deregisterSessionBackend
deregisterSessionBackend(SessionBackend $backend)
Deregister a SessionBackend.
Definition: SessionManager.php:899
MediaWiki\Session\SessionManager\validateSessionId
static validateSessionId( $id)
Validate a session ID.
Definition: SessionManager.php:389
MediaWiki\Session\Session
Manages data for an an authenticated session.
Definition: Session.php:48
MediaWiki\Session\SessionProvider
A SessionProvider provides SessionInfo and support for Session.
Definition: SessionProvider.php:80
MediaWiki\Session\SessionInfo\getProvider
getProvider()
Return the provider.
Definition: SessionInfo.php:180
$blob
$blob
Definition: testCompression.php:70
MediaWiki\Session\SessionManager\invalidateSessionsForUser
invalidateSessionsForUser(User $user)
Invalidate sessions for a user.
Definition: SessionManager.php:337
MediaWiki\Session\SessionProvider\setManager
setManager(SessionManager $manager)
Set the session manager.
Definition: SessionProvider.php:128
MediaWiki\Session\SessionManager\$allSessionIds
SessionId[] $allSessionIds
Definition: SessionManager.php:90
MediaWiki
A helper class for throttling authentication attempts.
MediaWiki\Session\SessionManager\$globalSession
static Session null $globalSession
Definition: SessionManager.php:57
ObjectCache\getInstance
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:78
MediaWiki\Session\SessionManager\singleton
static singleton()
Get the global SessionManager.
Definition: SessionManager.php:99
MediaWiki\Session\SessionManager\shutdown
shutdown()
Save all active sessions on shutdown.
Definition: SessionManager.php:469
MediaWiki\Session
Definition: BotPasswordSessionProvider.php:24
MediaWiki\Session\SessionInfo\wasPersisted
wasPersisted()
Return whether the session is persisted.
Definition: SessionInfo.php:243
MediaWiki\Session\SessionManager\resetCache
static resetCache()
Reset the internal caching for unit testing.
Definition: SessionManager.php:961
User\saveSettings
saveSettings()
Save this user's settings into the database.
Definition: User.php:3454
MediaWiki\Session\SessionManager\getVaryHeaders
getVaryHeaders()
Return the HTTP headers that need varying on.
Definition: SessionManager.php:346
MediaWiki\Session\SessionInfo\getProviderMetadata
getProviderMetadata()
Return provider metadata.
Definition: SessionInfo.php:251
MediaWiki\Session\PHPSessionHandler
Adapter for PHP's session handling.
Definition: PHPSessionHandler.php:35
MediaWiki\Session\SessionManager\$sessionProviders
SessionProvider[] $sessionProviders
Definition: SessionManager.php:78
MediaWiki\Session\SessionManager\getGlobalSession
static getGlobalSession()
Get the "global" session.
Definition: SessionManager.php:114
MediaWiki\Session\SessionManager\getSessionInfoForRequest
getSessionInfoForRequest(WebRequest $request)
Fetch the SessionInfo(s) for a request.
Definition: SessionManager.php:488
MediaWiki\Session\SessionBackend\getSessionId
getSessionId()
Fetch the SessionId object.
Definition: SessionBackend.php:242
MediaWiki\Session\SessionManager\setHookContainer
setHookContainer(HookContainer $hookContainer)
Definition: SessionManager.php:209
MediaWiki\Session\SessionManager\setLogger
setLogger(LoggerInterface $logger)
Definition: SessionManager.php:201
MediaWiki\Session\PHPSessionHandler\setManager
setManager(SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger)
Set the manager, store, and logger.
Definition: PHPSessionHandler.php:159
MediaWiki\Session\SessionManager\getEmptySession
getEmptySession(WebRequest $request=null)
Create a new, empty session.
Definition: SessionManager.php:267
$header
$header
Definition: updateCredits.php:41
MediaWiki\Session\UserInfo\newFromName
static newFromName( $name, $verified=false)
Create an instance for a logged-in user by name.
Definition: UserInfo.php:103
MediaWiki\Session\SessionManager
This serves as the entry point to the MediaWiki session handling system.
Definition: SessionManager.php:52
CachedBagOStuff
Wrapper around a BagOStuff that caches data in memory.
Definition: CachedBagOStuff.php:36
MediaWiki\Session\SessionManager\$store
CachedBagOStuff null $store
Definition: SessionManager.php:75
MediaWiki\Session\SessionManager\$preventUsers
string[] $preventUsers
Definition: SessionManager.php:93
MediaWiki\Session\SessionProvider\setConfig
setConfig(Config $config)
Set configuration.
Definition: SessionProvider.php:120
MWCryptRand\generateHex
static generateHex( $chars)
Generate a run of cryptographically random data and return it in hexadecimal string format.
Definition: MWCryptRand.php:36
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:451
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:43
MediaWiki\Session\SessionManager\getSessionFromInfo
getSessionFromInfo(SessionInfo $info, WebRequest $request)
Create a Session corresponding to the passed SessionInfo.
Definition: SessionManager.php:841
MediaWiki\Session\SessionInfo
Value object returned by SessionProvider.
Definition: SessionInfo.php:34
MediaWiki\Session\SessionManagerInterface
This exists to make IDEs happy, so they don't see the internal-but-required-to-be-public methods on S...
Definition: SessionManagerInterface.php:37
User\setToken
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:2538
MediaWiki\Session\SessionId
Value object holding the session ID in a manner that can be globally updated.
Definition: SessionId.php:38
MediaWiki\Session\UserInfo\newFromId
static newFromId( $id, $verified=false)
Create an instance for a logged-in user by ID.
Definition: UserInfo.php:85
MediaWiki\Session\SessionProvider\setHookContainer
setHookContainer( $hookContainer)
Set the hook container.
Definition: SessionProvider.php:145
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:44
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:571
MediaWiki\Session\SessionManager\getProvider
getProvider( $name)
Get a session provider by name.
Definition: SessionManager.php:460
MediaWiki\Session\SessionManager\changeBackendId
changeBackendId(SessionBackend $backend)
Change a SessionBackend's ID.
Definition: SessionManager.php:917
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:54
MediaWiki\Session\SessionInfo\MIN_PRIORITY
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:36
MediaWiki\Session\SessionManager\getSessionForRequest
getSessionForRequest(WebRequest $request)
Fetch the session for a request (or a new empty session if none is attached to it)
Definition: SessionManager.php:214
MediaWiki\Session\SessionManager\$hookRunner
HookRunner $hookRunner
Definition: SessionManager.php:69
MediaWiki\Session\SessionManager\$logger
LoggerInterface $logger
Definition: SessionManager.php:63
MediaWiki\Session\SessionManager\$varyCookies
string[] $varyCookies
Definition: SessionManager.php:81
MediaWiki\Session\SessionInfo\getUserInfo
getUserInfo()
Return the user.
Definition: SessionInfo.php:235
MediaWiki\Session\SessionInfo\wasRemembered
wasRemembered()
Return whether the user was remembered.
Definition: SessionInfo.php:270
MediaWiki\Session\SessionInfo\isIdSafe
isIdSafe()
Indicate whether the ID is "safe".
Definition: SessionInfo.php:204
MediaWiki\Session\SessionManager\__construct
__construct( $options=[])
Definition: SessionManager.php:155
MediaWiki\Session\SessionBackend
This is the actual workhorse for Session.
Definition: SessionBackend.php:52