RequestContext.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @since 1.18
  19. *
  20. * @author Alexandre Emsenhuber
  21. * @author Daniel Friesen
  22. * @file
  23. */
  24. use MediaWiki\Logger\LoggerFactory;
  25. use MediaWiki\MediaWikiServices;
  26. use Wikimedia\ScopedCallback;
  27. /**
  28. * Group all the pieces relevant to the context of a request into one instance
  29. */
  30. class RequestContext implements IContextSource, MutableContext {
  31. /**
  32. * @var WebRequest
  33. */
  34. private $request;
  35. /**
  36. * @var Title
  37. */
  38. private $title;
  39. /**
  40. * @var WikiPage
  41. */
  42. private $wikipage;
  43. /**
  44. * @var OutputPage
  45. */
  46. private $output;
  47. /**
  48. * @var User
  49. */
  50. private $user;
  51. /**
  52. * @var Language
  53. */
  54. private $lang;
  55. /**
  56. * @var Skin
  57. */
  58. private $skin;
  59. /**
  60. * @var Timing
  61. */
  62. private $timing;
  63. /**
  64. * @var Config
  65. */
  66. private $config;
  67. /**
  68. * @var RequestContext
  69. */
  70. private static $instance = null;
  71. /**
  72. * @param Config $config
  73. */
  74. public function setConfig( Config $config ) {
  75. $this->config = $config;
  76. }
  77. /**
  78. * @return Config
  79. */
  80. public function getConfig() {
  81. if ( $this->config === null ) {
  82. // @todo In the future, we could move this to WebStart.php so
  83. // the Config object is ready for when initialization happens
  84. $this->config = MediaWikiServices::getInstance()->getMainConfig();
  85. }
  86. return $this->config;
  87. }
  88. /**
  89. * @param WebRequest $request
  90. */
  91. public function setRequest( WebRequest $request ) {
  92. $this->request = $request;
  93. }
  94. /**
  95. * @return WebRequest
  96. */
  97. public function getRequest() {
  98. if ( $this->request === null ) {
  99. global $wgCommandLineMode;
  100. // create the WebRequest object on the fly
  101. if ( $wgCommandLineMode ) {
  102. $this->request = new FauxRequest( [] );
  103. } else {
  104. $this->request = new WebRequest();
  105. }
  106. }
  107. return $this->request;
  108. }
  109. /**
  110. * @deprecated since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)
  111. *
  112. * @return IBufferingStatsdDataFactory
  113. */
  114. public function getStats() {
  115. return MediaWikiServices::getInstance()->getStatsdDataFactory();
  116. }
  117. /**
  118. * @return Timing
  119. */
  120. public function getTiming() {
  121. if ( $this->timing === null ) {
  122. $this->timing = new Timing( [
  123. 'logger' => LoggerFactory::getInstance( 'Timing' )
  124. ] );
  125. }
  126. return $this->timing;
  127. }
  128. /**
  129. * @param Title|null $title
  130. */
  131. public function setTitle( Title $title = null ) {
  132. $this->title = $title;
  133. // Erase the WikiPage so a new one with the new title gets created.
  134. $this->wikipage = null;
  135. }
  136. /**
  137. * @return Title|null
  138. */
  139. public function getTitle() {
  140. if ( $this->title === null ) {
  141. global $wgTitle; # fallback to $wg till we can improve this
  142. $this->title = $wgTitle;
  143. wfDebugLog(
  144. 'GlobalTitleFail',
  145. __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.'
  146. );
  147. }
  148. return $this->title;
  149. }
  150. /**
  151. * Check, if a Title object is set
  152. *
  153. * @since 1.25
  154. * @return bool
  155. */
  156. public function hasTitle() {
  157. return $this->title !== null;
  158. }
  159. /**
  160. * Check whether a WikiPage object can be get with getWikiPage().
  161. * Callers should expect that an exception is thrown from getWikiPage()
  162. * if this method returns false.
  163. *
  164. * @since 1.19
  165. * @return bool
  166. */
  167. public function canUseWikiPage() {
  168. if ( $this->wikipage ) {
  169. // If there's a WikiPage object set, we can for sure get it
  170. return true;
  171. }
  172. // Only pages with legitimate titles can have WikiPages.
  173. // That usually means pages in non-virtual namespaces.
  174. $title = $this->getTitle();
  175. return $title ? $title->canExist() : false;
  176. }
  177. /**
  178. * @since 1.19
  179. * @param WikiPage $wikiPage
  180. */
  181. public function setWikiPage( WikiPage $wikiPage ) {
  182. $pageTitle = $wikiPage->getTitle();
  183. if ( !$this->hasTitle() || !$pageTitle->equals( $this->getTitle() ) ) {
  184. $this->setTitle( $pageTitle );
  185. }
  186. // Defer this to the end since setTitle sets it to null.
  187. $this->wikipage = $wikiPage;
  188. }
  189. /**
  190. * Get the WikiPage object.
  191. * May throw an exception if there's no Title object set or the Title object
  192. * belongs to a special namespace that doesn't have WikiPage, so use first
  193. * canUseWikiPage() to check whether this method can be called safely.
  194. *
  195. * @since 1.19
  196. * @throws MWException
  197. * @return WikiPage
  198. */
  199. public function getWikiPage() {
  200. if ( $this->wikipage === null ) {
  201. $title = $this->getTitle();
  202. if ( $title === null ) {
  203. throw new MWException( __METHOD__ . ' called without Title object set' );
  204. }
  205. $this->wikipage = WikiPage::factory( $title );
  206. }
  207. return $this->wikipage;
  208. }
  209. /**
  210. * @param OutputPage $output
  211. */
  212. public function setOutput( OutputPage $output ) {
  213. $this->output = $output;
  214. }
  215. /**
  216. * @return OutputPage
  217. */
  218. public function getOutput() {
  219. if ( $this->output === null ) {
  220. $this->output = new OutputPage( $this );
  221. }
  222. return $this->output;
  223. }
  224. /**
  225. * @param User $user
  226. */
  227. public function setUser( User $user ) {
  228. $this->user = $user;
  229. }
  230. /**
  231. * @return User
  232. */
  233. public function getUser() {
  234. if ( $this->user === null ) {
  235. $this->user = User::newFromSession( $this->getRequest() );
  236. }
  237. return $this->user;
  238. }
  239. /**
  240. * Accepts a language code and ensures it's sane. Outputs a cleaned up language
  241. * code and replaces with $wgLanguageCode if not sane.
  242. * @param string $code Language code
  243. * @return string
  244. */
  245. public static function sanitizeLangCode( $code ) {
  246. global $wgLanguageCode;
  247. // BCP 47 - letter case MUST NOT carry meaning
  248. $code = strtolower( $code );
  249. # Validate $code
  250. if ( !$code || !Language::isValidCode( $code ) || $code === 'qqq' ) {
  251. $code = $wgLanguageCode;
  252. }
  253. return $code;
  254. }
  255. /**
  256. * @param Language|string $language Language instance or language code
  257. * @throws MWException
  258. * @since 1.19
  259. */
  260. public function setLanguage( $language ) {
  261. if ( $language instanceof Language ) {
  262. $this->lang = $language;
  263. } elseif ( is_string( $language ) ) {
  264. $language = self::sanitizeLangCode( $language );
  265. $obj = Language::factory( $language );
  266. $this->lang = $obj;
  267. } else {
  268. throw new MWException( __METHOD__ . " was passed an invalid type of data." );
  269. }
  270. }
  271. /**
  272. * Get the Language object.
  273. * Initialization of user or request objects can depend on this.
  274. * @return Language
  275. * @throws Exception
  276. * @since 1.19
  277. */
  278. public function getLanguage() {
  279. if ( isset( $this->recursion ) ) {
  280. trigger_error( "Recursion detected in " . __METHOD__, E_USER_WARNING );
  281. $e = new Exception;
  282. wfDebugLog( 'recursion-guard', "Recursion detected:\n" . $e->getTraceAsString() );
  283. $code = $this->getConfig()->get( 'LanguageCode' ) ?: 'en';
  284. $this->lang = Language::factory( $code );
  285. } elseif ( $this->lang === null ) {
  286. $this->recursion = true;
  287. try {
  288. $request = $this->getRequest();
  289. $user = $this->getUser();
  290. $code = $request->getVal( 'uselang', 'user' );
  291. if ( $code === 'user' ) {
  292. $code = $user->getOption( 'language' );
  293. }
  294. $code = self::sanitizeLangCode( $code );
  295. Hooks::run( 'UserGetLanguageObject', [ $user, &$code, $this ] );
  296. if ( $code === $this->getConfig()->get( 'LanguageCode' ) ) {
  297. $this->lang = MediaWikiServices::getInstance()->getContentLanguage();
  298. } else {
  299. $obj = Language::factory( $code );
  300. $this->lang = $obj;
  301. }
  302. unset( $this->recursion );
  303. }
  304. catch ( Exception $ex ) {
  305. unset( $this->recursion );
  306. throw $ex;
  307. }
  308. }
  309. return $this->lang;
  310. }
  311. /**
  312. * @param Skin $skin
  313. */
  314. public function setSkin( Skin $skin ) {
  315. $this->skin = clone $skin;
  316. $this->skin->setContext( $this );
  317. }
  318. /**
  319. * @return Skin
  320. */
  321. public function getSkin() {
  322. if ( $this->skin === null ) {
  323. $skin = null;
  324. Hooks::run( 'RequestContextCreateSkin', [ $this, &$skin ] );
  325. $factory = SkinFactory::getDefaultInstance();
  326. // If the hook worked try to set a skin from it
  327. if ( $skin instanceof Skin ) {
  328. $this->skin = $skin;
  329. } elseif ( is_string( $skin ) ) {
  330. // Normalize the key, just in case the hook did something weird.
  331. $normalized = Skin::normalizeKey( $skin );
  332. $this->skin = $factory->makeSkin( $normalized );
  333. }
  334. // If this is still null (the hook didn't run or didn't work)
  335. // then go through the normal processing to load a skin
  336. if ( $this->skin === null ) {
  337. if ( !in_array( 'skin', $this->getConfig()->get( 'HiddenPrefs' ) ) ) {
  338. # get the user skin
  339. $userSkin = $this->getUser()->getOption( 'skin' );
  340. $userSkin = $this->getRequest()->getVal( 'useskin', $userSkin );
  341. } else {
  342. # if we're not allowing users to override, then use the default
  343. $userSkin = $this->getConfig()->get( 'DefaultSkin' );
  344. }
  345. // Normalize the key in case the user is passing gibberish
  346. // or has old preferences (T71566).
  347. $normalized = Skin::normalizeKey( $userSkin );
  348. // Skin::normalizeKey will also validate it, so
  349. // this won't throw an exception
  350. $this->skin = $factory->makeSkin( $normalized );
  351. }
  352. // After all that set a context on whatever skin got created
  353. $this->skin->setContext( $this );
  354. }
  355. return $this->skin;
  356. }
  357. /**
  358. * Get a Message object with context set
  359. * Parameters are the same as wfMessage()
  360. *
  361. * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
  362. * or a MessageSpecifier.
  363. * @param mixed $args,...
  364. * @return Message
  365. */
  366. public function msg( $key ) {
  367. $args = func_get_args();
  368. return wfMessage( ...$args )->setContext( $this );
  369. }
  370. /**
  371. * Get the RequestContext object associated with the main request
  372. *
  373. * @return RequestContext
  374. */
  375. public static function getMain() {
  376. if ( self::$instance === null ) {
  377. self::$instance = new self;
  378. }
  379. return self::$instance;
  380. }
  381. /**
  382. * Get the RequestContext object associated with the main request
  383. * and gives a warning to the log, to find places, where a context maybe is missing.
  384. *
  385. * @param string $func
  386. * @return RequestContext
  387. * @since 1.24
  388. */
  389. public static function getMainAndWarn( $func = __METHOD__ ) {
  390. wfDebug( $func . ' called without context. ' .
  391. "Using RequestContext::getMain() for sanity\n" );
  392. return self::getMain();
  393. }
  394. /**
  395. * Resets singleton returned by getMain(). Should be called only from unit tests.
  396. */
  397. public static function resetMain() {
  398. if ( !( defined( 'MW_PHPUNIT_TEST' ) || defined( 'MW_PARSER_TEST' ) ) ) {
  399. throw new MWException( __METHOD__ . '() should be called only from unit tests!' );
  400. }
  401. self::$instance = null;
  402. }
  403. /**
  404. * Export the resolved user IP, HTTP headers, user ID, and session ID.
  405. * The result will be reasonably sized to allow for serialization.
  406. *
  407. * @return array
  408. * @since 1.21
  409. */
  410. public function exportSession() {
  411. $session = MediaWiki\Session\SessionManager::getGlobalSession();
  412. return [
  413. 'ip' => $this->getRequest()->getIP(),
  414. 'headers' => $this->getRequest()->getAllHeaders(),
  415. 'sessionId' => $session->isPersistent() ? $session->getId() : '',
  416. 'userId' => $this->getUser()->getId()
  417. ];
  418. }
  419. /**
  420. * Import an client IP address, HTTP headers, user ID, and session ID
  421. *
  422. * This sets the current session, $wgUser, and $wgRequest from $params.
  423. * Once the return value falls out of scope, the old context is restored.
  424. * This method should only be called in contexts where there is no session
  425. * ID or end user receiving the response (CLI or HTTP job runners). This
  426. * is partly enforced, and is done so to avoid leaking cookies if certain
  427. * error conditions arise.
  428. *
  429. * This is useful when background scripts inherit context when acting on
  430. * behalf of a user. In general the 'sessionId' parameter should be set
  431. * to an empty string unless session importing is *truly* needed. This
  432. * feature is somewhat deprecated.
  433. *
  434. * @note suhosin.session.encrypt may interfere with this method.
  435. *
  436. * @param array $params Result of RequestContext::exportSession()
  437. * @return ScopedCallback
  438. * @throws MWException
  439. * @since 1.21
  440. */
  441. public static function importScopedSession( array $params ) {
  442. if ( strlen( $params['sessionId'] ) &&
  443. MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent()
  444. ) {
  445. // Sanity check to avoid sending random cookies for the wrong users.
  446. // This method should only called by CLI scripts or by HTTP job runners.
  447. throw new MWException( "Sessions can only be imported when none is active." );
  448. } elseif ( !IP::isValid( $params['ip'] ) ) {
  449. throw new MWException( "Invalid client IP address '{$params['ip']}'." );
  450. }
  451. if ( $params['userId'] ) { // logged-in user
  452. $user = User::newFromId( $params['userId'] );
  453. $user->load();
  454. if ( !$user->getId() ) {
  455. throw new MWException( "No user with ID '{$params['userId']}'." );
  456. }
  457. } else { // anon user
  458. $user = User::newFromName( $params['ip'], false );
  459. }
  460. $importSessionFunc = function ( User $user, array $params ) {
  461. global $wgRequest, $wgUser;
  462. $context = RequestContext::getMain();
  463. // Commit and close any current session
  464. if ( MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
  465. session_write_close(); // persist
  466. session_id( '' ); // detach
  467. $_SESSION = []; // clear in-memory array
  468. }
  469. // Get new session, if applicable
  470. $session = null;
  471. if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
  472. $manager = MediaWiki\Session\SessionManager::singleton();
  473. $session = $manager->getSessionById( $params['sessionId'], true )
  474. ?: $manager->getEmptySession();
  475. }
  476. // Remove any user IP or agent information, and attach the request
  477. // with the new session.
  478. $context->setRequest( new FauxRequest( [], false, $session ) );
  479. $wgRequest = $context->getRequest(); // b/c
  480. // Now that all private information is detached from the user, it should
  481. // be safe to load the new user. If errors occur or an exception is thrown
  482. // and caught (leaving the main context in a mixed state), there is no risk
  483. // of the User object being attached to the wrong IP, headers, or session.
  484. $context->setUser( $user );
  485. $wgUser = $context->getUser(); // b/c
  486. if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
  487. session_id( $session->getId() );
  488. Wikimedia\quietCall( 'session_start' );
  489. }
  490. $request = new FauxRequest( [], false, $session );
  491. $request->setIP( $params['ip'] );
  492. foreach ( $params['headers'] as $name => $value ) {
  493. $request->setHeader( $name, $value );
  494. }
  495. // Set the current context to use the new WebRequest
  496. $context->setRequest( $request );
  497. $wgRequest = $context->getRequest(); // b/c
  498. };
  499. // Stash the old session and load in the new one
  500. $oUser = self::getMain()->getUser();
  501. $oParams = self::getMain()->exportSession();
  502. $oRequest = self::getMain()->getRequest();
  503. $importSessionFunc( $user, $params );
  504. // Set callback to save and close the new session and reload the old one
  505. return new ScopedCallback(
  506. function () use ( $importSessionFunc, $oUser, $oParams, $oRequest ) {
  507. global $wgRequest;
  508. $importSessionFunc( $oUser, $oParams );
  509. // Restore the exact previous Request object (instead of leaving FauxRequest)
  510. RequestContext::getMain()->setRequest( $oRequest );
  511. $wgRequest = RequestContext::getMain()->getRequest(); // b/c
  512. }
  513. );
  514. }
  515. /**
  516. * Create a new extraneous context. The context is filled with information
  517. * external to the current session.
  518. * - Title is specified by argument
  519. * - Request is a FauxRequest, or a FauxRequest can be specified by argument
  520. * - User is an anonymous user, for separation IPv4 localhost is used
  521. * - Language will be based on the anonymous user and request, may be content
  522. * language or a uselang param in the fauxrequest data may change the lang
  523. * - Skin will be based on the anonymous user, should be the wiki's default skin
  524. *
  525. * @param Title $title Title to use for the extraneous request
  526. * @param WebRequest|array $request A WebRequest or data to use for a FauxRequest
  527. * @return RequestContext
  528. */
  529. public static function newExtraneousContext( Title $title, $request = [] ) {
  530. $context = new self;
  531. $context->setTitle( $title );
  532. if ( $request instanceof WebRequest ) {
  533. $context->setRequest( $request );
  534. } else {
  535. $context->setRequest( new FauxRequest( $request ) );
  536. }
  537. $context->user = User::newFromName( '127.0.0.1', false );
  538. return $context;
  539. }
  540. }