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