MediaWiki  1.34.0
SessionManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
27 use MWException;
28 use Psr\Log\LoggerInterface;
29 use BagOStuff;
30 use CachedBagOStuff;
31 use Config;
32 use FauxRequest;
33 use User;
34 use WebRequest;
35 use Wikimedia\ObjectFactory;
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 }
MediaWiki\Session\SessionManager\isUserSessionPrevented
isUserSessionPrevented( $username)
Test if a user is prevented.
Definition: SessionManager.php:398
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:253
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:278
MediaWiki\Session\SessionManager\loadSessionInfoFromStore
loadSessionInfoFromStore(SessionInfo &$info, WebRequest $request)
Load and verify the session info against the store.
Definition: SessionManager.php:525
MW_NO_SESSION
const MW_NO_SESSION
Definition: load.php:29
MediaWiki\Session\SessionManager\generateSessionId
generateSessionId()
Generate a new random session ID.
Definition: SessionManager.php:913
MediaWiki\Session\SessionManager\getVaryCookies
getVaryCookies()
Return the list of cookies that need varying on.
Definition: SessionManager.php:343
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
MediaWiki\Session\SessionManager\$globalSessionRequest
static WebRequest null $globalSessionRequest
Definition: SessionManager.php:58
MediaWiki\Session\SessionManager\getProviders
getProviders()
Get the available SessionProviders.
Definition: SessionManager.php:406
MediaWiki\Session\SessionBackend\getId
getId()
Returns the session ID.
Definition: SessionBackend.php:225
MediaWiki\Session\SessionManager\setupPHPSessionHandler
setupPHPSessionHandler(PHPSessionHandler $handler)
Call setters on a PHPSessionHandler.
Definition: SessionManager.php:926
MediaWiki\Session\SessionInfo\compare
static compare( $a, $b)
Compare two SessionInfo objects by priority.
Definition: SessionInfo.php:294
WebRequest\setSessionId
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:817
MediaWiki\Session\SessionManager\$varyHeaders
array $varyHeaders
Definition: SessionManager.php:76
MediaWiki\Session\SessionManager\preventSessionsForUser
preventSessionsForUser( $username)
Prevent future sessions for the user.
Definition: SessionManager.php:383
MediaWiki\Session\SessionManager\$instance
static SessionManager null $instance
Definition: SessionManager.php:52
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:201
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:79
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:63
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:138
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:64
MediaWiki\Session\SessionManager\deregisterSessionBackend
deregisterSessionBackend(SessionBackend $backend)
Deregister a SessionBackend.
Definition: SessionManager.php:873
MediaWiki\Session\SessionManager\validateSessionId
static validateSessionId( $id)
Validate a session ID.
Definition: SessionManager.php:365
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:78
MediaWiki\Session\SessionInfo\getProvider
getProvider()
Return the provider.
Definition: SessionInfo.php:180
$blob
$blob
Definition: testCompression.php:65
MediaWiki\Session\SessionManager\invalidateSessionsForUser
invalidateSessionsForUser(User $user)
Invalidate sessions for a user.
Definition: SessionManager.php:313
MediaWiki\Session\SessionManager\$allSessionIds
SessionId[] $allSessionIds
Definition: SessionManager.php:82
MediaWiki
This class serves as a utility class for this extension.
MediaWiki\Session\SessionManager\$globalSession
static Session null $globalSession
Definition: SessionManager.php:55
ObjectCache\getInstance
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:80
MediaWiki\Session\SessionManager\singleton
static singleton()
Get the global SessionManager.
Definition: SessionManager.php:91
MediaWiki\Session\SessionManager\shutdown
shutdown()
Save all active sessions on shutdown.
Definition: SessionManager.php:443
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:934
User\saveSettings
saveSettings()
Save this user's settings into the database.
Definition: User.php:3921
MediaWiki\Session\SessionManager\getVaryHeaders
getVaryHeaders()
Return the HTTP headers that need varying on.
Definition: SessionManager.php:322
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:70
MediaWiki\Session\SessionManager\getGlobalSession
static getGlobalSession()
Get the "global" session.
Definition: SessionManager.php:106
MediaWiki\Session\SessionManager\getSessionInfoForRequest
getSessionInfoForRequest(WebRequest $request)
Fetch the SessionInfo(s) for a request.
Definition: SessionManager.php:462
MediaWiki\Session\SessionBackend\getSessionId
getSessionId()
Fetch the SessionId object.
Definition: SessionBackend.php:234
MediaWiki\Session\SessionManager\setLogger
setLogger(LoggerInterface $logger)
Definition: SessionManager.php:186
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:243
$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:50
CachedBagOStuff
Wrapper around a BagOStuff that caches data in memory.
Definition: CachedBagOStuff.php:36
MediaWiki\Session\SessionManager\$store
CachedBagOStuff null $store
Definition: SessionManager.php:67
MediaWiki\Session\SessionManager\$preventUsers
string[] $preventUsers
Definition: SessionManager.php:85
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:431
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:42
MediaWiki\Session\SessionManager\getSessionFromInfo
getSessionFromInfo(SessionInfo $info, WebRequest $request)
Create a Session corresponding to the passed SessionInfo.
Definition: SessionManager.php:816
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
MediaWiki\Session\SessionBackend\delaySave
delaySave()
Delay automatic saving while multiple updates are being made.
Definition: SessionBackend.php:613
User\setToken
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:2785
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\SessionManager\getProvider
getProvider( $name)
Get a session provider by name.
Definition: SessionManager.php:434
MediaWiki\Session\SessionManager\changeBackendId
changeBackendId(SessionBackend $backend)
Change a SessionBackend's ID.
Definition: SessionManager.php:891
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
MediaWiki\Session\SessionInfo\MIN_PRIORITY
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:36
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
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:190
MediaWiki\Session\SessionManager\$logger
LoggerInterface $logger
Definition: SessionManager.php:61
MediaWiki\Session\SessionManager\$varyCookies
string[] $varyCookies
Definition: SessionManager.php:73
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:147
MediaWiki\Session\SessionBackend
This is the actual workhorse for Session.
Definition: SessionBackend.php:50