Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.06% covered (warning)
66.06%
146 / 221
48.72% covered (danger)
48.72%
19 / 39
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestContext
66.36% covered (warning)
66.36%
146 / 220
48.72% covered (danger)
48.72%
19 / 39
359.96
0.00% covered (danger)
0.00%
0 / 1
 setConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConfig
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequest
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 getTiming
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setTitle
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
3.69
 hasTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canUseWikiPage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 setWikiPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getWikiPage
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 setActionName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getActionName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 clearActionName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setOutput
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOutput
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setUser
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 hasUser
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 setAuthority
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getAuthority
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 sanitizeLangCode
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 setLanguage
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getLanguage
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 getLanguageCode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSkin
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getSkinName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 fetchSkinName
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
4.10
 getSkinFromHook
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getSkin
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 msg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMain
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMainAndWarn
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 resetMain
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 exportSession
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getCsrfTokenSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 importScopedSession
93.75% covered (success)
93.75%
45 / 48
0.00% covered (danger)
0.00%
0 / 1
12.04
 newExtraneousContext
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 __clone
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
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
25namespace MediaWiki\Context;
26
27use BadMethodCallException;
28use InvalidArgumentException;
29use LogicException;
30use MediaWiki\Config\Config;
31use MediaWiki\HookContainer\HookRunner;
32use MediaWiki\Language\Language;
33use MediaWiki\Logger\LoggerFactory;
34use MediaWiki\MainConfigNames;
35use MediaWiki\MediaWikiServices;
36use MediaWiki\Message\Message;
37use MediaWiki\Output\OutputPage;
38use MediaWiki\Permissions\Authority;
39use MediaWiki\Request\FauxRequest;
40use MediaWiki\Request\WebRequest;
41use MediaWiki\Session\CsrfTokenSet;
42use MediaWiki\Session\PHPSessionHandler;
43use MediaWiki\Session\SessionManager;
44use MediaWiki\StubObject\StubGlobalUser;
45use MediaWiki\Title\Title;
46use MediaWiki\User\User;
47use MediaWiki\User\UserRigorOptions;
48use RuntimeException;
49use Skin;
50use Timing;
51use Wikimedia\Assert\Assert;
52use Wikimedia\AtEase\AtEase;
53use Wikimedia\Bcp47Code\Bcp47Code;
54use Wikimedia\IPUtils;
55use Wikimedia\Message\MessageParam;
56use Wikimedia\Message\MessageSpecifier;
57use Wikimedia\NonSerializable\NonSerializableTrait;
58use Wikimedia\ScopedCallback;
59use WikiPage;
60
61/**
62 * Group all the pieces relevant to the context of a request into one instance
63 * @newable
64 * @note marked as newable in 1.35 for lack of a better alternative,
65 *       but should use a factory in the future and should be narrowed
66 *       down to not expose heavy weight objects.
67 */
68class RequestContext implements IContextSource, MutableContext {
69    use NonSerializableTrait;
70
71    /**
72     * @var WebRequest
73     */
74    private $request;
75
76    /**
77     * @var Title
78     */
79    private $title;
80
81    /**
82     * @var WikiPage|null
83     */
84    private $wikipage;
85
86    /**
87     * @var null|string
88     */
89    private $action;
90
91    /**
92     * @var OutputPage
93     */
94    private $output;
95
96    /**
97     * @var User|null
98     */
99    private $user;
100
101    /**
102     * @var Authority
103     */
104    private $authority;
105
106    /**
107     * @var Language|null
108     */
109    private $lang;
110
111    /**
112     * @var Skin|null
113     */
114    private $skin;
115
116    /**
117     * @var Timing
118     */
119    private $timing;
120
121    /**
122     * @var Config
123     */
124    private $config;
125
126    /**
127     * @var RequestContext|null
128     */
129    private static $instance = null;
130
131    /**
132     * Boolean flag to guard against recursion in getLanguage
133     * @var bool
134     */
135    private $languageRecursion = false;
136
137    /** @var Skin|string|null */
138    private $skinFromHook;
139
140    /** @var bool */
141    private $skinHookCalled = false;
142
143    /** @var string|null */
144    private $skinName;
145
146    /**
147     * @param Config $config
148     */
149    public function setConfig( Config $config ) {
150        $this->config = $config;
151    }
152
153    /**
154     * @return Config
155     */
156    public function getConfig() {
157        // @todo In the future, we could move this to WebStart.php so
158        // the Config object is ready for when initialization happens
159        $this->config ??= MediaWikiServices::getInstance()->getMainConfig();
160
161        return $this->config;
162    }
163
164    /**
165     * @param WebRequest $request
166     */
167    public function setRequest( WebRequest $request ) {
168        $this->request = $request;
169    }
170
171    /**
172     * @return WebRequest
173     */
174    public function getRequest() {
175        if ( $this->request === null ) {
176            // create the WebRequest object on the fly
177            if ( MW_ENTRY_POINT === 'cli' ) {
178                // Don't use real WebRequest in CLI mode, it throws errors when trying to access
179                // things that don't exist, e.g. "Unable to determine IP".
180                $this->request = new FauxRequest( [] );
181            } else {
182                $this->request = new WebRequest();
183            }
184        }
185
186        return $this->request;
187    }
188
189    /**
190     * @return Timing
191     */
192    public function getTiming() {
193        $this->timing ??= new Timing( [
194            'logger' => LoggerFactory::getInstance( 'Timing' )
195        ] );
196        return $this->timing;
197    }
198
199    /**
200     * @param Title|null $title
201     */
202    public function setTitle( ?Title $title = null ) {
203        $this->title = $title;
204        // Clear cache of derived getters
205        $this->wikipage = null;
206        $this->clearActionName();
207    }
208
209    /**
210     * @return Title|null
211     */
212    public function getTitle() {
213        if ( $this->title === null ) {
214            // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
215            global $wgTitle; # fallback to $wg till we can improve this
216            $this->title = $wgTitle;
217            $logger = LoggerFactory::getInstance( 'GlobalTitleFail' );
218            $logger->info(
219                __METHOD__ . ' called with no title set.',
220                [ 'exception' => new RuntimeException ]
221            );
222        }
223
224        return $this->title;
225    }
226
227    /**
228     * Check, if a Title object is set
229     *
230     * @since 1.25
231     * @return bool
232     */
233    public function hasTitle() {
234        return $this->title !== null;
235    }
236
237    /**
238     * Check whether a WikiPage object can be get with getWikiPage().
239     * Callers should expect that an exception is thrown from getWikiPage()
240     * if this method returns false.
241     *
242     * @since 1.19
243     * @return bool
244     */
245    public function canUseWikiPage() {
246        if ( $this->wikipage ) {
247            // If there's a WikiPage object set, we can for sure get it
248            return true;
249        }
250        // Only pages with legitimate titles can have WikiPages.
251        // That usually means pages in non-virtual namespaces.
252        $title = $this->getTitle();
253        return $title && $title->canExist();
254    }
255
256    /**
257     * @since 1.19
258     * @param WikiPage $wikiPage
259     */
260    public function setWikiPage( WikiPage $wikiPage ) {
261        $pageTitle = $wikiPage->getTitle();
262        if ( !$this->hasTitle() || !$pageTitle->equals( $this->getTitle() ) ) {
263            $this->setTitle( $pageTitle );
264        }
265        // Defer this to the end since setTitle sets it to null.
266        $this->wikipage = $wikiPage;
267        // Clear cache of derived getter
268        $this->clearActionName();
269    }
270
271    /**
272     * Get the WikiPage object.
273     * May throw an exception if there's no Title object set or the Title object
274     * belongs to a special namespace that doesn't have WikiPage, so use first
275     * canUseWikiPage() to check whether this method can be called safely.
276     *
277     * @since 1.19
278     * @return WikiPage
279     */
280    public function getWikiPage() {
281        if ( $this->wikipage === null ) {
282            $title = $this->getTitle();
283            if ( $title === null ) {
284                throw new BadMethodCallException( __METHOD__ . ' called without Title object set' );
285            }
286            $this->wikipage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
287        }
288
289        return $this->wikipage;
290    }
291
292    /**
293     * @since 1.38
294     * @param string $action
295     */
296    public function setActionName( string $action ): void {
297        $this->action = $action;
298    }
299
300    /**
301     * Get the action name for the current web request.
302     *
303     * This generally returns "view" if the current request or process is
304     * not for a skinned index.php web request (e.g. load.php, thumb.php,
305     * job runner, CLI, API).
306     *
307     * @warning This must not be called before or during the Setup.php phase,
308     * and may cause an error or warning if called too early.
309     *
310     * @since 1.38
311     * @return string Action
312     */
313    public function getActionName(): string {
314        // Optimisation: This is cached to avoid repeated running of the
315        // expensive operations to compute this. The computation involves creation
316        // of Article, WikiPage, and ContentHandler objects (and the various
317        // database queries these classes require to be instantiated), as well
318        // as potentially slow extension hooks in these classes.
319        //
320        // This value is frequently needed in OutputPage and in various
321        // Skin-related methods and classes.
322        $this->action ??= MediaWikiServices::getInstance()
323            ->getActionFactory()
324            ->getActionName( $this );
325
326        return $this->action;
327    }
328
329    private function clearActionName(): void {
330        if ( $this->action !== null ) {
331            // If we're clearing after something else has actually already computed the action,
332            // emit a warning.
333            //
334            // Doing so is unstable, given the first caller got something that turns out to be
335            // incomplete or incorrect. Even if we end up re-creating an instance of the same
336            // class, we may now be acting on a different title/skin/user etc.
337            //
338            // Re-computing the action is expensive and can be a performance problem (T302623).
339            trigger_error( 'Unexpected clearActionName after getActionName already called' );
340            $this->action = null;
341        }
342    }
343
344    /**
345     * @param OutputPage $output
346     */
347    public function setOutput( OutputPage $output ) {
348        $this->output = $output;
349    }
350
351    /**
352     * @return OutputPage
353     */
354    public function getOutput() {
355        $this->output ??= new OutputPage( $this );
356
357        return $this->output;
358    }
359
360    /**
361     * @param User $user
362     */
363    public function setUser( User $user ) {
364        $this->user = $user;
365        // Keep authority consistent
366        $this->authority = $user;
367        // Invalidate cached user interface language and skin
368        $this->lang = null;
369        $this->skin = null;
370        $this->skinName = null;
371    }
372
373    /**
374     * @return User
375     */
376    public function getUser() {
377        if ( $this->user === null ) {
378            if ( $this->authority !== null ) {
379                // Keep user consistent by using a possible set authority
380                $this->user = MediaWikiServices::getInstance()
381                    ->getUserFactory()
382                    ->newFromAuthority( $this->authority );
383            } else {
384                $this->user = User::newFromSession( $this->getRequest() );
385            }
386        }
387
388        return $this->user;
389    }
390
391    public function hasUser(): bool {
392        if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
393            throw new LogicException( __METHOD__ . '() should be called only from tests!' );
394        }
395        return $this->user !== null;
396    }
397
398    /**
399     * @param Authority $authority
400     */
401    public function setAuthority( Authority $authority ) {
402        $this->authority = $authority;
403        // If needed, a User object is constructed from this authority
404        $this->user = null;
405        // Invalidate cached user interface language and skin
406        $this->lang = null;
407        $this->skin = null;
408        $this->skinName = null;
409    }
410
411    /**
412     * @since 1.36
413     * @return Authority
414     */
415    public function getAuthority(): Authority {
416        return $this->authority ?: $this->getUser();
417    }
418
419    /**
420     * Accepts a language code and ensures it's sensible. Outputs a cleaned up language
421     * code and replaces with $wgLanguageCode if not sensible.
422     * @param ?string $code Language code
423     * @return string
424     */
425    public static function sanitizeLangCode( $code ) {
426        global $wgLanguageCode;
427
428        if ( !$code ) {
429            return $wgLanguageCode;
430        }
431
432        // BCP 47 - letter case MUST NOT carry meaning
433        $code = strtolower( $code );
434
435        # Validate $code
436        if ( !MediaWikiServices::getInstance()->getLanguageNameUtils()
437                ->isValidCode( $code )
438            || $code === 'qqq'
439        ) {
440            $code = $wgLanguageCode;
441        }
442
443        return $code;
444    }
445
446    /**
447     * @param Language|string $language Language instance or language code
448     * @since 1.19
449     */
450    public function setLanguage( $language ) {
451        Assert::parameterType( [ Language::class, 'string' ], $language, '$language' );
452        if ( $language instanceof Language ) {
453            $this->lang = $language;
454        } else {
455            $language = self::sanitizeLangCode( $language );
456            $obj = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $language );
457            $this->lang = $obj;
458        }
459        OutputPage::resetOOUI();
460    }
461
462    /**
463     * Get the Language object.
464     * Initialization of user or request objects can depend on this.
465     * @return Language
466     * @throws LogicException
467     * @since 1.19
468     */
469    public function getLanguage() {
470        if ( $this->languageRecursion === true ) {
471            throw new LogicException( 'Recursion detected' );
472        }
473
474        if ( $this->lang === null ) {
475            $this->languageRecursion = true;
476
477            try {
478                $request = $this->getRequest();
479                $user = $this->getUser();
480                $services = MediaWikiServices::getInstance();
481
482                // Optimisation: Avoid slow getVal(), this isn't user-generated content.
483                $code = $request->getRawVal( 'uselang' ) ?? 'user';
484                if ( $code === 'user' ) {
485                    $userOptionsLookup = $services->getUserOptionsLookup();
486                    $code = $userOptionsLookup->getOption( $user, 'language' );
487                }
488
489                // There are certain characters we don't allow in language code strings,
490                // but by and large almost any valid UTF-8 string will makes it past
491                // this check and the LanguageNameUtils::isValidCode method it uses.
492                // This is to support on-wiki interface message overrides for
493                // non-existent language codes. Also known as "Uselang hacks".
494                // See <https://www.mediawiki.org/wiki/Manual:Uselang_hack>
495                // For something like "en-whatever" or "de-whatever" it will end up
496                // with a mostly "en" or "de" interface, but with an extra layer of
497                // possible MessageCache overrides from `MediaWiki:*/<code>` titles.
498                // While non-ASCII works here, it is required that they are in
499                // NFC form given this will not convert to normalised form.
500                $code = self::sanitizeLangCode( $code );
501
502                ( new HookRunner( $services->getHookContainer() ) )->onUserGetLanguageObject( $user, $code, $this );
503
504                if ( $code === $this->getConfig()->get( MainConfigNames::LanguageCode ) ) {
505                    $this->lang = $services->getContentLanguage();
506                } else {
507                    $obj = $services->getLanguageFactory()
508                        ->getLanguage( $code );
509                    $this->lang = $obj;
510                }
511            } finally {
512                $this->languageRecursion = false;
513            }
514        }
515
516        return $this->lang;
517    }
518
519    /**
520     * @since 1.42
521     * @return Bcp47Code
522     */
523    public function getLanguageCode() {
524        return $this->getLanguage();
525    }
526
527    /**
528     * @param Skin $skin
529     */
530    public function setSkin( Skin $skin ) {
531        $this->skin = clone $skin;
532        $this->skin->setContext( $this );
533        $this->skinName = $skin->getSkinName();
534        OutputPage::resetOOUI();
535    }
536
537    /**
538     * Get the name of the skin
539     *
540     * @since 1.41
541     * @return string
542     */
543    public function getSkinName() {
544        if ( $this->skinName === null ) {
545            $this->skinName = $this->fetchSkinName();
546        }
547        return $this->skinName;
548    }
549
550    /**
551     * Get the name of the skin, without caching
552     *
553     * @return string
554     */
555    private function fetchSkinName() {
556        $skinFromHook = $this->getSkinFromHook();
557        if ( $skinFromHook instanceof Skin ) {
558            // The hook provided a skin object
559            return $skinFromHook->getSkinName();
560        } elseif ( is_string( $skinFromHook ) ) {
561            // The hook provided a skin name
562            $skinName = $skinFromHook;
563        } elseif ( !in_array( 'skin', $this->getConfig()->get( MainConfigNames::HiddenPrefs ) ) ) {
564            // The normal case
565            $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
566            $userSkin = $userOptionsLookup->getOption( $this->getUser(), 'skin' );
567            // Optimisation: Avoid slow getVal(), this isn't user-generated content.
568            $skinName = $this->getRequest()->getRawVal( 'useskin' ) ?? $userSkin;
569        } else {
570            // User preference disabled
571            $skinName = $this->getConfig()->get( MainConfigNames::DefaultSkin );
572        }
573        return Skin::normalizeKey( $skinName );
574    }
575
576    /**
577     * Get the skin set by the RequestContextCreateSkin hook, if there is any.
578     *
579     * @return Skin|string|null
580     */
581    private function getSkinFromHook() {
582        if ( !$this->skinHookCalled ) {
583            $this->skinHookCalled = true;
584            ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
585                ->onRequestContextCreateSkin( $this, $this->skinFromHook );
586        }
587        return $this->skinFromHook;
588    }
589
590    /**
591     * @return Skin
592     */
593    public function getSkin() {
594        if ( $this->skin === null ) {
595            $skinFromHook = $this->getSkinFromHook();
596            if ( $skinFromHook instanceof Skin ) {
597                $this->skin = $skinFromHook;
598            } else {
599                $skinName = is_string( $skinFromHook )
600                    ? Skin::normalizeKey( $skinFromHook )
601                    : $this->getSkinName();
602                $factory = MediaWikiServices::getInstance()->getSkinFactory();
603                $this->skin = $factory->makeSkin( $skinName );
604            }
605            $this->skin->setContext( $this );
606        }
607        return $this->skin;
608    }
609
610    /**
611     * Get a Message object with context set
612     * Parameters are the same as wfMessage()
613     *
614     * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
615     *   or a MessageSpecifier.
616     * @phpcs:ignore Generic.Files.LineLength
617     * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params
618     *   See Message::params()
619     * @return Message
620     */
621    public function msg( $key, ...$params ) {
622        return wfMessage( $key, ...$params )->setContext( $this );
623    }
624
625    /**
626     * Get the RequestContext object associated with the main request
627     *
628     * @return RequestContext
629     */
630    public static function getMain(): RequestContext {
631        self::$instance ??= new self;
632
633        return self::$instance;
634    }
635
636    /**
637     * Get the RequestContext object associated with the main request
638     * and gives a warning to the log, to find places, where a context maybe is missing.
639     *
640     * @param string $func @phan-mandatory-param
641     * @return RequestContext
642     * @since 1.24
643     */
644    public static function getMainAndWarn( $func = __METHOD__ ) {
645        wfDebug( $func . ' called without context. ' .
646            "Using RequestContext::getMain()" );
647
648        return self::getMain();
649    }
650
651    /**
652     * Resets singleton returned by getMain(). Should be called only from unit tests.
653     */
654    public static function resetMain() {
655        if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
656            throw new LogicException( __METHOD__ . '() should be called only from unit tests!' );
657        }
658        self::$instance = null;
659    }
660
661    /**
662     * Export the resolved user IP, HTTP headers, user ID, and session ID.
663     * The result will be reasonably sized to allow for serialization.
664     *
665     * @return array
666     * @since 1.21
667     */
668    public function exportSession() {
669        $session = SessionManager::getGlobalSession();
670        return [
671            'ip' => $this->getRequest()->getIP(),
672            'headers' => $this->getRequest()->getAllHeaders(),
673            'sessionId' => $session->isPersistent() ? $session->getId() : '',
674            'userId' => $this->getUser()->getId()
675        ];
676    }
677
678    public function getCsrfTokenSet(): CsrfTokenSet {
679        return new CsrfTokenSet( $this->getRequest() );
680    }
681
682    /**
683     * Import a client IP address, HTTP headers, user ID, and session ID
684     *
685     * This sets the current session, $wgUser, and $wgRequest from $params.
686     * Once the return value falls out of scope, the old context is restored.
687     * This method should only be called in contexts where there is no session
688     * ID or end user receiving the response (CLI or HTTP job runners). This
689     * is partly enforced, and is done so to avoid leaking cookies if certain
690     * error conditions arise.
691     *
692     * This is useful when background scripts inherit context when acting on
693     * behalf of a user. In general the 'sessionId' parameter should be set
694     * to an empty string unless session importing is *truly* needed. This
695     * feature is somewhat deprecated.
696     *
697     * @param array $params Result of RequestContext::exportSession()
698     * @return ScopedCallback
699     * @since 1.21
700     */
701    public static function importScopedSession( array $params ) {
702        if ( strlen( $params['sessionId'] ) &&
703            SessionManager::getGlobalSession()->isPersistent()
704        ) {
705            // Check to avoid sending random cookies for the wrong users.
706            // This method should only called by CLI scripts or by HTTP job runners.
707            throw new BadMethodCallException( "Sessions can only be imported when none is active." );
708        } elseif ( !IPUtils::isValid( $params['ip'] ) ) {
709            throw new InvalidArgumentException( "Invalid client IP address '{$params['ip']}'." );
710        }
711
712        $userFactory = MediaWikiServices::getInstance()->getUserFactory();
713
714        if ( $params['userId'] ) { // logged-in user
715            $user = $userFactory->newFromId( (int)$params['userId'] );
716            $user->load();
717            if ( !$user->isRegistered() ) {
718                throw new InvalidArgumentException( "No user with ID '{$params['userId']}'." );
719            }
720        } else { // anon user
721            $user = $userFactory->newFromName( $params['ip'], UserRigorOptions::RIGOR_NONE );
722        }
723
724        $importSessionFunc = static function ( User $user, array $params ) {
725            global $wgRequest;
726
727            $context = RequestContext::getMain();
728
729            // Commit and close any current session
730            if ( PHPSessionHandler::isEnabled() ) {
731                session_write_close(); // persist
732                session_id( '' ); // detach
733                $_SESSION = []; // clear in-memory array
734            }
735
736            // Get new session, if applicable
737            $session = null;
738            if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
739                $manager = SessionManager::singleton();
740                $session = $manager->getSessionById( $params['sessionId'], true )
741                    ?: $manager->getEmptySession();
742            }
743
744            // Remove any user IP or agent information, and attach the request
745            // with the new session.
746            $context->setRequest( new FauxRequest( [], false, $session ) );
747            $wgRequest = $context->getRequest(); // b/c
748
749            // Now that all private information is detached from the user, it should
750            // be safe to load the new user. If errors occur or an exception is thrown
751            // and caught (leaving the main context in a mixed state), there is no risk
752            // of the User object being attached to the wrong IP, headers, or session.
753            $context->setUser( $user );
754            StubGlobalUser::setUser( $context->getUser() ); // b/c
755            if ( $session && PHPSessionHandler::isEnabled() ) {
756                session_id( $session->getId() );
757                AtEase::quietCall( 'session_start' );
758            }
759            $request = new FauxRequest( [], false, $session );
760            $request->setIP( $params['ip'] );
761            foreach ( $params['headers'] as $name => $value ) {
762                $request->setHeader( $name, $value );
763            }
764            // Set the current context to use the new WebRequest
765            $context->setRequest( $request );
766            $wgRequest = $context->getRequest(); // b/c
767        };
768
769        // Stash the old session and load in the new one
770        $oUser = self::getMain()->getUser();
771        $oParams = self::getMain()->exportSession();
772        $oRequest = self::getMain()->getRequest();
773        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable exceptions triggered above prevent the null case
774        $importSessionFunc( $user, $params );
775
776        // Set callback to save and close the new session and reload the old one
777        return new ScopedCallback(
778            static function () use ( $importSessionFunc, $oUser, $oParams, $oRequest ) {
779                global $wgRequest;
780                $importSessionFunc( $oUser, $oParams );
781                // Restore the exact previous Request object (instead of leaving MediaWiki\Request\FauxRequest)
782                RequestContext::getMain()->setRequest( $oRequest );
783                $wgRequest = RequestContext::getMain()->getRequest(); // b/c
784            }
785        );
786    }
787
788    /**
789     * Create a new extraneous context. The context is filled with information
790     * external to the current session.
791     * - Title is specified by argument
792     * - Request is a MediaWiki\Request\FauxRequest, or a MediaWiki\Request\FauxRequest can be specified by argument
793     * - User is an anonymous user, for separation IPv4 localhost is used
794     * - Language will be based on the anonymous user and request, may be content
795     *   language or a uselang param in the fauxrequest data may change the lang
796     * - Skin will be based on the anonymous user, should be the wiki's default skin
797     *
798     * @param Title $title Title to use for the extraneous request
799     * @param WebRequest|array $request A WebRequest or data to use for a MediaWiki\Request\FauxRequest
800     * @return RequestContext
801     */
802    public static function newExtraneousContext( Title $title, $request = [] ) {
803        $context = new self;
804        $context->setTitle( $title );
805        if ( $request instanceof WebRequest ) {
806            $context->setRequest( $request );
807        } else {
808            $context->setRequest( new FauxRequest( $request ) );
809        }
810        $context->user = MediaWikiServices::getInstance()->getUserFactory()->newFromName(
811            '127.0.0.1',
812            UserRigorOptions::RIGOR_NONE
813        );
814
815        return $context;
816    }
817
818    /** @return never */
819    public function __clone() {
820        throw new LogicException(
821            __CLASS__ . ' should not be cloned, use DerivativeContext instead.'
822        );
823    }
824
825}
826
827/** @deprecated class alias since 1.42 */
828class_alias( RequestContext::class, 'RequestContext' );