123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- <?php
- /**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @since 1.18
- *
- * @author Alexandre Emsenhuber
- * @author Daniel Friesen
- * @file
- */
- use MediaWiki\Logger\LoggerFactory;
- use MediaWiki\MediaWikiServices;
- use Wikimedia\ScopedCallback;
- /**
- * Group all the pieces relevant to the context of a request into one instance
- */
- class RequestContext implements IContextSource, MutableContext {
- /**
- * @var WebRequest
- */
- private $request;
- /**
- * @var Title
- */
- private $title;
- /**
- * @var WikiPage
- */
- private $wikipage;
- /**
- * @var OutputPage
- */
- private $output;
- /**
- * @var User
- */
- private $user;
- /**
- * @var Language
- */
- private $lang;
- /**
- * @var Skin
- */
- private $skin;
- /**
- * @var Timing
- */
- private $timing;
- /**
- * @var Config
- */
- private $config;
- /**
- * @var RequestContext
- */
- private static $instance = null;
- /**
- * @param Config $config
- */
- public function setConfig( Config $config ) {
- $this->config = $config;
- }
- /**
- * @return Config
- */
- public function getConfig() {
- if ( $this->config === null ) {
- // @todo In the future, we could move this to WebStart.php so
- // the Config object is ready for when initialization happens
- $this->config = MediaWikiServices::getInstance()->getMainConfig();
- }
- return $this->config;
- }
- /**
- * @param WebRequest $request
- */
- public function setRequest( WebRequest $request ) {
- $this->request = $request;
- }
- /**
- * @return WebRequest
- */
- public function getRequest() {
- if ( $this->request === null ) {
- global $wgCommandLineMode;
- // create the WebRequest object on the fly
- if ( $wgCommandLineMode ) {
- $this->request = new FauxRequest( [] );
- } else {
- $this->request = new WebRequest();
- }
- }
- return $this->request;
- }
- /**
- * @deprecated since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)
- *
- * @return IBufferingStatsdDataFactory
- */
- public function getStats() {
- return MediaWikiServices::getInstance()->getStatsdDataFactory();
- }
- /**
- * @return Timing
- */
- public function getTiming() {
- if ( $this->timing === null ) {
- $this->timing = new Timing( [
- 'logger' => LoggerFactory::getInstance( 'Timing' )
- ] );
- }
- return $this->timing;
- }
- /**
- * @param Title|null $title
- */
- public function setTitle( Title $title = null ) {
- $this->title = $title;
- // Erase the WikiPage so a new one with the new title gets created.
- $this->wikipage = null;
- }
- /**
- * @return Title|null
- */
- public function getTitle() {
- if ( $this->title === null ) {
- global $wgTitle; # fallback to $wg till we can improve this
- $this->title = $wgTitle;
- wfDebugLog(
- 'GlobalTitleFail',
- __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.'
- );
- }
- return $this->title;
- }
- /**
- * Check, if a Title object is set
- *
- * @since 1.25
- * @return bool
- */
- public function hasTitle() {
- return $this->title !== null;
- }
- /**
- * Check whether a WikiPage object can be get with getWikiPage().
- * Callers should expect that an exception is thrown from getWikiPage()
- * if this method returns false.
- *
- * @since 1.19
- * @return bool
- */
- public function canUseWikiPage() {
- if ( $this->wikipage ) {
- // If there's a WikiPage object set, we can for sure get it
- return true;
- }
- // Only pages with legitimate titles can have WikiPages.
- // That usually means pages in non-virtual namespaces.
- $title = $this->getTitle();
- return $title ? $title->canExist() : false;
- }
- /**
- * @since 1.19
- * @param WikiPage $wikiPage
- */
- public function setWikiPage( WikiPage $wikiPage ) {
- $pageTitle = $wikiPage->getTitle();
- if ( !$this->hasTitle() || !$pageTitle->equals( $this->getTitle() ) ) {
- $this->setTitle( $pageTitle );
- }
- // Defer this to the end since setTitle sets it to null.
- $this->wikipage = $wikiPage;
- }
- /**
- * Get the WikiPage object.
- * May throw an exception if there's no Title object set or the Title object
- * belongs to a special namespace that doesn't have WikiPage, so use first
- * canUseWikiPage() to check whether this method can be called safely.
- *
- * @since 1.19
- * @throws MWException
- * @return WikiPage
- */
- public function getWikiPage() {
- if ( $this->wikipage === null ) {
- $title = $this->getTitle();
- if ( $title === null ) {
- throw new MWException( __METHOD__ . ' called without Title object set' );
- }
- $this->wikipage = WikiPage::factory( $title );
- }
- return $this->wikipage;
- }
- /**
- * @param OutputPage $output
- */
- public function setOutput( OutputPage $output ) {
- $this->output = $output;
- }
- /**
- * @return OutputPage
- */
- public function getOutput() {
- if ( $this->output === null ) {
- $this->output = new OutputPage( $this );
- }
- return $this->output;
- }
- /**
- * @param User $user
- */
- public function setUser( User $user ) {
- $this->user = $user;
- }
- /**
- * @return User
- */
- public function getUser() {
- if ( $this->user === null ) {
- $this->user = User::newFromSession( $this->getRequest() );
- }
- return $this->user;
- }
- /**
- * Accepts a language code and ensures it's sane. Outputs a cleaned up language
- * code and replaces with $wgLanguageCode if not sane.
- * @param string $code Language code
- * @return string
- */
- public static function sanitizeLangCode( $code ) {
- global $wgLanguageCode;
- // BCP 47 - letter case MUST NOT carry meaning
- $code = strtolower( $code );
- # Validate $code
- if ( !$code || !Language::isValidCode( $code ) || $code === 'qqq' ) {
- $code = $wgLanguageCode;
- }
- return $code;
- }
- /**
- * @param Language|string $language Language instance or language code
- * @throws MWException
- * @since 1.19
- */
- public function setLanguage( $language ) {
- if ( $language instanceof Language ) {
- $this->lang = $language;
- } elseif ( is_string( $language ) ) {
- $language = self::sanitizeLangCode( $language );
- $obj = Language::factory( $language );
- $this->lang = $obj;
- } else {
- throw new MWException( __METHOD__ . " was passed an invalid type of data." );
- }
- }
- /**
- * Get the Language object.
- * Initialization of user or request objects can depend on this.
- * @return Language
- * @throws Exception
- * @since 1.19
- */
- public function getLanguage() {
- if ( isset( $this->recursion ) ) {
- trigger_error( "Recursion detected in " . __METHOD__, E_USER_WARNING );
- $e = new Exception;
- wfDebugLog( 'recursion-guard', "Recursion detected:\n" . $e->getTraceAsString() );
- $code = $this->getConfig()->get( 'LanguageCode' ) ?: 'en';
- $this->lang = Language::factory( $code );
- } elseif ( $this->lang === null ) {
- $this->recursion = true;
- try {
- $request = $this->getRequest();
- $user = $this->getUser();
- $code = $request->getVal( 'uselang', 'user' );
- if ( $code === 'user' ) {
- $code = $user->getOption( 'language' );
- }
- $code = self::sanitizeLangCode( $code );
- Hooks::run( 'UserGetLanguageObject', [ $user, &$code, $this ] );
- if ( $code === $this->getConfig()->get( 'LanguageCode' ) ) {
- $this->lang = MediaWikiServices::getInstance()->getContentLanguage();
- } else {
- $obj = Language::factory( $code );
- $this->lang = $obj;
- }
- unset( $this->recursion );
- }
- catch ( Exception $ex ) {
- unset( $this->recursion );
- throw $ex;
- }
- }
- return $this->lang;
- }
- /**
- * @param Skin $skin
- */
- public function setSkin( Skin $skin ) {
- $this->skin = clone $skin;
- $this->skin->setContext( $this );
- }
- /**
- * @return Skin
- */
- public function getSkin() {
- if ( $this->skin === null ) {
- $skin = null;
- Hooks::run( 'RequestContextCreateSkin', [ $this, &$skin ] );
- $factory = SkinFactory::getDefaultInstance();
- // If the hook worked try to set a skin from it
- if ( $skin instanceof Skin ) {
- $this->skin = $skin;
- } elseif ( is_string( $skin ) ) {
- // Normalize the key, just in case the hook did something weird.
- $normalized = Skin::normalizeKey( $skin );
- $this->skin = $factory->makeSkin( $normalized );
- }
- // If this is still null (the hook didn't run or didn't work)
- // then go through the normal processing to load a skin
- if ( $this->skin === null ) {
- if ( !in_array( 'skin', $this->getConfig()->get( 'HiddenPrefs' ) ) ) {
- # get the user skin
- $userSkin = $this->getUser()->getOption( 'skin' );
- $userSkin = $this->getRequest()->getVal( 'useskin', $userSkin );
- } else {
- # if we're not allowing users to override, then use the default
- $userSkin = $this->getConfig()->get( 'DefaultSkin' );
- }
- // Normalize the key in case the user is passing gibberish
- // or has old preferences (T71566).
- $normalized = Skin::normalizeKey( $userSkin );
- // Skin::normalizeKey will also validate it, so
- // this won't throw an exception
- $this->skin = $factory->makeSkin( $normalized );
- }
- // After all that set a context on whatever skin got created
- $this->skin->setContext( $this );
- }
- return $this->skin;
- }
- /**
- * Get a Message object with context set
- * Parameters are the same as wfMessage()
- *
- * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
- * or a MessageSpecifier.
- * @param mixed $args,...
- * @return Message
- */
- public function msg( $key ) {
- $args = func_get_args();
- return wfMessage( ...$args )->setContext( $this );
- }
- /**
- * Get the RequestContext object associated with the main request
- *
- * @return RequestContext
- */
- public static function getMain() {
- if ( self::$instance === null ) {
- self::$instance = new self;
- }
- return self::$instance;
- }
- /**
- * Get the RequestContext object associated with the main request
- * and gives a warning to the log, to find places, where a context maybe is missing.
- *
- * @param string $func
- * @return RequestContext
- * @since 1.24
- */
- public static function getMainAndWarn( $func = __METHOD__ ) {
- wfDebug( $func . ' called without context. ' .
- "Using RequestContext::getMain() for sanity\n" );
- return self::getMain();
- }
- /**
- * Resets singleton returned by getMain(). Should be called only from unit tests.
- */
- public static function resetMain() {
- if ( !( defined( 'MW_PHPUNIT_TEST' ) || defined( 'MW_PARSER_TEST' ) ) ) {
- throw new MWException( __METHOD__ . '() should be called only from unit tests!' );
- }
- self::$instance = null;
- }
- /**
- * Export the resolved user IP, HTTP headers, user ID, and session ID.
- * The result will be reasonably sized to allow for serialization.
- *
- * @return array
- * @since 1.21
- */
- public function exportSession() {
- $session = MediaWiki\Session\SessionManager::getGlobalSession();
- return [
- 'ip' => $this->getRequest()->getIP(),
- 'headers' => $this->getRequest()->getAllHeaders(),
- 'sessionId' => $session->isPersistent() ? $session->getId() : '',
- 'userId' => $this->getUser()->getId()
- ];
- }
- /**
- * Import an client IP address, HTTP headers, user ID, and session ID
- *
- * This sets the current session, $wgUser, and $wgRequest from $params.
- * Once the return value falls out of scope, the old context is restored.
- * This method should only be called in contexts where there is no session
- * ID or end user receiving the response (CLI or HTTP job runners). This
- * is partly enforced, and is done so to avoid leaking cookies if certain
- * error conditions arise.
- *
- * This is useful when background scripts inherit context when acting on
- * behalf of a user. In general the 'sessionId' parameter should be set
- * to an empty string unless session importing is *truly* needed. This
- * feature is somewhat deprecated.
- *
- * @note suhosin.session.encrypt may interfere with this method.
- *
- * @param array $params Result of RequestContext::exportSession()
- * @return ScopedCallback
- * @throws MWException
- * @since 1.21
- */
- public static function importScopedSession( array $params ) {
- if ( strlen( $params['sessionId'] ) &&
- MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent()
- ) {
- // Sanity check to avoid sending random cookies for the wrong users.
- // This method should only called by CLI scripts or by HTTP job runners.
- throw new MWException( "Sessions can only be imported when none is active." );
- } elseif ( !IP::isValid( $params['ip'] ) ) {
- throw new MWException( "Invalid client IP address '{$params['ip']}'." );
- }
- if ( $params['userId'] ) { // logged-in user
- $user = User::newFromId( $params['userId'] );
- $user->load();
- if ( !$user->getId() ) {
- throw new MWException( "No user with ID '{$params['userId']}'." );
- }
- } else { // anon user
- $user = User::newFromName( $params['ip'], false );
- }
- $importSessionFunc = function ( User $user, array $params ) {
- global $wgRequest, $wgUser;
- $context = RequestContext::getMain();
- // Commit and close any current session
- if ( MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
- session_write_close(); // persist
- session_id( '' ); // detach
- $_SESSION = []; // clear in-memory array
- }
- // Get new session, if applicable
- $session = null;
- if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
- $manager = MediaWiki\Session\SessionManager::singleton();
- $session = $manager->getSessionById( $params['sessionId'], true )
- ?: $manager->getEmptySession();
- }
- // Remove any user IP or agent information, and attach the request
- // with the new session.
- $context->setRequest( new FauxRequest( [], false, $session ) );
- $wgRequest = $context->getRequest(); // b/c
- // Now that all private information is detached from the user, it should
- // be safe to load the new user. If errors occur or an exception is thrown
- // and caught (leaving the main context in a mixed state), there is no risk
- // of the User object being attached to the wrong IP, headers, or session.
- $context->setUser( $user );
- $wgUser = $context->getUser(); // b/c
- if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
- session_id( $session->getId() );
- Wikimedia\quietCall( 'session_start' );
- }
- $request = new FauxRequest( [], false, $session );
- $request->setIP( $params['ip'] );
- foreach ( $params['headers'] as $name => $value ) {
- $request->setHeader( $name, $value );
- }
- // Set the current context to use the new WebRequest
- $context->setRequest( $request );
- $wgRequest = $context->getRequest(); // b/c
- };
- // Stash the old session and load in the new one
- $oUser = self::getMain()->getUser();
- $oParams = self::getMain()->exportSession();
- $oRequest = self::getMain()->getRequest();
- $importSessionFunc( $user, $params );
- // Set callback to save and close the new session and reload the old one
- return new ScopedCallback(
- function () use ( $importSessionFunc, $oUser, $oParams, $oRequest ) {
- global $wgRequest;
- $importSessionFunc( $oUser, $oParams );
- // Restore the exact previous Request object (instead of leaving FauxRequest)
- RequestContext::getMain()->setRequest( $oRequest );
- $wgRequest = RequestContext::getMain()->getRequest(); // b/c
- }
- );
- }
- /**
- * Create a new extraneous context. The context is filled with information
- * external to the current session.
- * - Title is specified by argument
- * - Request is a FauxRequest, or a FauxRequest can be specified by argument
- * - User is an anonymous user, for separation IPv4 localhost is used
- * - Language will be based on the anonymous user and request, may be content
- * language or a uselang param in the fauxrequest data may change the lang
- * - Skin will be based on the anonymous user, should be the wiki's default skin
- *
- * @param Title $title Title to use for the extraneous request
- * @param WebRequest|array $request A WebRequest or data to use for a FauxRequest
- * @return RequestContext
- */
- public static function newExtraneousContext( Title $title, $request = [] ) {
- $context = new self;
- $context->setTitle( $title );
- if ( $request instanceof WebRequest ) {
- $context->setRequest( $request );
- } else {
- $context->setRequest( new FauxRequest( $request ) );
- }
- $context->user = User::newFromName( '127.0.0.1', false );
- return $context;
- }
- }
|