MediaWiki  master
SessionManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
31 use Config;
33 use User;
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 
71 
73  private $varyCookies = null;
74 
76  private $varyHeaders = null;
77 
79  private $allSessionBackends = [];
80 
82  private $allSessionIds = [];
83 
85  private $preventUsers = [];
86 
92  public static function singleton() {
93  if ( self::$instance === null ) {
94  self::$instance = new self();
95  }
96  return self::$instance;
97  }
98 
107  public static function getGlobalSession() {
108  if ( !PHPSessionHandler::isEnabled() ) {
109  $id = '';
110  } else {
111  $id = session_id();
112  }
113 
114  $request = \RequestContext::getMain()->getRequest();
115  if (
116  !self::$globalSession // No global session is set up yet
117  || self::$globalSessionRequest !== $request // The global WebRequest changed
118  || $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
119  ) {
120  self::$globalSessionRequest = $request;
121  if ( $id === '' ) {
122  // session_id() wasn't used, so fetch the Session from the WebRequest.
123  // We use $request->getSession() instead of $singleton->getSessionForRequest()
124  // because doing the latter would require a public
125  // "$request->getSessionId()" method that would confuse end
126  // users by returning SessionId|null where they'd expect it to
127  // be short for $request->getSession()->getId(), and would
128  // wind up being a duplicate of the code in
129  // $request->getSession() anyway.
130  self::$globalSession = $request->getSession();
131  } else {
132  // Someone used session_id(), so we need to follow suit.
133  // Note this overwrites whatever session might already be
134  // associated with $request with the one for $id.
135  self::$globalSession = self::singleton()->getSessionById( $id, true, $request )
136  ?: $request->getSession();
137  }
138  }
139  return self::$globalSession;
140  }
141 
148  public function __construct( $options = [] ) {
149  if ( isset( $options['config'] ) ) {
150  $this->config = $options['config'];
151  if ( !$this->config instanceof Config ) {
152  throw new \InvalidArgumentException(
153  '$options[\'config\'] must be an instance of Config'
154  );
155  }
156  } else {
157  $this->config = MediaWikiServices::getInstance()->getMainConfig();
158  }
159 
160  if ( isset( $options['logger'] ) ) {
161  if ( !$options['logger'] instanceof LoggerInterface ) {
162  throw new \InvalidArgumentException(
163  '$options[\'logger\'] must be an instance of LoggerInterface'
164  );
165  }
166  $this->setLogger( $options['logger'] );
167  } else {
168  $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
169  }
170 
171  if ( isset( $options['store'] ) ) {
172  if ( !$options['store'] instanceof BagOStuff ) {
173  throw new \InvalidArgumentException(
174  '$options[\'store\'] must be an instance of BagOStuff'
175  );
176  }
177  $store = $options['store'];
178  } else {
179  $store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
180  }
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 
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  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  if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
345  return [];
346  }
347  // @codeCoverageIgnoreEnd
348  if ( $this->varyCookies === null ) {
349  $cookies = [];
350  foreach ( $this->getProviders() as $provider ) {
351  $cookies = array_merge( $cookies, $provider->getVaryCookies() );
352  }
353  $this->varyCookies = array_values( array_unique( $cookies ) );
354  }
355  return $this->varyCookies;
356  }
357 
363  public static function validateSessionId( $id ) {
364  return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
365  }
366 
381  public function preventSessionsForUser( $username ) {
382  $this->preventUsers[$username] = true;
383 
384  // Instruct the session providers to kill any other sessions too.
385  foreach ( $this->getProviders() as $provider ) {
386  $provider->preventSessionsForUser( $username );
387  }
388  }
389 
396  public function isUserSessionPrevented( $username ) {
397  return !empty( $this->preventUsers[$username] );
398  }
399 
404  protected function getProviders() {
405  if ( $this->sessionProviders === null ) {
406  $this->sessionProviders = [];
407  foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
408  $provider = ObjectFactory::getObjectFromSpec( $spec );
409  $provider->setLogger( $this->logger );
410  $provider->setConfig( $this->config );
411  $provider->setManager( $this );
412  if ( isset( $this->sessionProviders[(string)$provider] ) ) {
413  throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
414  }
415  $this->sessionProviders[(string)$provider] = $provider;
416  }
417  }
419  }
420 
431  public function getProvider( $name ) {
432  $providers = $this->getProviders();
433  return $providers[$name] ?? null;
434  }
435 
440  public function shutdown() {
441  if ( $this->allSessionBackends ) {
442  $this->logger->debug( 'Saving all sessions on shutdown' );
443  if ( session_id() !== '' ) {
444  // @codeCoverageIgnoreStart
445  session_write_close();
446  }
447  // @codeCoverageIgnoreEnd
448  foreach ( $this->allSessionBackends as $backend ) {
449  $backend->shutdown();
450  }
451  }
452  }
453 
460  // Call all providers to fetch "the" session
461  $infos = [];
462  foreach ( $this->getProviders() as $provider ) {
463  $info = $provider->provideSessionInfo( $request );
464  if ( !$info ) {
465  continue;
466  }
467  if ( $info->getProvider() !== $provider ) {
468  throw new \UnexpectedValueException(
469  "$provider returned session info for a different provider: $info"
470  );
471  }
472  $infos[] = $info;
473  }
474 
475  // Sort the SessionInfos. Then find the first one that can be
476  // successfully loaded, and then all the ones after it with the same
477  // priority.
478  usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
479  $retInfos = [];
480  while ( $infos ) {
481  $info = array_pop( $infos );
482  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
483  $retInfos[] = $info;
484  while ( $infos ) {
485  $info = array_pop( $infos );
486  if ( SessionInfo::compare( $retInfos[0], $info ) ) {
487  // We hit a lower priority, stop checking.
488  break;
489  }
490  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
491  // This is going to error out below, but we want to
492  // provide a complete list.
493  $retInfos[] = $info;
494  } else {
495  // Session load failed, so unpersist it from this request
496  $info->getProvider()->unpersistSession( $request );
497  }
498  }
499  } else {
500  // Session load failed, so unpersist it from this request
501  $info->getProvider()->unpersistSession( $request );
502  }
503  }
504 
505  if ( count( $retInfos ) > 1 ) {
506  $ex = new \OverflowException(
507  'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
508  );
509  $ex->sessionInfos = $retInfos;
510  throw $ex;
511  }
512 
513  return $retInfos ? $retInfos[0] : null;
514  }
515 
524  $key = $this->store->makeKey( 'MWSession', $info->getId() );
525  $blob = $this->store->get( $key );
526 
527  // If we got data from the store and the SessionInfo says to force use,
528  // "fail" means to delete the data from the store and retry. Otherwise,
529  // "fail" is just return false.
530  if ( $info->forceUse() && $blob !== false ) {
531  $failHandler = function () use ( $key, &$info, $request ) {
532  $this->store->delete( $key );
533  return $this->loadSessionInfoFromStore( $info, $request );
534  };
535  } else {
536  $failHandler = function () {
537  return false;
538  };
539  }
540 
541  $newParams = [];
542 
543  if ( $blob !== false ) {
544  // Sanity check: blob must be an array, if it's saved at all
545  if ( !is_array( $blob ) ) {
546  $this->logger->warning( 'Session "{session}": Bad data', [
547  'session' => $info,
548  ] );
549  $this->store->delete( $key );
550  return $failHandler();
551  }
552 
553  // Sanity check: blob has data and metadata arrays
554  if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
555  !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
556  ) {
557  $this->logger->warning( 'Session "{session}": Bad data structure', [
558  'session' => $info,
559  ] );
560  $this->store->delete( $key );
561  return $failHandler();
562  }
563 
564  $data = $blob['data'];
565  $metadata = $blob['metadata'];
566 
567  // Sanity check: metadata must be an array and must contain certain
568  // keys, if it's saved at all
569  if ( !array_key_exists( 'userId', $metadata ) ||
570  !array_key_exists( 'userName', $metadata ) ||
571  !array_key_exists( 'userToken', $metadata ) ||
572  !array_key_exists( 'provider', $metadata )
573  ) {
574  $this->logger->warning( 'Session "{session}": Bad metadata', [
575  'session' => $info,
576  ] );
577  $this->store->delete( $key );
578  return $failHandler();
579  }
580 
581  // First, load the provider from metadata, or validate it against the metadata.
582  $provider = $info->getProvider();
583  if ( $provider === null ) {
584  $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
585  if ( !$provider ) {
586  $this->logger->warning(
587  'Session "{session}": Unknown provider ' . $metadata['provider'],
588  [
589  'session' => $info,
590  ]
591  );
592  $this->store->delete( $key );
593  return $failHandler();
594  }
595  } elseif ( $metadata['provider'] !== (string)$provider ) {
596  $this->logger->warning( 'Session "{session}": Wrong provider ' .
597  $metadata['provider'] . ' !== ' . $provider,
598  [
599  'session' => $info,
600  ] );
601  return $failHandler();
602  }
603 
604  // Load provider metadata from metadata, or validate it against the metadata
605  $providerMetadata = $info->getProviderMetadata();
606  if ( isset( $metadata['providerMetadata'] ) ) {
607  if ( $providerMetadata === null ) {
608  $newParams['metadata'] = $metadata['providerMetadata'];
609  } else {
610  try {
611  $newProviderMetadata = $provider->mergeMetadata(
612  $metadata['providerMetadata'], $providerMetadata
613  );
614  if ( $newProviderMetadata !== $providerMetadata ) {
615  $newParams['metadata'] = $newProviderMetadata;
616  }
617  } catch ( MetadataMergeException $ex ) {
618  $this->logger->warning(
619  'Session "{session}": Metadata merge failed: {exception}',
620  [
621  'session' => $info,
622  'exception' => $ex,
623  ] + $ex->getContext()
624  );
625  return $failHandler();
626  }
627  }
628  }
629 
630  // Next, load the user from metadata, or validate it against the metadata.
631  $userInfo = $info->getUserInfo();
632  if ( !$userInfo ) {
633  // For loading, id is preferred to name.
634  try {
635  if ( $metadata['userId'] ) {
636  $userInfo = UserInfo::newFromId( $metadata['userId'] );
637  } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
638  $userInfo = UserInfo::newFromName( $metadata['userName'] );
639  } else {
640  $userInfo = UserInfo::newAnonymous();
641  }
642  } catch ( \InvalidArgumentException $ex ) {
643  $this->logger->error( 'Session "{session}": {exception}', [
644  'session' => $info,
645  'exception' => $ex,
646  ] );
647  return $failHandler();
648  }
649  $newParams['userInfo'] = $userInfo;
650  } else {
651  // User validation passes if user ID matches, or if there
652  // is no saved ID and the names match.
653  if ( $metadata['userId'] ) {
654  if ( $metadata['userId'] !== $userInfo->getId() ) {
655  $this->logger->warning(
656  'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
657  [
658  'session' => $info,
659  'uid_a' => $metadata['userId'],
660  'uid_b' => $userInfo->getId(),
661  ] );
662  return $failHandler();
663  }
664 
665  // If the user was renamed, probably best to fail here.
666  if ( $metadata['userName'] !== null &&
667  $userInfo->getName() !== $metadata['userName']
668  ) {
669  $this->logger->warning(
670  'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
671  [
672  'session' => $info,
673  'uname_a' => $metadata['userName'],
674  'uname_b' => $userInfo->getName(),
675  ] );
676  return $failHandler();
677  }
678 
679  } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
680  if ( $metadata['userName'] !== $userInfo->getName() ) {
681  $this->logger->warning(
682  'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
683  [
684  'session' => $info,
685  'uname_a' => $metadata['userName'],
686  'uname_b' => $userInfo->getName(),
687  ] );
688  return $failHandler();
689  }
690  } elseif ( !$userInfo->isAnon() ) {
691  // Metadata specifies an anonymous user, but the passed-in
692  // user isn't anonymous.
693  $this->logger->warning(
694  'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
695  [
696  'session' => $info,
697  ] );
698  return $failHandler();
699  }
700  }
701 
702  // And if we have a token in the metadata, it must match the loaded/provided user.
703  if ( $metadata['userToken'] !== null &&
704  $userInfo->getToken() !== $metadata['userToken']
705  ) {
706  $this->logger->warning( 'Session "{session}": User token mismatch', [
707  'session' => $info,
708  ] );
709  return $failHandler();
710  }
711  if ( !$userInfo->isVerified() ) {
712  $newParams['userInfo'] = $userInfo->verified();
713  }
714 
715  if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
716  $newParams['remembered'] = true;
717  }
718  if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
719  $newParams['forceHTTPS'] = true;
720  }
721  if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) {
722  $newParams['persisted'] = true;
723  }
724 
725  if ( !$info->isIdSafe() ) {
726  $newParams['idIsSafe'] = true;
727  }
728  } else {
729  // No metadata, so we can't load the provider if one wasn't given.
730  if ( $info->getProvider() === null ) {
731  $this->logger->warning(
732  'Session "{session}": Null provider and no metadata',
733  [
734  'session' => $info,
735  ] );
736  return $failHandler();
737  }
738 
739  // If no user was provided and no metadata, it must be anon.
740  if ( !$info->getUserInfo() ) {
741  if ( $info->getProvider()->canChangeUser() ) {
742  $newParams['userInfo'] = UserInfo::newAnonymous();
743  } else {
744  $this->logger->info(
745  'Session "{session}": No user provided and provider cannot set user',
746  [
747  'session' => $info,
748  ] );
749  return $failHandler();
750  }
751  } elseif ( !$info->getUserInfo()->isVerified() ) {
752  // probably just a session timeout
753  $this->logger->info(
754  'Session "{session}": Unverified user provided and no metadata to auth it',
755  [
756  'session' => $info,
757  ] );
758  return $failHandler();
759  }
760 
761  $data = false;
762  $metadata = false;
763 
764  if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
765  // The ID doesn't come from the user, so it should be safe
766  // (and if not, nothing we can do about it anyway)
767  $newParams['idIsSafe'] = true;
768  }
769  }
770 
771  // Construct the replacement SessionInfo, if necessary
772  if ( $newParams ) {
773  $newParams['copyFrom'] = $info;
774  $info = new SessionInfo( $info->getPriority(), $newParams );
775  }
776 
777  // Allow the provider to check the loaded SessionInfo
778  $providerMetadata = $info->getProviderMetadata();
779  if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
780  return $failHandler();
781  }
782  if ( $providerMetadata !== $info->getProviderMetadata() ) {
783  $info = new SessionInfo( $info->getPriority(), [
784  'metadata' => $providerMetadata,
785  'copyFrom' => $info,
786  ] );
787  }
788 
789  // Give hooks a chance to abort. Combined with the SessionMetadata
790  // hook, this can allow for tying a session to an IP address or the
791  // like.
792  $reason = 'Hook aborted';
793  if ( !\Hooks::run(
794  'SessionCheckInfo',
795  [ &$reason, $info, $request, $metadata, $data ]
796  ) ) {
797  $this->logger->warning( 'Session "{session}": ' . $reason, [
798  'session' => $info,
799  ] );
800  return $failHandler();
801  }
802 
803  return true;
804  }
805 
814  public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
815  // @codeCoverageIgnoreStart
816  if ( defined( 'MW_NO_SESSION' ) ) {
817  if ( MW_NO_SESSION === 'warn' ) {
818  // Undocumented safety case for converting existing entry points
819  $this->logger->error( 'Sessions are supposed to be disabled for this entry point', [
820  'exception' => new \BadMethodCallException( 'Sessions are disabled for this entry point' ),
821  ] );
822  } else {
823  throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
824  }
825  }
826  // @codeCoverageIgnoreEnd
827 
828  $id = $info->getId();
829 
830  if ( !isset( $this->allSessionBackends[$id] ) ) {
831  if ( !isset( $this->allSessionIds[$id] ) ) {
832  $this->allSessionIds[$id] = new SessionId( $id );
833  }
834  $backend = new SessionBackend(
835  $this->allSessionIds[$id],
836  $info,
837  $this->store,
838  $this->logger,
839  $this->config->get( 'ObjectCacheSessionExpiry' )
840  );
841  $this->allSessionBackends[$id] = $backend;
842  $delay = $backend->delaySave();
843  } else {
844  $backend = $this->allSessionBackends[$id];
845  $delay = $backend->delaySave();
846  if ( $info->wasPersisted() ) {
847  $backend->persist();
848  }
849  if ( $info->wasRemembered() ) {
850  $backend->setRememberUser( true );
851  }
852  }
853 
854  $request->setSessionId( $backend->getSessionId() );
855  $session = $backend->getSession( $request );
856 
857  if ( !$info->isIdSafe() ) {
858  $session->resetId();
859  }
860 
861  \Wikimedia\ScopedCallback::consume( $delay );
862  return $session;
863  }
864 
870  public function deregisterSessionBackend( SessionBackend $backend ) {
871  $id = $backend->getId();
872  if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
873  $this->allSessionBackends[$id] !== $backend ||
874  $this->allSessionIds[$id] !== $backend->getSessionId()
875  ) {
876  throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
877  }
878 
879  unset( $this->allSessionBackends[$id] );
880  // Explicitly do not unset $this->allSessionIds[$id]
881  }
882 
888  public function changeBackendId( SessionBackend $backend ) {
889  $sessionId = $backend->getSessionId();
890  $oldId = (string)$sessionId;
891  if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
892  $this->allSessionBackends[$oldId] !== $backend ||
893  $this->allSessionIds[$oldId] !== $sessionId
894  ) {
895  throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
896  }
897 
898  $newId = $this->generateSessionId();
899 
900  unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
901  $sessionId->setId( $newId );
902  $this->allSessionBackends[$newId] = $backend;
903  $this->allSessionIds[$newId] = $sessionId;
904  }
905 
910  public function generateSessionId() {
911  do {
912  $id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
913  $key = $this->store->makeKey( 'MWSession', $id );
914  } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
915  return $id;
916  }
917 
924  $handler->setManager( $this, $this->store, $this->logger );
925  }
926 
931  public static function resetCache() {
932  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
933  // @codeCoverageIgnoreStart
934  throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
935  // @codeCoverageIgnoreEnd
936  }
937 
938  self::$globalSession = null;
939  self::$globalSessionRequest = null;
940  }
941 
944 }
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:3969
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
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.
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition: hooks.txt:175
static getInstance( $id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:92
static SessionManager null $instance
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:2823
static compare( $a, $b)
Compare two SessionInfo objects by priority.
shutdown()
Save all active sessions on shutdown.
A helper class for throttling authentication attempts.
getId()
Return the session ID.
static getInstance()
Returns the global default instance of the top level service locator.
Subclass of UnexpectedValueException that can be annotated with additional data for debug logging...
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
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:767
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.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1963
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
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
getId()
Returns the session ID.
getProviderMetadata()
Return provider metadata.
wasPersisted()
Return whether the session is persisted.
$header
Adapter for PHP&#39;s session handling.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
static singleton()
Get the global SessionManager.
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:769
generateSessionId()
Generate a new random session ID.
static isEnabled()
Test whether the handler is installed and enabled.
static getGlobalSession()
Get the "global" session.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
invalidateSessionsForUser(User $user)
Invalidate sessions for a user.
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:767
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
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
This serves as the entry point to the MediaWiki session handling system.
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.
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2614
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.
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1454
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".