MediaWiki  master
RequestContext.php
Go to the documentation of this file.
1 <?php
28 use Wikimedia\AtEase\AtEase;
29 use Wikimedia\IPUtils;
30 use Wikimedia\NonSerializable\NonSerializableTrait;
31 use Wikimedia\ScopedCallback;
32 
41  use NonSerializableTrait;
42 
46  private $request;
47 
51  private $title;
52 
56  private $wikipage;
57 
61  private $output;
62 
66  private $user;
67 
71  private $authority;
72 
76  private $lang;
77 
81  private $skin;
82 
86  private $timing;
87 
91  private $config;
92 
96  private static $instance = null;
97 
102  private $languageRecursion = false;
103 
107  public function setConfig( Config $config ) {
108  $this->config = $config;
109  }
110 
114  public function getConfig() {
115  if ( $this->config === null ) {
116  // @todo In the future, we could move this to WebStart.php so
117  // the Config object is ready for when initialization happens
118  $this->config = MediaWikiServices::getInstance()->getMainConfig();
119  }
120 
121  return $this->config;
122  }
123 
127  public function setRequest( WebRequest $request ) {
128  $this->request = $request;
129  }
130 
134  public function getRequest() {
135  if ( $this->request === null ) {
136  global $wgCommandLineMode;
137  // create the WebRequest object on the fly
138  if ( $wgCommandLineMode ) {
139  $this->request = new FauxRequest( [] );
140  } else {
141  $this->request = new WebRequest();
142  }
143  }
144 
145  return $this->request;
146  }
147 
153  public function getStats() {
154  return MediaWikiServices::getInstance()->getStatsdDataFactory();
155  }
156 
160  public function getTiming() {
161  if ( $this->timing === null ) {
162  $this->timing = new Timing( [
163  'logger' => LoggerFactory::getInstance( 'Timing' )
164  ] );
165  }
166  return $this->timing;
167  }
168 
172  public function setTitle( Title $title = null ) {
173  $this->title = $title;
174  // Erase the WikiPage so a new one with the new title gets created.
175  $this->wikipage = null;
176  }
177 
181  public function getTitle() {
182  if ( $this->title === null ) {
183  global $wgTitle; # fallback to $wg till we can improve this
184  $this->title = $wgTitle;
185  $logger = LoggerFactory::getInstance( 'GlobalTitleFail' );
186  $logger->info(
187  __METHOD__ . ' called with no title set.',
188  [ 'exception' => new Exception ]
189  );
190  }
191 
192  return $this->title;
193  }
194 
201  public function hasTitle() {
202  return $this->title !== null;
203  }
204 
213  public function canUseWikiPage() {
214  if ( $this->wikipage ) {
215  // If there's a WikiPage object set, we can for sure get it
216  return true;
217  }
218  // Only pages with legitimate titles can have WikiPages.
219  // That usually means pages in non-virtual namespaces.
220  $title = $this->getTitle();
221  return $title ? $title->canExist() : false;
222  }
223 
228  public function setWikiPage( WikiPage $wikiPage ) {
229  $pageTitle = $wikiPage->getTitle();
230  if ( !$this->hasTitle() || !$pageTitle->equals( $this->getTitle() ) ) {
231  $this->setTitle( $pageTitle );
232  }
233  // Defer this to the end since setTitle sets it to null.
234  $this->wikipage = $wikiPage;
235  }
236 
247  public function getWikiPage() {
248  if ( $this->wikipage === null ) {
249  $title = $this->getTitle();
250  if ( $title === null ) {
251  throw new MWException( __METHOD__ . ' called without Title object set' );
252  }
253  $this->wikipage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
254  }
255 
256  return $this->wikipage;
257  }
258 
262  public function setOutput( OutputPage $output ) {
263  $this->output = $output;
264  }
265 
269  public function getOutput() {
270  if ( $this->output === null ) {
271  $this->output = new OutputPage( $this );
272  }
273 
274  return $this->output;
275  }
276 
280  public function setUser( User $user ) {
281  $this->user = $user;
282  // Keep authority consistent
283  $this->authority = $user;
284  // Invalidate cached user interface language
285  $this->lang = null;
286  }
287 
291  public function getUser() {
292  if ( $this->user === null ) {
293  $this->user = User::newFromSession( $this->getRequest() );
294  }
295 
296  return $this->user;
297  }
298 
299  public function setAuthority( Authority $authority ) {
300  $this->authority = $authority;
301  // Keep user consistent
302  $this->user = MediaWikiServices::getInstance()
303  ->getUserFactory()
304  ->newFromAuthority( $authority );
305  // Invalidate cached user interface language
306  $this->lang = null;
307  }
308 
309  public function getAuthority(): Authority {
310  return $this->authority ?: $this->getUser();
311  }
312 
319  public static function sanitizeLangCode( $code ) {
320  global $wgLanguageCode;
321 
322  // BCP 47 - letter case MUST NOT carry meaning
323  $code = strtolower( $code );
324 
325  # Validate $code
326  if ( !$code
327  || !MediaWikiServices::getInstance()->getLanguageNameUtils()
328  ->isValidCode( $code )
329  || $code === 'qqq'
330  ) {
331  $code = $wgLanguageCode;
332  }
333 
334  return $code;
335  }
336 
342  public function setLanguage( $language ) {
343  if ( $language instanceof Language ) {
344  $this->lang = $language;
345  } elseif ( is_string( $language ) ) {
346  $language = self::sanitizeLangCode( $language );
347  $obj = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $language );
348  $this->lang = $obj;
349  } else {
350  throw new MWException( __METHOD__ . " was passed an invalid type of data." );
351  }
352  }
353 
361  public function getLanguage() {
362  if ( $this->languageRecursion === true ) {
363  throw new LogicException( 'Recursion detected' );
364  }
365 
366  if ( $this->lang === null ) {
367  $this->languageRecursion = true;
368 
369  try {
370  $request = $this->getRequest();
371  $user = $this->getUser();
372 
373  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
374  $code = $request->getRawVal( 'uselang', 'user' );
375  if ( $code === 'user' ) {
376  $code = $user->getOption( 'language' );
377  }
378 
379  // There are certain characters we don't allow in language code strings,
380  // but by and large almost any valid UTF-8 string will makes it past
381  // this check and the LanguageNameUtils::isValidCode method it uses.
382  // This is to support on-wiki interface message overrides for
383  // non-existent language codes. Also known as "Uselang hacks".
384  // See <https://www.mediawiki.org/wiki/Manual:Uselang_hack>
385  // For something like "en-whatever" or "de-whatever" it will end up
386  // with a mostly "en" or "de" interface, but with an extra layer of
387  // possible MessageCache overrides from `MediaWiki:*/<code>` titles.
388  // While non-ASCII works here, it is required that they are in
389  // NFC form given this will not convert to normalised form.
390  $code = self::sanitizeLangCode( $code );
391 
392  Hooks::runner()->onUserGetLanguageObject( $user, $code, $this );
393 
394  if ( $code === $this->getConfig()->get( 'LanguageCode' ) ) {
395  $this->lang = MediaWikiServices::getInstance()->getContentLanguage();
396  } else {
397  $obj = MediaWikiServices::getInstance()->getLanguageFactory()
398  ->getLanguage( $code );
399  $this->lang = $obj;
400  }
401  } finally {
402  $this->languageRecursion = false;
403  }
404  }
405 
406  return $this->lang;
407  }
408 
412  public function setSkin( Skin $skin ) {
413  $this->skin = clone $skin;
414  $this->skin->setContext( $this );
415  }
416 
420  public function getSkin() {
421  if ( $this->skin === null ) {
422  $skin = null;
423  Hooks::runner()->onRequestContextCreateSkin( $this, $skin );
424  $factory = MediaWikiServices::getInstance()->getSkinFactory();
425 
426  if ( $skin instanceof Skin ) {
427  // The hook provided a skin object
428  $this->skin = $skin;
429  } elseif ( is_string( $skin ) ) {
430  // The hook provided a skin name
431  // Normalize the key, just in case the hook did something weird.
432  $normalized = Skin::normalizeKey( $skin );
433  $this->skin = $factory->makeSkin( $normalized );
434  } else {
435  // No hook override, go through normal processing
436  if ( !in_array( 'skin', $this->getConfig()->get( 'HiddenPrefs' ) ) ) {
437  $userSkin = $this->getUser()->getOption( 'skin' );
438  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
439  $userSkin = $this->getRequest()->getRawVal( 'useskin', $userSkin );
440  } else {
441  $userSkin = $this->getConfig()->get( 'DefaultSkin' );
442  }
443 
444  // Normalize the key in case the user is passing gibberish query params
445  // or has old user preferences (T71566).
446  // Skin::normalizeKey will also validate it, so makeSkin() won't throw.
447  $normalized = Skin::normalizeKey( $userSkin );
448  $this->skin = $factory->makeSkin( $normalized );
449  }
450 
451  // After all that set a context on whatever skin got created
452  $this->skin->setContext( $this );
453  }
454 
455  return $this->skin;
456  }
457 
467  public function msg( $key, ...$params ) {
468  return wfMessage( $key, ...$params )->setContext( $this );
469  }
470 
476  public static function getMain() : RequestContext {
477  if ( self::$instance === null ) {
478  self::$instance = new self;
479  }
480 
481  return self::$instance;
482  }
483 
492  public static function getMainAndWarn( $func = __METHOD__ ) {
493  wfDebug( $func . ' called without context. ' .
494  "Using RequestContext::getMain() for sanity" );
495 
496  return self::getMain();
497  }
498 
502  public static function resetMain() {
503  if ( !( defined( 'MW_PHPUNIT_TEST' ) || defined( 'MW_PARSER_TEST' ) ) ) {
504  throw new MWException( __METHOD__ . '() should be called only from unit tests!' );
505  }
506  self::$instance = null;
507  }
508 
516  public function exportSession() {
518  return [
519  'ip' => $this->getRequest()->getIP(),
520  'headers' => $this->getRequest()->getAllHeaders(),
521  'sessionId' => $session->isPersistent() ? $session->getId() : '',
522  'userId' => $this->getUser()->getId()
523  ];
524  }
525 
548  public static function importScopedSession( array $params ) {
549  if ( strlen( $params['sessionId'] ) &&
550  MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent()
551  ) {
552  // Sanity check to avoid sending random cookies for the wrong users.
553  // This method should only called by CLI scripts or by HTTP job runners.
554  throw new MWException( "Sessions can only be imported when none is active." );
555  } elseif ( !IPUtils::isValid( $params['ip'] ) ) {
556  throw new MWException( "Invalid client IP address '{$params['ip']}'." );
557  }
558 
559  if ( $params['userId'] ) { // logged-in user
560  $user = User::newFromId( $params['userId'] );
561  $user->load();
562  if ( !$user->getId() ) {
563  throw new MWException( "No user with ID '{$params['userId']}'." );
564  }
565  } else { // anon user
566  $user = User::newFromName( $params['ip'], false );
567  }
568 
569  $importSessionFunc = static function ( User $user, array $params ) {
570  global $wgRequest, $wgUser;
571 
572  $context = RequestContext::getMain();
573 
574  // Commit and close any current session
575  if ( MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
576  session_write_close(); // persist
577  session_id( '' ); // detach
578  $_SESSION = []; // clear in-memory array
579  }
580 
581  // Get new session, if applicable
582  $session = null;
583  if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
585  $session = $manager->getSessionById( $params['sessionId'], true )
586  ?: $manager->getEmptySession();
587  }
588 
589  // Remove any user IP or agent information, and attach the request
590  // with the new session.
591  $context->setRequest( new FauxRequest( [], false, $session ) );
592  $wgRequest = $context->getRequest(); // b/c
593 
594  // Now that all private information is detached from the user, it should
595  // be safe to load the new user. If errors occur or an exception is thrown
596  // and caught (leaving the main context in a mixed state), there is no risk
597  // of the User object being attached to the wrong IP, headers, or session.
598  $context->setUser( $user );
599  $wgUser = $context->getUser(); // b/c
600  if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
601  session_id( $session->getId() );
602  AtEase::quietCall( 'session_start' );
603  }
604  $request = new FauxRequest( [], false, $session );
605  $request->setIP( $params['ip'] );
606  foreach ( $params['headers'] as $name => $value ) {
607  $request->setHeader( $name, $value );
608  }
609  // Set the current context to use the new WebRequest
610  $context->setRequest( $request );
611  $wgRequest = $context->getRequest(); // b/c
612  };
613 
614  // Stash the old session and load in the new one
615  $oUser = self::getMain()->getUser();
616  $oParams = self::getMain()->exportSession();
617  $oRequest = self::getMain()->getRequest();
618  $importSessionFunc( $user, $params );
619 
620  // Set callback to save and close the new session and reload the old one
621  return new ScopedCallback(
622  static function () use ( $importSessionFunc, $oUser, $oParams, $oRequest ) {
623  global $wgRequest;
624  $importSessionFunc( $oUser, $oParams );
625  // Restore the exact previous Request object (instead of leaving FauxRequest)
626  RequestContext::getMain()->setRequest( $oRequest );
627  $wgRequest = RequestContext::getMain()->getRequest(); // b/c
628  }
629  );
630  }
631 
646  public static function newExtraneousContext( Title $title, $request = [] ) {
647  $context = new self;
648  $context->setTitle( $title );
649  if ( $request instanceof WebRequest ) {
650  $context->setRequest( $request );
651  } else {
652  $context->setRequest( new FauxRequest( $request ) );
653  }
654  $context->user = User::newFromName( '127.0.0.1', false );
655 
656  return $context;
657  }
658 }
User\load
load( $flags=self::READ_NORMAL)
Load the user table data for this object from the source given by mFrom.
Definition: User.php:358
RequestContext\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: RequestContext.php:467
FauxRequest
WebRequest clone which takes values from a provided array.
Definition: FauxRequest.php:35
User\newFromId
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition: User.php:627
RequestContext\getStats
getStats()
Definition: RequestContext.php:153
RequestContext\sanitizeLangCode
static sanitizeLangCode( $code)
Accepts a language code and ensures it's sane.
Definition: RequestContext.php:319
RequestContext\setRequest
setRequest(WebRequest $request)
Definition: RequestContext.php:127
RequestContext\$authority
Authority $authority
Definition: RequestContext.php:71
RequestContext\$title
Title $title
Definition: RequestContext.php:51
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:173
RequestContext\$user
User $user
Definition: RequestContext.php:66
RequestContext\setWikiPage
setWikiPage(WikiPage $wikiPage)
Definition: RequestContext.php:228
User\newFromSession
static newFromSession(WebRequest $request=null)
Create a new user object using data from session.
Definition: User.php:726
RequestContext\$instance
static RequestContext $instance
Definition: RequestContext.php:96
RequestContext\setSkin
setSkin(Skin $skin)
Definition: RequestContext.php:412
RequestContext\getRequest
getRequest()
Definition: RequestContext.php:134
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:61
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:586
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1231
RequestContext\newExtraneousContext
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
Definition: RequestContext.php:646
RequestContext\$config
Config $config
Definition: RequestContext.php:91
RequestContext\$skin
Skin $skin
Definition: RequestContext.php:81
RequestContext\getUser
getUser()
Definition: RequestContext.php:291
RequestContext\setUser
setUser(User $user)
Definition: RequestContext.php:280
Config
Interface for configuration instances.
Definition: Config.php:30
MWException
MediaWiki exception.
Definition: MWException.php:29
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
Skin\normalizeKey
static normalizeKey( $key)
Normalize a skin preference value to a form that can be loaded.
Definition: Skin.php:121
Timing
An interface to help developers measure the performance of their applications.
Definition: Timing.php:45
$wgCommandLineMode
global $wgCommandLineMode
Definition: DevelopmentSettings.php:29
RequestContext\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: RequestContext.php:247
RequestContext\getConfig
getConfig()
Definition: RequestContext.php:114
WikiPage\getTitle
getTitle()
Get the title object of the article.
Definition: WikiPage.php:317
MediaWiki
A helper class for throttling authentication attempts.
MediaWiki\Session\SessionManager\singleton
static singleton()
Get the global SessionManager.
Definition: SessionManager.php:100
RequestContext\$lang
Language $lang
Definition: RequestContext.php:76
RequestContext\getAuthority
getAuthority()
Definition: RequestContext.php:309
RequestContext\setLanguage
setLanguage( $language)
Definition: RequestContext.php:342
RequestContext
Group all the pieces relevant to the context of a request into one instance @newable.
Definition: RequestContext.php:40
Title\canExist
canExist()
Can this title represent a page in the wiki's database?
Definition: Title.php:1222
RequestContext\$wikipage
WikiPage $wikipage
Definition: RequestContext.php:56
User\getId
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition: User.php:2081
RequestContext\getTiming
getTiming()
Definition: RequestContext.php:160
RequestContext\getLanguage
getLanguage()
Get the Language object.
Definition: RequestContext.php:361
RequestContext\getMainAndWarn
static getMainAndWarn( $func=__METHOD__)
Get the RequestContext object associated with the main request and gives a warning to the log,...
Definition: RequestContext.php:492
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:915
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:62
OutputPage
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:50
WebRequest\getRawVal
getRawVal( $name, $default=null)
Fetch a scalar from the input without normalization, or return $default if it's not set.
Definition: WebRequest.php:480
$wgTitle
$wgTitle
Definition: Setup.php:800
RequestContext\getSkin
getSkin()
Definition: RequestContext.php:420
MediaWiki\Permissions\Authority
Definition: Authority.php:30
MutableContext
Request-dependent objects containers.
Definition: MutableContext.php:28
MediaWiki\Session\SessionManager\getGlobalSession
static getGlobalSession()
If PHP's session_id() has been set, returns that session.
Definition: SessionManager.php:113
RequestContext\resetMain
static resetMain()
Resets singleton returned by getMain().
Definition: RequestContext.php:502
RequestContext\$output
OutputPage $output
Definition: RequestContext.php:61
WebRequest\setIP
setIP( $ip)
Definition: WebRequest.php:1366
User\getOption
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition: User.php:2661
RequestContext\$request
WebRequest $request
Definition: RequestContext.php:46
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
$wgLanguageCode
$wgLanguageCode
Site language code.
Definition: DefaultSettings.php:3196
RequestContext\getTitle
getTitle()
Definition: RequestContext.php:181
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:476
RequestContext\importScopedSession
static importScopedSession(array $params)
Import an client IP address, HTTP headers, user ID, and session ID.
Definition: RequestContext.php:548
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:57
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:42
RequestContext\$timing
Timing $timing
Definition: RequestContext.php:86
Title
Represents a title within MediaWiki.
Definition: Title.php:46
RequestContext\$languageRecursion
bool $languageRecursion
Boolean flag to guard against recursion in getLanguage.
Definition: RequestContext.php:102
RequestContext\setOutput
setOutput(OutputPage $output)
Definition: RequestContext.php:262
RequestContext\canUseWikiPage
canUseWikiPage()
Check whether a WikiPage object can be get with getWikiPage().
Definition: RequestContext.php:213
$wgRequest
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:651
RequestContext\getOutput
getOutput()
Definition: RequestContext.php:269
Skin
The main skin class which provides methods and properties for all other skins.
Definition: Skin.php:42
RequestContext\setAuthority
setAuthority(Authority $authority)
Definition: RequestContext.php:299
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:66
RequestContext\setConfig
setConfig(Config $config)
Definition: RequestContext.php:107
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:43
RequestContext\exportSession
exportSession()
Export the resolved user IP, HTTP headers, user ID, and session ID.
Definition: RequestContext.php:516
RequestContext\hasTitle
hasTitle()
Check, if a Title object is set.
Definition: RequestContext.php:201
RequestContext\setTitle
setTitle(Title $title=null)
Definition: RequestContext.php:172