MediaWiki  master
SessionManager.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Session;
25 
27 use Psr\Log\LoggerInterface;
30 use Config;
32 use User;
34 
41 final class SessionManager implements SessionManagerInterface {
43  private static $instance = null;
44 
46  private static $globalSession = null;
47 
49  private static $globalSessionRequest = null;
50 
52  private $logger;
53 
55  private $config;
56 
58  private $store;
59 
61  private $sessionProviders = null;
62 
64  private $varyCookies = null;
65 
67  private $varyHeaders = null;
68 
70  private $allSessionBackends = [];
71 
73  private $allSessionIds = [];
74 
76  private $preventUsers = [];
77 
83  public static function singleton() {
84  if ( self::$instance === null ) {
85  self::$instance = new self();
86  }
87  return self::$instance;
88  }
89 
98  public static function getGlobalSession() {
100  $id = '';
101  } else {
102  $id = session_id();
103  }
104 
105  $request = \RequestContext::getMain()->getRequest();
106  if (
107  !self::$globalSession // No global session is set up yet
108  || self::$globalSessionRequest !== $request // The global WebRequest changed
109  || $id !== '' && self::$globalSession->getId() !== $id // Someone messed with session_id()
110  ) {
111  self::$globalSessionRequest = $request;
112  if ( $id === '' ) {
113  // session_id() wasn't used, so fetch the Session from the WebRequest.
114  // We use $request->getSession() instead of $singleton->getSessionForRequest()
115  // because doing the latter would require a public
116  // "$request->getSessionId()" method that would confuse end
117  // users by returning SessionId|null where they'd expect it to
118  // be short for $request->getSession()->getId(), and would
119  // wind up being a duplicate of the code in
120  // $request->getSession() anyway.
121  self::$globalSession = $request->getSession();
122  } else {
123  // Someone used session_id(), so we need to follow suit.
124  // Note this overwrites whatever session might already be
125  // associated with $request with the one for $id.
126  self::$globalSession = self::singleton()->getSessionById( $id, true, $request )
127  ?: $request->getSession();
128  }
129  }
130  return self::$globalSession;
131  }
132 
139  public function __construct( $options = [] ) {
140  if ( isset( $options['config'] ) ) {
141  $this->config = $options['config'];
142  if ( !$this->config instanceof Config ) {
143  throw new \InvalidArgumentException(
144  '$options[\'config\'] must be an instance of Config'
145  );
146  }
147  } else {
148  $this->config = \ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
149  }
150 
151  if ( isset( $options['logger'] ) ) {
152  if ( !$options['logger'] instanceof LoggerInterface ) {
153  throw new \InvalidArgumentException(
154  '$options[\'logger\'] must be an instance of LoggerInterface'
155  );
156  }
157  $this->setLogger( $options['logger'] );
158  } else {
159  $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'session' ) );
160  }
161 
162  if ( isset( $options['store'] ) ) {
163  if ( !$options['store'] instanceof BagOStuff ) {
164  throw new \InvalidArgumentException(
165  '$options[\'store\'] must be an instance of BagOStuff'
166  );
167  }
168  $store = $options['store'];
169  } else {
170  $store = \ObjectCache::getInstance( $this->config->get( 'SessionCacheType' ) );
171  }
172  $this->store = $store instanceof CachedBagOStuff ? $store : new CachedBagOStuff( $store );
173 
174  register_shutdown_function( [ $this, 'shutdown' ] );
175  }
176 
177  public function setLogger( LoggerInterface $logger ) {
178  $this->logger = $logger;
179  }
180 
182  $info = $this->getSessionInfoForRequest( $request );
183 
184  if ( !$info ) {
185  $session = $this->getEmptySession( $request );
186  } else {
187  $session = $this->getSessionFromInfo( $info, $request );
188  }
189  return $session;
190  }
191 
192  public function getSessionById( $id, $create = false, WebRequest $request = null ) {
193  if ( !self::validateSessionId( $id ) ) {
194  throw new \InvalidArgumentException( 'Invalid session ID' );
195  }
196  if ( !$request ) {
197  $request = new FauxRequest;
198  }
199 
200  $session = null;
201  $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [ 'id' => $id, 'idIsSafe' => true ] );
202 
203  // If we already have the backend loaded, use it directly
204  if ( isset( $this->allSessionBackends[$id] ) ) {
205  return $this->getSessionFromInfo( $info, $request );
206  }
207 
208  // Test if the session is in storage, and if so try to load it.
209  $key = wfMemcKey( 'MWSession', $id );
210  if ( is_array( $this->store->get( $key ) ) ) {
211  $create = false; // If loading fails, don't bother creating because it probably will fail too.
212  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
213  $session = $this->getSessionFromInfo( $info, $request );
214  }
215  }
216 
217  if ( $create && $session === null ) {
218  $ex = null;
219  try {
220  $session = $this->getEmptySessionInternal( $request, $id );
221  } catch ( \Exception $ex ) {
222  $this->logger->error( 'Failed to create empty session: {exception}',
223  [
224  'method' => __METHOD__,
225  'exception' => $ex,
226  ] );
227  $session = null;
228  }
229  }
230 
231  return $session;
232  }
233 
234  public function getEmptySession( WebRequest $request = null ) {
235  return $this->getEmptySessionInternal( $request );
236  }
237 
244  private function getEmptySessionInternal( WebRequest $request = null, $id = null ) {
245  if ( $id !== null ) {
246  if ( !self::validateSessionId( $id ) ) {
247  throw new \InvalidArgumentException( 'Invalid session ID' );
248  }
249 
250  $key = wfMemcKey( 'MWSession', $id );
251  if ( is_array( $this->store->get( $key ) ) ) {
252  throw new \InvalidArgumentException( 'Session ID already exists' );
253  }
254  }
255  if ( !$request ) {
256  $request = new FauxRequest;
257  }
258 
259  $infos = [];
260  foreach ( $this->getProviders() as $provider ) {
261  $info = $provider->newSessionInfo( $id );
262  if ( !$info ) {
263  continue;
264  }
265  if ( $info->getProvider() !== $provider ) {
266  throw new \UnexpectedValueException(
267  "$provider returned an empty session info for a different provider: $info"
268  );
269  }
270  if ( $id !== null && $info->getId() !== $id ) {
271  throw new \UnexpectedValueException(
272  "$provider returned empty session info with a wrong id: " .
273  $info->getId() . ' != ' . $id
274  );
275  }
276  if ( !$info->isIdSafe() ) {
277  throw new \UnexpectedValueException(
278  "$provider returned empty session info with id flagged unsafe"
279  );
280  }
281  $compare = $infos ? SessionInfo::compare( $infos[0], $info ) : -1;
282  if ( $compare > 0 ) {
283  continue;
284  }
285  if ( $compare === 0 ) {
286  $infos[] = $info;
287  } else {
288  $infos = [ $info ];
289  }
290  }
291 
292  // Make sure there's exactly one
293  if ( count( $infos ) > 1 ) {
294  throw new \UnexpectedValueException(
295  'Multiple empty sessions tied for top priority: ' . implode( ', ', $infos )
296  );
297  } elseif ( count( $infos ) < 1 ) {
298  throw new \UnexpectedValueException( 'No provider could provide an empty session!' );
299  }
300 
301  return $this->getSessionFromInfo( $infos[0], $request );
302  }
303 
304  public function invalidateSessionsForUser( User $user ) {
305  $user->setToken();
306  $user->saveSettings();
307 
308  $authUser = \MediaWiki\Auth\AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ] );
309  if ( $authUser ) {
310  $authUser->resetAuthToken();
311  }
312 
313  foreach ( $this->getProviders() as $provider ) {
314  $provider->invalidateSessionsForUser( $user );
315  }
316  }
317 
318  public function getVaryHeaders() {
319  // @codeCoverageIgnoreStart
320  if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
321  return [];
322  }
323  // @codeCoverageIgnoreEnd
324  if ( $this->varyHeaders === null ) {
325  $headers = [];
326  foreach ( $this->getProviders() as $provider ) {
327  foreach ( $provider->getVaryHeaders() as $header => $options ) {
328  if ( !isset( $headers[$header] ) ) {
329  $headers[$header] = [];
330  }
331  if ( is_array( $options ) ) {
332  $headers[$header] = array_unique( array_merge( $headers[$header], $options ) );
333  }
334  }
335  }
336  $this->varyHeaders = $headers;
337  }
338  return $this->varyHeaders;
339  }
340 
341  public function getVaryCookies() {
342  // @codeCoverageIgnoreStart
343  if ( defined( 'MW_NO_SESSION' ) && MW_NO_SESSION !== 'warn' ) {
344  return [];
345  }
346  // @codeCoverageIgnoreEnd
347  if ( $this->varyCookies === null ) {
348  $cookies = [];
349  foreach ( $this->getProviders() as $provider ) {
350  $cookies = array_merge( $cookies, $provider->getVaryCookies() );
351  }
352  $this->varyCookies = array_values( array_unique( $cookies ) );
353  }
354  return $this->varyCookies;
355  }
356 
362  public static function validateSessionId( $id ) {
363  return is_string( $id ) && preg_match( '/^[a-zA-Z0-9_-]{32,}$/', $id );
364  }
365 
378  public static function autoCreateUser( User $user ) {
380 
381  // @codeCoverageIgnoreStart
382  if ( !$wgDisableAuthManager ) {
383  wfDeprecated( __METHOD__, '1.27' );
384  return \MediaWiki\Auth\AuthManager::singleton()->autoCreateUser(
385  $user,
386  \MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION,
387  false
388  )->isGood();
389  }
390  // @codeCoverageIgnoreEnd
391 
392  $logger = self::singleton()->logger;
393 
394  // Much of this code is based on that in CentralAuth
395 
396  // Try the local user from the slave DB
397  $localId = User::idFromName( $user->getName() );
398  $flags = 0;
399 
400  // Fetch the user ID from the master, so that we don't try to create the user
401  // when they already exist, due to replication lag
402  // @codeCoverageIgnoreStart
403  if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
404  $localId = User::idFromName( $user->getName(), User::READ_LATEST );
405  $flags = User::READ_LATEST;
406  }
407  // @codeCoverageIgnoreEnd
408 
409  if ( $localId ) {
410  // User exists after all.
411  $user->setId( $localId );
412  $user->loadFromId( $flags );
413  return false;
414  }
415 
416  // Denied by AuthPlugin? But ignore AuthPlugin itself.
417  if ( get_class( $wgAuth ) !== 'AuthPlugin' && !$wgAuth->autoCreate() ) {
418  $logger->debug( __METHOD__ . ': denied by AuthPlugin' );
419  $user->setId( 0 );
420  $user->loadFromId();
421  return false;
422  }
423 
424  // Wiki is read-only?
425  if ( wfReadOnly() ) {
426  $logger->debug( __METHOD__ . ': denied by wfReadOnly()' );
427  $user->setId( 0 );
428  $user->loadFromId();
429  return false;
430  }
431 
432  $userName = $user->getName();
433 
434  // Check the session, if we tried to create this user already there's
435  // no point in retrying.
436  $session = self::getGlobalSession();
437  $reason = $session->get( 'MWSession::AutoCreateBlacklist' );
438  if ( $reason ) {
439  $logger->debug( __METHOD__ . ": blacklisted in session ($reason)" );
440  $user->setId( 0 );
441  $user->loadFromId();
442  return false;
443  }
444 
445  // Is the IP user able to create accounts?
446  $anon = new User;
447  if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
448  || $anon->isBlockedFromCreateAccount()
449  ) {
450  // Blacklist the user to avoid repeated DB queries subsequently
451  $logger->debug( __METHOD__ . ': user is blocked from this wiki, blacklisting' );
452  $session->set( 'MWSession::AutoCreateBlacklist', 'blocked', 600 );
453  $session->persist();
454  $user->setId( 0 );
455  $user->loadFromId();
456  return false;
457  }
458 
459  // Check for validity of username
460  if ( !User::isCreatableName( $userName ) ) {
461  $logger->debug( __METHOD__ . ': Invalid username, blacklisting' );
462  $session->set( 'MWSession::AutoCreateBlacklist', 'invalid username', 600 );
463  $session->persist();
464  $user->setId( 0 );
465  $user->loadFromId();
466  return false;
467  }
468 
469  // Give other extensions a chance to stop auto creation.
470  $user->loadDefaults( $userName );
471  $abortMessage = '';
472  if ( !\Hooks::run( 'AbortAutoAccount', [ $user, &$abortMessage ] ) ) {
473  // In this case we have no way to return the message to the user,
474  // but we can log it.
475  $logger->debug( __METHOD__ . ": denied by hook: $abortMessage" );
476  $session->set( 'MWSession::AutoCreateBlacklist', "hook aborted: $abortMessage", 600 );
477  $session->persist();
478  $user->setId( 0 );
479  $user->loadFromId();
480  return false;
481  }
482 
483  // Make sure the name has not been changed
484  if ( $user->getName() !== $userName ) {
485  $user->setId( 0 );
486  $user->loadFromId();
487  throw new \UnexpectedValueException(
488  'AbortAutoAccount hook tried to change the user name'
489  );
490  }
491 
492  // Ignore warnings about master connections/writes...hard to avoid here
493  \Profiler::instance()->getTransactionProfiler()->resetExpectations();
494 
496  $backoffKey = wfMemcKey( 'MWSession', 'autocreate-failed', md5( $userName ) );
497  if ( $cache->get( $backoffKey ) ) {
498  $logger->debug( __METHOD__ . ': denied by prior creation attempt failures' );
499  $user->setId( 0 );
500  $user->loadFromId();
501  return false;
502  }
503 
504  // Checks passed, create the user...
505  $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
506  $logger->info( __METHOD__ . ': creating new user ({username}) - from: {url}',
507  [
508  'username' => $userName,
509  'url' => $from,
510  ] );
511 
512  try {
513  // Insert the user into the local DB master
514  $status = $user->addToDatabase();
515  if ( !$status->isOK() ) {
516  // @codeCoverageIgnoreStart
517  // double-check for a race condition (T70012)
518  $id = User::idFromName( $user->getName(), User::READ_LATEST );
519  if ( $id ) {
520  $logger->info( __METHOD__ . ': tried to autocreate existing user',
521  [
522  'username' => $userName,
523  ] );
524  } else {
525  $logger->error(
526  __METHOD__ . ': failed with message ' . $status->getWikiText( false, false, 'en' ),
527  [
528  'username' => $userName,
529  ]
530  );
531  }
532  $user->setId( $id );
533  $user->loadFromId( User::READ_LATEST );
534  return false;
535  // @codeCoverageIgnoreEnd
536  }
537  } catch ( \Exception $ex ) {
538  // @codeCoverageIgnoreStart
539  $logger->error( __METHOD__ . ': failed with exception {exception}', [
540  'exception' => $ex,
541  'username' => $userName,
542  ] );
543  // Do not keep throwing errors for a while
544  $cache->set( $backoffKey, 1, 600 );
545  // Bubble up error; which should normally trigger DB rollbacks
546  throw $ex;
547  // @codeCoverageIgnoreEnd
548  }
549 
550  # Notify AuthPlugin
551  // @codeCoverageIgnoreStart
552  $tmpUser = $user;
553  $wgAuth->initUser( $tmpUser, true );
554  if ( $tmpUser !== $user ) {
555  $logger->warning( __METHOD__ . ': ' .
556  get_class( $wgAuth ) . '::initUser() replaced the user object' );
557  }
558  // @codeCoverageIgnoreEnd
559 
560  # Notify hooks (e.g. Newuserlog)
561  \Hooks::run( 'AuthPluginAutoCreate', [ $user ] );
562  \Hooks::run( 'LocalUserCreated', [ $user, true ] );
563 
564  $user->saveSettings();
565 
566  # Update user count
567  \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
568 
569  # Watch user's userpage and talk page
570  $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
571 
572  return true;
573  }
574 
584  public function preventSessionsForUser( $username ) {
585  $this->preventUsers[$username] = true;
586 
587  // Instruct the session providers to kill any other sessions too.
588  foreach ( $this->getProviders() as $provider ) {
589  $provider->preventSessionsForUser( $username );
590  }
591  }
592 
599  public function isUserSessionPrevented( $username ) {
600  return !empty( $this->preventUsers[$username] );
601  }
602 
607  protected function getProviders() {
608  if ( $this->sessionProviders === null ) {
609  $this->sessionProviders = [];
610  foreach ( $this->config->get( 'SessionProviders' ) as $spec ) {
611  $provider = \ObjectFactory::getObjectFromSpec( $spec );
612  $provider->setLogger( $this->logger );
613  $provider->setConfig( $this->config );
614  $provider->setManager( $this );
615  if ( isset( $this->sessionProviders[(string)$provider] ) ) {
616  throw new \UnexpectedValueException( "Duplicate provider name \"$provider\"" );
617  }
618  $this->sessionProviders[(string)$provider] = $provider;
619  }
620  }
622  }
623 
634  public function getProvider( $name ) {
635  $providers = $this->getProviders();
636  return isset( $providers[$name] ) ? $providers[$name] : null;
637  }
638 
643  public function shutdown() {
644  if ( $this->allSessionBackends ) {
645  $this->logger->debug( 'Saving all sessions on shutdown' );
646  if ( session_id() !== '' ) {
647  // @codeCoverageIgnoreStart
648  session_write_close();
649  }
650  // @codeCoverageIgnoreEnd
651  foreach ( $this->allSessionBackends as $backend ) {
652  $backend->shutdown();
653  }
654  }
655  }
656 
663  // Call all providers to fetch "the" session
664  $infos = [];
665  foreach ( $this->getProviders() as $provider ) {
666  $info = $provider->provideSessionInfo( $request );
667  if ( !$info ) {
668  continue;
669  }
670  if ( $info->getProvider() !== $provider ) {
671  throw new \UnexpectedValueException(
672  "$provider returned session info for a different provider: $info"
673  );
674  }
675  $infos[] = $info;
676  }
677 
678  // Sort the SessionInfos. Then find the first one that can be
679  // successfully loaded, and then all the ones after it with the same
680  // priority.
681  usort( $infos, 'MediaWiki\\Session\\SessionInfo::compare' );
682  $retInfos = [];
683  while ( $infos ) {
684  $info = array_pop( $infos );
685  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
686  $retInfos[] = $info;
687  while ( $infos ) {
688  $info = array_pop( $infos );
689  if ( SessionInfo::compare( $retInfos[0], $info ) ) {
690  // We hit a lower priority, stop checking.
691  break;
692  }
693  if ( $this->loadSessionInfoFromStore( $info, $request ) ) {
694  // This is going to error out below, but we want to
695  // provide a complete list.
696  $retInfos[] = $info;
697  } else {
698  // Session load failed, so unpersist it from this request
699  $info->getProvider()->unpersistSession( $request );
700  }
701  }
702  } else {
703  // Session load failed, so unpersist it from this request
704  $info->getProvider()->unpersistSession( $request );
705  }
706  }
707 
708  if ( count( $retInfos ) > 1 ) {
709  $ex = new \OverflowException(
710  'Multiple sessions for this request tied for top priority: ' . implode( ', ', $retInfos )
711  );
712  $ex->sessionInfos = $retInfos;
713  throw $ex;
714  }
715 
716  return $retInfos ? $retInfos[0] : null;
717  }
718 
727  $key = wfMemcKey( 'MWSession', $info->getId() );
728  $blob = $this->store->get( $key );
729 
730  // If we got data from the store and the SessionInfo says to force use,
731  // "fail" means to delete the data from the store and retry. Otherwise,
732  // "fail" is just return false.
733  if ( $info->forceUse() && $blob !== false ) {
734  $failHandler = function () use ( $key, &$info, $request ) {
735  $this->store->delete( $key );
736  return $this->loadSessionInfoFromStore( $info, $request );
737  };
738  } else {
739  $failHandler = function () {
740  return false;
741  };
742  }
743 
744  $newParams = [];
745 
746  if ( $blob !== false ) {
747  // Sanity check: blob must be an array, if it's saved at all
748  if ( !is_array( $blob ) ) {
749  $this->logger->warning( 'Session "{session}": Bad data', [
750  'session' => $info,
751  ] );
752  $this->store->delete( $key );
753  return $failHandler();
754  }
755 
756  // Sanity check: blob has data and metadata arrays
757  if ( !isset( $blob['data'] ) || !is_array( $blob['data'] ) ||
758  !isset( $blob['metadata'] ) || !is_array( $blob['metadata'] )
759  ) {
760  $this->logger->warning( 'Session "{session}": Bad data structure', [
761  'session' => $info,
762  ] );
763  $this->store->delete( $key );
764  return $failHandler();
765  }
766 
767  $data = $blob['data'];
768  $metadata = $blob['metadata'];
769 
770  // Sanity check: metadata must be an array and must contain certain
771  // keys, if it's saved at all
772  if ( !array_key_exists( 'userId', $metadata ) ||
773  !array_key_exists( 'userName', $metadata ) ||
774  !array_key_exists( 'userToken', $metadata ) ||
775  !array_key_exists( 'provider', $metadata )
776  ) {
777  $this->logger->warning( 'Session "{session}": Bad metadata', [
778  'session' => $info,
779  ] );
780  $this->store->delete( $key );
781  return $failHandler();
782  }
783 
784  // First, load the provider from metadata, or validate it against the metadata.
785  $provider = $info->getProvider();
786  if ( $provider === null ) {
787  $newParams['provider'] = $provider = $this->getProvider( $metadata['provider'] );
788  if ( !$provider ) {
789  $this->logger->warning(
790  'Session "{session}": Unknown provider ' . $metadata['provider'],
791  [
792  'session' => $info,
793  ]
794  );
795  $this->store->delete( $key );
796  return $failHandler();
797  }
798  } elseif ( $metadata['provider'] !== (string)$provider ) {
799  $this->logger->warning( 'Session "{session}": Wrong provider ' .
800  $metadata['provider'] . ' !== ' . $provider,
801  [
802  'session' => $info,
803  ] );
804  return $failHandler();
805  }
806 
807  // Load provider metadata from metadata, or validate it against the metadata
808  $providerMetadata = $info->getProviderMetadata();
809  if ( isset( $metadata['providerMetadata'] ) ) {
810  if ( $providerMetadata === null ) {
811  $newParams['metadata'] = $metadata['providerMetadata'];
812  } else {
813  try {
814  $newProviderMetadata = $provider->mergeMetadata(
815  $metadata['providerMetadata'], $providerMetadata
816  );
817  if ( $newProviderMetadata !== $providerMetadata ) {
818  $newParams['metadata'] = $newProviderMetadata;
819  }
820  } catch ( MetadataMergeException $ex ) {
821  $this->logger->warning(
822  'Session "{session}": Metadata merge failed: {exception}',
823  [
824  'session' => $info,
825  'exception' => $ex,
826  ] + $ex->getContext()
827  );
828  return $failHandler();
829  }
830  }
831  }
832 
833  // Next, load the user from metadata, or validate it against the metadata.
834  $userInfo = $info->getUserInfo();
835  if ( !$userInfo ) {
836  // For loading, id is preferred to name.
837  try {
838  if ( $metadata['userId'] ) {
839  $userInfo = UserInfo::newFromId( $metadata['userId'] );
840  } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
841  $userInfo = UserInfo::newFromName( $metadata['userName'] );
842  } else {
843  $userInfo = UserInfo::newAnonymous();
844  }
845  } catch ( \InvalidArgumentException $ex ) {
846  $this->logger->error( 'Session "{session}": {exception}', [
847  'session' => $info,
848  'exception' => $ex,
849  ] );
850  return $failHandler();
851  }
852  $newParams['userInfo'] = $userInfo;
853  } else {
854  // User validation passes if user ID matches, or if there
855  // is no saved ID and the names match.
856  if ( $metadata['userId'] ) {
857  if ( $metadata['userId'] !== $userInfo->getId() ) {
858  $this->logger->warning(
859  'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}',
860  [
861  'session' => $info,
862  'uid_a' => $metadata['userId'],
863  'uid_b' => $userInfo->getId(),
864  ] );
865  return $failHandler();
866  }
867 
868  // If the user was renamed, probably best to fail here.
869  if ( $metadata['userName'] !== null &&
870  $userInfo->getName() !== $metadata['userName']
871  ) {
872  $this->logger->warning(
873  'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}',
874  [
875  'session' => $info,
876  'uname_a' => $metadata['userName'],
877  'uname_b' => $userInfo->getName(),
878  ] );
879  return $failHandler();
880  }
881 
882  } elseif ( $metadata['userName'] !== null ) { // Shouldn't happen, but just in case
883  if ( $metadata['userName'] !== $userInfo->getName() ) {
884  $this->logger->warning(
885  'Session "{session}": User name mismatch, {uname_a} !== {uname_b}',
886  [
887  'session' => $info,
888  'uname_a' => $metadata['userName'],
889  'uname_b' => $userInfo->getName(),
890  ] );
891  return $failHandler();
892  }
893  } elseif ( !$userInfo->isAnon() ) {
894  // Metadata specifies an anonymous user, but the passed-in
895  // user isn't anonymous.
896  $this->logger->warning(
897  'Session "{session}": Metadata has an anonymous user, but a non-anon user was provided',
898  [
899  'session' => $info,
900  ] );
901  return $failHandler();
902  }
903  }
904 
905  // And if we have a token in the metadata, it must match the loaded/provided user.
906  if ( $metadata['userToken'] !== null &&
907  $userInfo->getToken() !== $metadata['userToken']
908  ) {
909  $this->logger->warning( 'Session "{session}": User token mismatch', [
910  'session' => $info,
911  ] );
912  return $failHandler();
913  }
914  if ( !$userInfo->isVerified() ) {
915  $newParams['userInfo'] = $userInfo->verified();
916  }
917 
918  if ( !empty( $metadata['remember'] ) && !$info->wasRemembered() ) {
919  $newParams['remembered'] = true;
920  }
921  if ( !empty( $metadata['forceHTTPS'] ) && !$info->forceHTTPS() ) {
922  $newParams['forceHTTPS'] = true;
923  }
924  if ( !empty( $metadata['persisted'] ) && !$info->wasPersisted() ) {
925  $newParams['persisted'] = true;
926  }
927 
928  if ( !$info->isIdSafe() ) {
929  $newParams['idIsSafe'] = true;
930  }
931  } else {
932  // No metadata, so we can't load the provider if one wasn't given.
933  if ( $info->getProvider() === null ) {
934  $this->logger->warning(
935  'Session "{session}": Null provider and no metadata',
936  [
937  'session' => $info,
938  ] );
939  return $failHandler();
940  }
941 
942  // If no user was provided and no metadata, it must be anon.
943  if ( !$info->getUserInfo() ) {
944  if ( $info->getProvider()->canChangeUser() ) {
945  $newParams['userInfo'] = UserInfo::newAnonymous();
946  } else {
947  $this->logger->info(
948  'Session "{session}": No user provided and provider cannot set user',
949  [
950  'session' => $info,
951  ] );
952  return $failHandler();
953  }
954  } elseif ( !$info->getUserInfo()->isVerified() ) {
955  $this->logger->warning(
956  'Session "{session}": Unverified user provided and no metadata to auth it',
957  [
958  'session' => $info,
959  ] );
960  return $failHandler();
961  }
962 
963  $data = false;
964  $metadata = false;
965 
966  if ( !$info->getProvider()->persistsSessionId() && !$info->isIdSafe() ) {
967  // The ID doesn't come from the user, so it should be safe
968  // (and if not, nothing we can do about it anyway)
969  $newParams['idIsSafe'] = true;
970  }
971  }
972 
973  // Construct the replacement SessionInfo, if necessary
974  if ( $newParams ) {
975  $newParams['copyFrom'] = $info;
976  $info = new SessionInfo( $info->getPriority(), $newParams );
977  }
978 
979  // Allow the provider to check the loaded SessionInfo
980  $providerMetadata = $info->getProviderMetadata();
981  if ( !$info->getProvider()->refreshSessionInfo( $info, $request, $providerMetadata ) ) {
982  return $failHandler();
983  }
984  if ( $providerMetadata !== $info->getProviderMetadata() ) {
985  $info = new SessionInfo( $info->getPriority(), [
986  'metadata' => $providerMetadata,
987  'copyFrom' => $info,
988  ] );
989  }
990 
991  // Give hooks a chance to abort. Combined with the SessionMetadata
992  // hook, this can allow for tying a session to an IP address or the
993  // like.
994  $reason = 'Hook aborted';
995  if ( !\Hooks::run(
996  'SessionCheckInfo',
997  [ &$reason, $info, $request, $metadata, $data ]
998  ) ) {
999  $this->logger->warning( 'Session "{session}": ' . $reason, [
1000  'session' => $info,
1001  ] );
1002  return $failHandler();
1003  }
1004 
1005  return true;
1006  }
1007 
1017  // @codeCoverageIgnoreStart
1018  if ( defined( 'MW_NO_SESSION' ) ) {
1019  if ( MW_NO_SESSION === 'warn' ) {
1020  // Undocumented safety case for converting existing entry points
1021  $this->logger->error( 'Sessions are supposed to be disabled for this entry point', [
1022  'exception' => new \BadMethodCallException( 'Sessions are disabled for this entry point' ),
1023  ] );
1024  } else {
1025  throw new \BadMethodCallException( 'Sessions are disabled for this entry point' );
1026  }
1027  }
1028  // @codeCoverageIgnoreEnd
1029 
1030  $id = $info->getId();
1031 
1032  if ( !isset( $this->allSessionBackends[$id] ) ) {
1033  if ( !isset( $this->allSessionIds[$id] ) ) {
1034  $this->allSessionIds[$id] = new SessionId( $id );
1035  }
1036  $backend = new SessionBackend(
1037  $this->allSessionIds[$id],
1038  $info,
1039  $this->store,
1040  $this->logger,
1041  $this->config->get( 'ObjectCacheSessionExpiry' )
1042  );
1043  $this->allSessionBackends[$id] = $backend;
1044  $delay = $backend->delaySave();
1045  } else {
1046  $backend = $this->allSessionBackends[$id];
1047  $delay = $backend->delaySave();
1048  if ( $info->wasPersisted() ) {
1049  $backend->persist();
1050  }
1051  if ( $info->wasRemembered() ) {
1052  $backend->setRememberUser( true );
1053  }
1054  }
1055 
1056  $request->setSessionId( $backend->getSessionId() );
1057  $session = $backend->getSession( $request );
1058 
1059  if ( !$info->isIdSafe() ) {
1060  $session->resetId();
1061  }
1062 
1063  \ScopedCallback::consume( $delay );
1064  return $session;
1065  }
1066 
1072  public function deregisterSessionBackend( SessionBackend $backend ) {
1073  $id = $backend->getId();
1074  if ( !isset( $this->allSessionBackends[$id] ) || !isset( $this->allSessionIds[$id] ) ||
1075  $this->allSessionBackends[$id] !== $backend ||
1076  $this->allSessionIds[$id] !== $backend->getSessionId()
1077  ) {
1078  throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
1079  }
1080 
1081  unset( $this->allSessionBackends[$id] );
1082  // Explicitly do not unset $this->allSessionIds[$id]
1083  }
1084 
1090  public function changeBackendId( SessionBackend $backend ) {
1091  $sessionId = $backend->getSessionId();
1092  $oldId = (string)$sessionId;
1093  if ( !isset( $this->allSessionBackends[$oldId] ) || !isset( $this->allSessionIds[$oldId] ) ||
1094  $this->allSessionBackends[$oldId] !== $backend ||
1095  $this->allSessionIds[$oldId] !== $sessionId
1096  ) {
1097  throw new \InvalidArgumentException( 'Backend was not registered with this SessionManager' );
1098  }
1099 
1100  $newId = $this->generateSessionId();
1101 
1102  unset( $this->allSessionBackends[$oldId], $this->allSessionIds[$oldId] );
1103  $sessionId->setId( $newId );
1104  $this->allSessionBackends[$newId] = $backend;
1105  $this->allSessionIds[$newId] = $sessionId;
1106  }
1107 
1112  public function generateSessionId() {
1113  do {
1114  $id = \Wikimedia\base_convert( \MWCryptRand::generateHex( 40 ), 16, 32, 32 );
1115  $key = wfMemcKey( 'MWSession', $id );
1116  } while ( isset( $this->allSessionIds[$id] ) || is_array( $this->store->get( $key ) ) );
1117  return $id;
1118  }
1119 
1126  $handler->setManager( $this, $this->store, $this->logger );
1127  }
1128 
1132  public static function resetCache() {
1133  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
1134  // @codeCoverageIgnoreStart
1135  throw new MWException( __METHOD__ . ' may only be called from unit tests!' );
1136  // @codeCoverageIgnoreEnd
1137  }
1138 
1139  self::$globalSession = null;
1140  self::$globalSessionRequest = null;
1141  }
1142 
1145 }
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:3942
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
static instance()
Singleton.
Definition: Profiler.php:60
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:37
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:93
getEmptySession(WebRequest $request=null)
Fetch 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 getLocalClusterInstance()
Get the main cluster-local cache object.
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2588
The MediaWiki class is the helper class for the index.php entry point.
Definition: MediaWiki.php:28
$wgAuth $wgAuth
Authentication plugin.
static autoCreateUser(User $user)
Auto-create the given user, if necessary.
shutdown()
Save all active sessions on shutdown For internal use with register_shutdown_function() ...
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
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:2707
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.
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2139
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:29
static newAnonymous()
Create an instance for an anonymous (i.e.
Definition: UserInfo.php:74
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:47
loadDefaults($name=false)
Set cached properties to default.
Definition: User.php:1166
interface is intended to be more or less compatible with the PHP memcached client.
Definition: BagOStuff.php:45
wfGetLB($wiki=false)
Get a load balancer object.
static resetCache()
Reset the internal caching for unit testing.
wfReadOnly()
Check whether the wiki is in read-only mode.
getSessionForRequest(WebRequest $request)
Fetch the session for a request.
static getMain()
Static methods.
Interface for configuration instances.
Definition: Config.php:28
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:1020
getProvider()
Return the provider.
Class for handling updates to the site_stats table.
isUserSessionPrevented($username)
Test if a user is prevented For use from SessionBackend only.
MediaWiki exception.
Definition: MWException.php:26
$cache
Definition: mcc.php:33
const IGNORE_USER_RIGHTS
Definition: User.php:84
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 addUpdate(DeferrableUpdate $update, $type=self::POSTSEND)
Add an update to the deferred list.
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.
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:710
generateSessionId()
Generate a new random session ID.
static getDefaultInstance()
setId($v)
Set the user and reload all fields according to a given ID.
Definition: User.php:2130
static isEnabled()
Test whether the handler is installed and enabled.
static getGlobalSession()
Get the "global" session.
$from
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:776
error also a ContextSource you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2458
preventSessionsForUser($username)
Prevent future sessions for the user.
addToDatabase()
Add this existing user object to the database.
Definition: User.php:4110
static validateSessionId($id)
Validate a session ID.
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:33
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...
$wgDisableAuthManager
Disable AuthManager.
static isCreatableName($name)
Usernames which fail to pass this function will be blocked from new account registrations, but may be used internally either by batch processes or by user accounts which have already been created.
Definition: User.php:964
getUserPage()
Get this user's personal page title.
Definition: User.php:4255
static idFromName($name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:764
Wrapper around a BagOStuff that caches data in memory.
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 $status
Definition: hooks.txt:1020
getVaryHeaders()
Return the HTTP headers that need varying on.
static consume(ScopedCallback &$sc=null)
Trigger a scoped callback and destroy it.
static Session null $globalSession
wfMemcKey()
Make a cache key for the local wiki.
loadFromId($flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition: User.php:420
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:776
addWatch($title, $checkRights=self::CHECK_USER_RIGHTS)
Watch an article.
Definition: User.php:3654
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:310