MediaWiki  1.28.1
SessionManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
30 use Config;
32 use User;
34 
48 final class SessionManager implements SessionManagerInterface {
50  private static $instance = null;
51 
53  private static $globalSession = null;
54 
56  private static $globalSessionRequest = null;
57 
59  private $logger;
60 
62  private $config;
63 
65  private $store;
66 
68  private $sessionProviders = null;
69 
71  private $varyCookies = null;
72 
74  private $varyHeaders = null;
75 
77  private $allSessionBackends = [];
78 
80  private $allSessionIds = [];
81 
83  private $preventUsers = [];
84 
90  public static function singleton() {
91  if ( self::$instance === null ) {
92  self::$instance = new self();
93  }
94  return self::$instance;
95  }
96 
105  public static function getGlobalSession() {
106  if ( !PHPSessionHandler::isEnabled() ) {
107  $id = '';
108  } else {
109  $id = session_id();
110  }
111 
112  $request = \RequestContext::getMain()->getRequest();
113  if (
114  !self::$globalSession // No global session is set up yet
115  || self::$globalSessionRequest !== $request // The global WebRequest changed
116  || $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
117  ) {
118  self::$globalSessionRequest = $request;
119  if ( $id === '' ) {
120  // session_id() wasn't used, so fetch the Session from the WebRequest.
121  // We use $request->getSession() instead of $singleton->getSessionForRequest()
122  // because doing the latter would require a public
123  // "$request->getSessionId()" method that would confuse end
124  // users by returning SessionId|null where they'd expect it to
125  // be short for $request->getSession()->getId(), and would
126  // wind up being a duplicate of the code in
127  // $request->getSession() anyway.
128  self::$globalSession = $request->getSession();
129  } else {
130  // Someone used session_id(), so we need to follow suit.
131  // Note this overwrites whatever session might already be
132  // associated with $request with the one for $id.
133  self::$globalSession = self::singleton()->getSessionById( $id, true, $request )
134  ?: $request->getSession();
135  }
136  }
137  return self::$globalSession;
138  }
139 
146  public function __construct( $options = [] ) {
147  if ( isset( $options['config'] ) ) {
148  $this->config = $options['config'];
149  if ( !$this->config instanceof Config ) {
150  throw new \InvalidArgumentException(
151  '$options[\'config\'] must be an instance of Config'
152  );
153  }
154  } else {
155  $this->config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
156  }
157 
158  if ( isset( $options['logger'] ) ) {
159  if ( !$options['logger'] instanceof LoggerInterface ) {
160  throw new \InvalidArgumentException(
161  '$options[\'logger\'] must be an instance of LoggerInterface'
162  );
163  }
164  $this->setLogger( $options['logger'] );
165  } else {
166  $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
167  }
168 
169  if ( isset( $options['store'] ) ) {
170  if ( !$options['store'] instanceof BagOStuff ) {
171  throw new \InvalidArgumentException(
172  '$options[\'store\'] must be an instance of BagOStuff'
173  );
174  }
175  $store = $options['store'];
176  } else {
177  $store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
178  }
179  $this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store );
180 
181  register_shutdown_function( [ $this, 'shutdown' ] );
182  }
183 
184  public function setLogger( LoggerInterface $logger ) {
185  $this->logger = $logger;
186  }
187 
189  $info = $this->getSessionInfoForRequest( $request );
190 
191  if ( !$info ) {
192  $session = $this->getEmptySession( $request );
193  } else {
194  $session = $this->getSessionFromInfo( $info, $request );
195  }
196  return $session;
197  }
198 
199  public function getSessionById( $id, $create = false, WebRequest $request = null ) {
200  if ( !self::validateSessionId( $id ) ) {
201  throw new \InvalidArgumentException( 'Invalid session ID' );
202  }
203  if ( !$request ) {
204  $request = new FauxRequest;
205  }
206 
207  $session = null;
208  $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => $id, 'idIsSafe' => true ] );
209 
210  // If we already have the backend loaded, use it directly
211  if ( isset( $this->allSessionBackends[$id] ) ) {
212  return $this->getSessionFromInfo( $info, $request );
213  }
214 
215  // Test if the session is in storage, and if so try to load it.
216  $key = wfMemcKey( 'MWSession', $id );
217  if ( is_array( $this->store->get( $key ) ) ) {
218  $create = false; // If loading fails, don't bother creating because it probably will fail too.
219  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
220  $session = $this->getSessionFromInfo( $info, $request );
221  }
222  }
223 
224  if ( $create && $session === null ) {
225  $ex = null;
226  try {
227  $session = $this->getEmptySessionInternal( $request, $id );
228  } catch ( \Exception $ex ) {
229  $this->logger->error( 'Failed to create empty session: {exception}',
230  [
231  'method' => __METHOD__,
232  'exception' => $ex,
233  ] );
234  $session = null;
235  }
236  }
237 
238  return $session;
239  }
240 
241  public function getEmptySession( WebRequest $request = null ) {
242  return $this->getEmptySessionInternal( $request );
243  }
244 
251  private function getEmptySessionInternal( WebRequest $request = null, $id = null ) {
252  if ( $id !== null ) {
253  if ( !self::validateSessionId( $id ) ) {
254  throw new \InvalidArgumentException( 'Invalid session ID' );
255  }
256 
257  $key = wfMemcKey( 'MWSession', $id );
258  if ( is_array( $this->store->get( $key ) ) ) {
259  throw new \InvalidArgumentException( 'Session ID already exists' );
260  }
261  }
262  if ( !$request ) {
263  $request = new FauxRequest;
264  }
265 
266  $infos = [];
267  foreach ( $this->getProviders() as $provider ) {
268  $info = $provider->newSessionInfo( $id );
269  if ( !$info ) {
270  continue;
271  }
272  if ( $info->getProvider() !== $provider ) {
273  throw new \UnexpectedValueException(
274  "$provider returned an empty session info for a different provider: $info"
275  );
276  }
277  if ( $id !== null && $info->getId() !== $id ) {
278  throw new \UnexpectedValueException(
279  "$provider returned empty session info with a wrong id: " .
280  $info->getId() . ' != ' . $id
281  );
282  }
283  if ( !$info->isIdSafe() ) {
284  throw new \UnexpectedValueException(
285  "$provider returned empty session info with id flagged unsafe"
286  );
287  }
288  $compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
289  if ( $compare > 0 ) {
290  continue;
291  }
292  if ( $compare === 0 ) {
293  $infos[] = $info;
294  } else {
295  $infos = [ $info ];
296  }
297  }
298 
299  // Make sure there's exactly one
300  if ( count( $infos ) > 1 ) {
301  throw new \UnexpectedValueException(
302  'Multiple empty sessions tied for top priority: ' . implode( ', ', $infos )
303  );
304  } elseif ( count( $infos ) < 1 ) {
305  throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
306  }
307 
308  return $this->getSessionFromInfo( $infos[0], $request );
309  }
310 
311  public function invalidateSessionsForUser( User $user ) {
312  $user->setToken();
313  $user->saveSettings();
314 
315  $authUser = \MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ] );
316  if ( $authUser ) {
317  $authUser->resetAuthToken();
318  }
319 
320  foreach ( $this->getProviders() as $provider ) {
321  $provider->invalidateSessionsForUser( $user );
322  }
323  }
324 
325  public function getVaryHeaders() {
326  // @codeCoverageIgnoreStart
327  if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
328  return [];
329  }
330  // @codeCoverageIgnoreEnd
331  if ( $this->varyHeaders === null ) {
332  $headers = [];
333  foreach ( $this->getProviders() as $provider ) {
334  foreach ( $provider->getVaryHeaders() as $header => $options ) {
335  if ( !isset( $headers[$header] ) ) {
336  $headers[$header] = [];
337  }
338  if ( is_array( $options ) ) {
339  $headers[$header] = array_unique( array_merge( $headers[$header], $options ) );
340  }
341  }
342  }
343  $this->varyHeaders = $headers;
344  }
345  return $this->varyHeaders;
346  }
347 
348  public function getVaryCookies() {
349  // @codeCoverageIgnoreStart
350  if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
351  return [];
352  }
353  // @codeCoverageIgnoreEnd
354  if ( $this->varyCookies === null ) {
355  $cookies = [];
356  foreach ( $this->getProviders() as $provider ) {
357  $cookies = array_merge( $cookies, $provider->getVaryCookies() );
358  }
359  $this->varyCookies = array_values( array_unique( $cookies ) );
360  }
361  return $this->varyCookies;
362  }
363 
369  public static function validateSessionId( $id ) {
370  return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
371  }
372 
386  public static function autoCreateUser( User $user ) {
387  wfDeprecated( __METHOD__, '1.27' );
388  return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser(
389  $user,
390  \MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION,
391  false
392  )->isGood();
393  }
394 
404  public function preventSessionsForUser( $username ) {
405  $this->preventUsers[$username] = true;
406 
407  // Instruct the session providers to kill any other sessions too.
408  foreach ( $this->getProviders() as $provider ) {
409  $provider->preventSessionsForUser( $username );
410  }
411  }
412 
419  public function isUserSessionPrevented( $username ) {
420  return !empty( $this->preventUsers[$username] );
421  }
422 
427  protected function getProviders() {
428  if ( $this->sessionProviders === null ) {
429  $this->sessionProviders = [];
430  foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
431  $provider = \ObjectFactory::getObjectFromSpec( $spec );
432  $provider->setLogger( $this->logger );
433  $provider->setConfig( $this->config );
434  $provider->setManager( $this );
435  if ( isset( $this->sessionProviders[(string)$provider] ) ) {
436  throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
437  }
438  $this->sessionProviders[(string)$provider] = $provider;
439  }
440  }
442  }
443 
454  public function getProvider( $name ) {
455  $providers = $this->getProviders();
456  return isset( $providers[$name] ) ? $providers[$name] : null;
457  }
458 
463  public function shutdown() {
464  if ( $this->allSessionBackends ) {
465  $this->logger->debug( 'Saving all sessions on shutdown' );
466  if ( session_id() !== '' ) {
467  // @codeCoverageIgnoreStart
468  session_write_close();
469  }
470  // @codeCoverageIgnoreEnd
471  foreach ( $this->allSessionBackends as $backend ) {
472  $backend->shutdown();
473  }
474  }
475  }
476 
483  // Call all providers to fetch "the" session
484  $infos = [];
485  foreach ( $this->getProviders() as $provider ) {
486  $info = $provider->provideSessionInfo( $request );
487  if ( !$info ) {
488  continue;
489  }
490  if ( $info->getProvider() !== $provider ) {
491  throw new \UnexpectedValueException(
492  "$provider returned session info for a different provider: $info"
493  );
494  }
495  $infos[] = $info;
496  }
497 
498  // Sort the SessionInfos. Then find the first one that can be
499  // successfully loaded, and then all the ones after it with the same
500  // priority.
501  usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
502  $retInfos = [];
503  while ( $infos ) {
504  $info = array_pop( $infos );
505  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
506  $retInfos[] = $info;
507  while ( $infos ) {
508  $info = array_pop( $infos );
509  if ( SessionInfo::compare( $retInfos[0], $info ) ) {
510  // We hit a lower priority, stop checking.
511  break;
512  }
513  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
514  // This is going to error out below, but we want to
515  // provide a complete list.
516  $retInfos[] = $info;
517  } else {
518  // Session load failed, so unpersist it from this request
519  $info->getProvider()->unpersistSession( $request );
520  }
521  }
522  } else {
523  // Session load failed, so unpersist it from this request
524  $info->getProvider()->unpersistSession( $request );
525  }
526  }
527 
528  if ( count( $retInfos ) > 1 ) {
529  $ex = new \OverflowException(
530  'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
531  );
532  $ex->sessionInfos = $retInfos;
533  throw $ex;
534  }
535 
536  return $retInfos ? $retInfos[0] : null;
537  }
538 
547  $key = wfMemcKey( 'MWSession', $info->getId() );
548  $blob = $this->store->get( $key );
549 
550  // If we got data from the store and the SessionInfo says to force use,
551  // "fail" means to delete the data from the store and retry. Otherwise,
552  // "fail" is just return false.
553  if ( $info->forceUse() && $blob !== false ) {
554  $failHandler = function () use ( $key, &$info, $request ) {
555  $this->store->delete( $key );
556  return $this->loadSessionInfoFromStore( $info, $request );
557  };
558  } else {
559  $failHandler = function () {
560  return false;
561  };
562  }
563 
564  $newParams = [];
565 
566  if ( $blob !== false ) {
567  // Sanity check: blob must be an array, if it's saved at all
568  if ( !is_array( $blob ) ) {
569  $this->logger->warning( 'Session "{session}": Bad data', [
570  'session' => $info,
571  ] );
572  $this->store->delete( $key );
573  return $failHandler();
574  }
575 
576  // Sanity check: blob has data and metadata arrays
577  if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
578  !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
579  ) {
580  $this->logger->warning( 'Session "{session}": Bad data structure', [
581  'session' => $info,
582  ] );
583  $this->store->delete( $key );
584  return $failHandler();
585  }
586 
587  $data = $blob['data'];
588  $metadata = $blob['metadata'];
589 
590  // Sanity check: metadata must be an array and must contain certain
591  // keys, if it's saved at all
592  if ( !array_key_exists( 'userId', $metadata ) ||
593  !array_key_exists( 'userName', $metadata ) ||
594  !array_key_exists( 'userToken', $metadata ) ||
595  !array_key_exists( 'provider', $metadata )
596  ) {
597  $this->logger->warning( 'Session "{session}": Bad metadata', [
598  'session' => $info,
599  ] );
600  $this->store->delete( $key );
601  return $failHandler();
602  }
603 
604  // First, load the provider from metadata, or validate it against the metadata.
605  $provider = $info->getProvider();
606  if ( $provider === null ) {
607  $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
608  if ( !$provider ) {
609  $this->logger->warning(
610  'Session "{session}": Unknown provider ' . $metadata['provider'],
611  [
612  'session' => $info,
613  ]
614  );
615  $this->store->delete( $key );
616  return $failHandler();
617  }
618  } elseif ( $metadata['provider'] !== (string)$provider ) {
619  $this->logger->warning( 'Session "{session}": Wrong provider ' .
620  $metadata['provider'] . ' !== ' . $provider,
621  [
622  'session' => $info,
623  ] );
624  return $failHandler();
625  }
626 
627  // Load provider metadata from metadata, or validate it against the metadata
628  $providerMetadata = $info->getProviderMetadata();
629  if ( isset( $metadata['providerMetadata'] ) ) {
630  if ( $providerMetadata === null ) {
631  $newParams['metadata'] = $metadata['providerMetadata'];
632  } else {
633  try {
634  $newProviderMetadata = $provider->mergeMetadata(
635  $metadata['providerMetadata'], $providerMetadata
636  );
637  if ( $newProviderMetadata !== $providerMetadata ) {
638  $newParams['metadata'] = $newProviderMetadata;
639  }
640  } catch ( MetadataMergeException $ex ) {
641  $this->logger->warning(
642  'Session "{session}": Metadata merge failed: {exception}',
643  [
644  'session' => $info,
645  'exception' => $ex,
646  ] + $ex->getContext()
647  );
648  return $failHandler();
649  }
650  }
651  }
652 
653  // Next, load the user from metadata, or validate it against the metadata.
654  $userInfo = $info->getUserInfo();
655  if ( !$userInfo ) {
656  // For loading, id is preferred to name.
657  try {
658  if ( $metadata['userId'] ) {
659  $userInfo = UserInfo::newFromId( $metadata['userId'] );
660  } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
661  $userInfo = UserInfo::newFromName( $metadata['userName'] );
662  } else {
663  $userInfo = UserInfo::newAnonymous();
664  }
665  } catch ( \InvalidArgumentException $ex ) {
666  $this->logger->error( 'Session "{session}": {exception}', [
667  'session' => $info,
668  'exception' => $ex,
669  ] );
670  return $failHandler();
671  }
672  $newParams['userInfo'] = $userInfo;
673  } else {
674  // User validation passes if user ID matches, or if there
675  // is no saved ID and the names match.
676  if ( $metadata['userId'] ) {
677  if ( $metadata['userId'] !== $userInfo->getId() ) {
678  $this->logger->warning(
679  'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
680  [
681  'session' => $info,
682  'uid_a' => $metadata['userId'],
683  'uid_b' => $userInfo->getId(),
684  ] );
685  return $failHandler();
686  }
687 
688  // If the user was renamed, probably best to fail here.
689  if ( $metadata['userName'] !== null &&
690  $userInfo->getName() !== $metadata['userName']
691  ) {
692  $this->logger->warning(
693  'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
694  [
695  'session' => $info,
696  'uname_a' => $metadata['userName'],
697  'uname_b' => $userInfo->getName(),
698  ] );
699  return $failHandler();
700  }
701 
702  } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
703  if ( $metadata['userName'] !== $userInfo->getName() ) {
704  $this->logger->warning(
705  'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
706  [
707  'session' => $info,
708  'uname_a' => $metadata['userName'],
709  'uname_b' => $userInfo->getName(),
710  ] );
711  return $failHandler();
712  }
713  } elseif ( !$userInfo->isAnon() ) {
714  // Metadata specifies an anonymous user, but the passed-in
715  // user isn't anonymous.
716  $this->logger->warning(
717  'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
718  [
719  'session' => $info,
720  ] );
721  return $failHandler();
722  }
723  }
724 
725  // And if we have a token in the metadata, it must match the loaded/provided user.
726  if ( $metadata['userToken'] !== null &&
727  $userInfo->getToken() !== $metadata['userToken']
728  ) {
729  $this->logger->warning( 'Session "{session}": User token mismatch', [
730  'session' => $info,
731  ] );
732  return $failHandler();
733  }
734  if ( !$userInfo->isVerified() ) {
735  $newParams['userInfo'] = $userInfo->verified();
736  }
737 
738  if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
739  $newParams['remembered'] = true;
740  }
741  if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
742  $newParams['forceHTTPS'] = true;
743  }
744  if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) {
745  $newParams['persisted'] = true;
746  }
747 
748  if ( !$info->isIdSafe() ) {
749  $newParams['idIsSafe'] = true;
750  }
751  } else {
752  // No metadata, so we can't load the provider if one wasn't given.
753  if ( $info->getProvider() === null ) {
754  $this->logger->warning(
755  'Session "{session}": Null provider and no metadata',
756  [
757  'session' => $info,
758  ] );
759  return $failHandler();
760  }
761 
762  // If no user was provided and no metadata, it must be anon.
763  if ( !$info->getUserInfo() ) {
764  if ( $info->getProvider()->canChangeUser() ) {
765  $newParams['userInfo'] = UserInfo::newAnonymous();
766  } else {
767  $this->logger->info(
768  'Session "{session}": No user provided and provider cannot set user',
769  [
770  'session' => $info,
771  ] );
772  return $failHandler();
773  }
774  } elseif ( !$info->getUserInfo()->isVerified() ) {
775  $this->logger->warning(
776  'Session "{session}": Unverified user provided and no metadata to auth it',
777  [
778  'session' => $info,
779  ] );
780  return $failHandler();
781  }
782 
783  $data = false;
784  $metadata = false;
785 
786  if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
787  // The ID doesn't come from the user, so it should be safe
788  // (and if not, nothing we can do about it anyway)
789  $newParams['idIsSafe'] = true;
790  }
791  }
792 
793  // Construct the replacement SessionInfo, if necessary
794  if ( $newParams ) {
795  $newParams['copyFrom'] = $info;
796  $info = new SessionInfo( $info->getPriority(), $newParams );
797  }
798 
799  // Allow the provider to check the loaded SessionInfo
800  $providerMetadata = $info->getProviderMetadata();
801  if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
802  return $failHandler();
803  }
804  if ( $providerMetadata !== $info->getProviderMetadata() ) {
805  $info = new SessionInfo( $info->getPriority(), [
806  'metadata' => $providerMetadata,
807  'copyFrom' => $info,
808  ] );
809  }
810 
811  // Give hooks a chance to abort. Combined with the SessionMetadata
812  // hook, this can allow for tying a session to an IP address or the
813  // like.
814  $reason = 'Hook aborted';
815  if ( !\Hooks::run(
816  'SessionCheckInfo',
817  [ &$reason, $info, $request, $metadata, $data ]
818  ) ) {
819  $this->logger->warning( 'Session "{session}": ' . $reason, [
820  'session' => $info,
821  ] );
822  return $failHandler();
823  }
824 
825  return true;
826  }
827 
836  public function getSessionFromInfo( SessionInfo $info, WebRequest $request ) {
837  // @codeCoverageIgnoreStart
838  if ( defined( 'MW_NO_SESSION' ) ) {
839  if ( MW_NO_SESSION === 'warn' ) {
840  // Undocumented safety case for converting existing entry points
841  $this->logger->error( 'Sessions are supposed to be disabled for this entry point', [
842  'exception' => new \BadMethodCallException( 'Sessions are disabled for this entry point' ),
843  ] );
844  } else {
845  throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
846  }
847  }
848  // @codeCoverageIgnoreEnd
849 
850  $id = $info->getId();
851 
852  if ( !isset( $this->allSessionBackends[$id] ) ) {
853  if ( !isset( $this->allSessionIds[$id] ) ) {
854  $this->allSessionIds[$id] = new SessionId( $id );
855  }
856  $backend = new SessionBackend(
857  $this->allSessionIds[$id],
858  $info,
859  $this->store,
860  $this->logger,
861  $this->config->get( 'ObjectCacheSessionExpiry' )
862  );
863  $this->allSessionBackends[$id] = $backend;
864  $delay = $backend->delaySave();
865  } else {
866  $backend = $this->allSessionBackends[$id];
867  $delay = $backend->delaySave();
868  if ( $info->wasPersisted() ) {
869  $backend->persist();
870  }
871  if ( $info->wasRemembered() ) {
872  $backend->setRememberUser( true );
873  }
874  }
875 
876  $request->setSessionId( $backend->getSessionId() );
877  $session = $backend->getSession( $request );
878 
879  if ( !$info->isIdSafe() ) {
880  $session->resetId();
881  }
882 
883  \Wikimedia\ScopedCallback::consume( $delay );
884  return $session;
885  }
886 
892  public function deregisterSessionBackend( SessionBackend $backend ) {
893  $id = $backend->getId();
894  if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
895  $this->allSessionBackends[$id] !== $backend ||
896  $this->allSessionIds[$id] !== $backend->getSessionId()
897  ) {
898  throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
899  }
900 
901  unset( $this->allSessionBackends[$id] );
902  // Explicitly do not unset $this->allSessionIds[$id]
903  }
904 
910  public function changeBackendId( SessionBackend $backend ) {
911  $sessionId = $backend->getSessionId();
912  $oldId = (string)$sessionId;
913  if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
914  $this->allSessionBackends[$oldId] !== $backend ||
915  $this->allSessionIds[$oldId] !== $sessionId
916  ) {
917  throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
918  }
919 
920  $newId = $this->generateSessionId();
921 
922  unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
923  $sessionId->setId( $newId );
924  $this->allSessionBackends[$newId] = $backend;
925  $this->allSessionIds[$newId] = $sessionId;
926  }
927 
932  public function generateSessionId() {
933  do {
934  $id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
935  $key = wfMemcKey( 'MWSession', $id );
936  } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
937  return $id;
938  }
939 
946  $handler->setManager( $this, $this->store, $this->logger );
947  }
948 
953  public static function resetCache() {
954  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
955  // @codeCoverageIgnoreStart
956  throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
957  // @codeCoverageIgnoreEnd
958  }
959 
960  self::$globalSession = null;
961  self::$globalSessionRequest = null;
962  }
963 
966 }
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.
static getObjectFromSpec($spec)
Instantiate an object based on a specification array.
saveSettings()
Save this user's settings into the database.
Definition: User.php:3847
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
changeBackendId(SessionBackend $backend)
Change a SessionBackend's ID.
getEmptySessionInternal(WebRequest $request=null, $id=null)
getPriority()
Return the priority.
static getInstance($id)
Get a cached instance of the specified type of cache object.
Definition: ObjectCache.php:92
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:177
static SessionManager null $instance
static newFromId($id, $verified=false)
Create an instance for a logged-in user by ID.
Definition: UserInfo.php:84
static autoCreateUser(User $user)
Auto-create the given user, if necessary.
shutdown()
Save all active sessions on shutdown.
A helper class for throttling authentication attempts.
getId()
Return the session ID.
setManager(SessionManager $manager, BagOStuff $store, LoggerInterface $logger)
Set the manager, store, and logger.
setToken($token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition: User.php:2609
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.
MediaWiki s SiteStore can be cached and stored in a flat in a json format If the SiteStore is frequently the file cache may provide a performance benefit over a database store
Definition: sitescache.txt:1
const MW_NO_SESSION
Definition: load.php:30
static newAnonymous()
Create an instance for an anonymous (i.e.
Definition: UserInfo.php:74
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()
Static methods.
getSessionInfoForRequest(WebRequest $request)
Fetch the SessionInfo(s) for a request.
getProvider($name)
Get a session provider by name.
deregisterSessionBackend(SessionBackend $backend)
Deregister a SessionBackend.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1046
getProvider()
Return the provider.
isUserSessionPrevented($username)
Test if a user is prevented.
setLogger(LoggerInterface $logger)
wfDeprecated($function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
static WebRequest null $globalSessionRequest
static callLegacyAuthPlugin($method, array $params, $return=null)
Call a legacy AuthPlugin method, if necessary.
getId()
Returns the session ID.
static run($event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:131
getProviderMetadata()
Return provider metadata.
wasPersisted()
Return whether the session is persisted.
$header
Adapter for PHP'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 compare($a, $b)
Compare two SessionInfo objects by priority.
static singleton()
Get the global SessionManager.
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition: hooks.txt:242
setSessionId(SessionId $sessionId)
Set the session for this request.
Definition: WebRequest.php:746
generateSessionId()
Generate a new random session ID.
static getDefaultInstance()
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:802
error also a ContextSource you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2573
preventSessionsForUser($username)
Prevent future sessions for the user.
static validateSessionId($id)
Validate a session ID.
Value object holding the session ID in a manner that can be globally updated.
Definition: SessionId.php:38
static generateHex($chars, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in hexadecimal string format...
Definition: MWCryptRand.php:76
getVaryHeaders()
Return the HTTP headers that need varying on.
static Session null $globalSession
wfMemcKey()
Make a cache key for the local wiki.
This serves as the entry point to the MediaWiki session handling system.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:802
This exists to make IDEs happy, so they don't see the internal-but-required-to-be-public methods on S...
getSessionById($id, $create=false, WebRequest $request=null)
Fetch a session by ID.
static newFromName($name, $verified=false)
Create an instance for a logged-in user by name.
Definition: UserInfo.php:102
forceHTTPS()
Whether this session should only be used over HTTPS.
Value object returned by SessionProvider.
Definition: SessionInfo.php:34
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".
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:300