Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.56% covered (warning)
84.56%
115 / 136
83.33% covered (warning)
83.33%
25 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
Context
84.56% covered (warning)
84.56%
115 / 136
83.33% covered (warning)
83.33%
25 / 30
71.82
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
4
 debugFromString
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 newDummyContext
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getResourceLoader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getModules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLanguage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getDirection
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getSkin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 msg
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getUserIdentity
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getUserObj
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getDebug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOnly
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRaw
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSourceMap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getImage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVariant
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImageObj
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 getContentOverrideCallback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shouldIncludeScripts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 shouldIncludeStyles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 shouldIncludeMessages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHash
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 getReqBase
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
4.10
 encodeJson
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
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 * @file
19 * @author Trevor Parscal
20 * @author Roan Kattouw
21 */
22
23namespace MediaWiki\ResourceLoader;
24
25use MediaWiki\Logger\LoggerFactory;
26use MediaWiki\MediaWikiServices;
27use MediaWiki\Message\Message;
28use MediaWiki\Page\PageReferenceValue;
29use MediaWiki\Request\FauxRequest;
30use MediaWiki\Request\WebRequest;
31use MediaWiki\User\User;
32use MediaWiki\User\UserIdentity;
33use MediaWiki\User\UserRigorOptions;
34use MessageLocalizer;
35use MessageSpecifier;
36use Psr\Log\LoggerInterface;
37
38/**
39 * Context object that contains information about the state of a specific
40 * ResourceLoader web request. Passed around to Module methods.
41 *
42 * @ingroup ResourceLoader
43 * @since 1.17
44 */
45class Context implements MessageLocalizer {
46    public const DEFAULT_LANG = 'qqx';
47    public const DEFAULT_SKIN = 'fallback';
48
49    /** @internal For use in ResourceLoader classes. */
50    public const DEBUG_OFF = 0;
51    /** @internal For use in ResourceLoader classes. */
52    public const DEBUG_LEGACY = 1;
53    /** @internal For use in SpecialJavaScriptTest. */
54    public const DEBUG_MAIN = 2;
55
56    /** @var ResourceLoader */
57    protected $resourceLoader;
58    /** @var WebRequest */
59    protected $request;
60    /** @var LoggerInterface */
61    protected $logger;
62
63    // Module content vary
64    /** @var string */
65    protected $skin;
66    /** @var string */
67    protected $language;
68    /** @var int */
69    protected $debug;
70    /** @var string|null */
71    protected $user;
72
73    // Request vary (in addition to cache vary)
74    /** @var string[] */
75    protected $modules;
76    /** @var string|null */
77    protected $only;
78    /** @var string|null */
79    protected $version;
80    /** @var bool */
81    protected $raw;
82    /** @var bool */
83    protected $sourcemap;
84    /** @var string|null */
85    protected $image;
86    /** @var string|null */
87    protected $variant;
88    /** @var string|null */
89    protected $format;
90
91    /** @var string|null */
92    protected $direction;
93    /** @var string|null */
94    protected $hash;
95    /** @var User|null */
96    protected $userObj;
97    /** @var UserIdentity|null|false */
98    protected $userIdentity = false;
99    /** @var Image|false */
100    protected $imageObj;
101
102    /**
103     * @param ResourceLoader $resourceLoader
104     * @param WebRequest $request
105     */
106    public function __construct( ResourceLoader $resourceLoader, WebRequest $request ) {
107        $this->resourceLoader = $resourceLoader;
108        $this->request = $request;
109        $this->logger = $resourceLoader->getLogger();
110
111        // Optimisation: Use WebRequest::getRawVal() instead of getVal(). We don't
112        // need the slow Language+UTF logic meant for user input here. (f303bb9360)
113
114        // List of modules
115        $modules = $request->getRawVal( 'modules' );
116        $this->modules = $modules ? ResourceLoader::expandModuleNames( $modules ) : [];
117
118        // Various parameters
119        $this->user = $request->getRawVal( 'user' );
120        $this->debug = self::debugFromString( $request->getRawVal( 'debug' ) );
121        $this->only = $request->getRawVal( 'only' );
122        $this->version = $request->getRawVal( 'version' );
123        $this->raw = $request->getFuzzyBool( 'raw' );
124        $this->sourcemap = $request->getFuzzyBool( 'sourcemap' );
125
126        // Image requests
127        $this->image = $request->getRawVal( 'image' );
128        $this->variant = $request->getRawVal( 'variant' );
129        $this->format = $request->getRawVal( 'format' );
130
131        $this->skin = $request->getRawVal( 'skin' );
132        $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
133        $skinnames = $skinFactory->getInstalledSkins();
134
135        if ( !$this->skin || !isset( $skinnames[$this->skin] ) ) {
136            // The 'skin' parameter is required. (Not yet enforced.)
137            // For requests without a known skin specified,
138            // use MediaWiki's 'fallback' skin for skin-specific decisions.
139            $this->skin = self::DEFAULT_SKIN;
140        }
141    }
142
143    /**
144     * @internal For use in ResourceLoader::inDebugMode
145     * @param string|null $debug
146     * @return int
147     */
148    public static function debugFromString( ?string $debug ): int {
149        // The canonical way to enable debug mode is via debug=true
150        // This continues to map to v1 until v2 is ready (T85805).
151        if ( $debug === 'true' || $debug === '1' ) {
152            $ret = self::DEBUG_LEGACY;
153        } elseif ( $debug === '2' ) {
154            $ret = self::DEBUG_MAIN;
155        } else {
156            $ret = self::DEBUG_OFF;
157        }
158
159        return $ret;
160    }
161
162    /**
163     * Return a dummy Context object suitable for passing into
164     * things that don't "really" need a context.
165     *
166     * Use cases:
167     * - Unit tests (deprecated, create empty instance directly or use RLTestCase).
168     *
169     * @return Context
170     */
171    public static function newDummyContext(): Context {
172        // This currently creates a non-empty instance of ResourceLoader (all modules registered),
173        // but that's probably not needed. So once that moves into ServiceWiring, this'll
174        // become more like the EmptyResourceLoader class we have in PHPUnit tests, which
175        // is what this should've had originally. If this turns out to be untrue, change to:
176        // `MediaWikiServices::getInstance()->getResourceLoader()` instead.
177        return new self( new ResourceLoader(
178            MediaWikiServices::getInstance()->getMainConfig(),
179            LoggerFactory::getInstance( 'resourceloader' )
180        ), new FauxRequest( [] ) );
181    }
182
183    public function getResourceLoader(): ResourceLoader {
184        return $this->resourceLoader;
185    }
186
187    public function getRequest(): WebRequest {
188        return $this->request;
189    }
190
191    /**
192     * @deprecated since 1.34 Use Module::getLogger instead
193     * inside module methods. Use ResourceLoader::getLogger elsewhere.
194     * @since 1.27
195     * @return LoggerInterface
196     */
197    public function getLogger() {
198        return $this->logger;
199    }
200
201    public function getModules(): array {
202        return $this->modules;
203    }
204
205    public function getLanguage(): string {
206        if ( $this->language === null ) {
207            // Must be a valid language code after this point (T64849)
208            // Only support uselang values that follow built-in conventions (T102058)
209            $lang = $this->getRequest()->getRawVal( 'lang', '' );
210            '@phan-var string $lang'; // getRawVal does not return null here
211            // Stricter version of RequestContext::sanitizeLangCode()
212            $validBuiltinCode = MediaWikiServices::getInstance()->getLanguageNameUtils()
213                ->isValidBuiltInCode( $lang );
214            if ( !$validBuiltinCode ) {
215                // The 'lang' parameter is required. (Not yet enforced.)
216                // If omitted, localise with the dummy language code.
217                $lang = self::DEFAULT_LANG;
218            }
219            $this->language = $lang;
220        }
221        return $this->language;
222    }
223
224    public function getDirection(): string {
225        if ( $this->direction === null ) {
226            $direction = $this->getRequest()->getRawVal( 'dir' );
227            if ( $direction === 'ltr' || $direction === 'rtl' ) {
228                $this->direction = $direction;
229            } else {
230                // Determine directionality based on user language (T8100)
231                $this->direction = MediaWikiServices::getInstance()->getLanguageFactory()
232                    ->getLanguage( $this->getLanguage() )->getDir();
233            }
234        }
235        return $this->direction;
236    }
237
238    public function getSkin(): string {
239        return $this->skin;
240    }
241
242    /**
243     * @return string|null
244     */
245    public function getUser(): ?string {
246        return $this->user;
247    }
248
249    /**
250     * Get a Message object with context set.  See wfMessage for parameters.
251     *
252     * @since 1.27
253     * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
254     *   or a MessageSpecifier.
255     * @param mixed ...$params
256     * @return Message
257     */
258    public function msg( $key, ...$params ): Message {
259        return wfMessage( $key, ...$params )
260            // Do not use MediaWiki user language from session. Use the provided one instead.
261            ->inLanguage( $this->getLanguage() )
262            // inLanguage() clears the interface flag, so we need re-enable it. (T291601)
263            ->setInterfaceMessageFlag( true )
264            // Use a dummy title because there is no real title for this endpoint, and the cache won't
265            // vary on it anyways.
266            ->page( PageReferenceValue::localReference( NS_SPECIAL, 'Badtitle/ResourceLoaderContext' ) );
267    }
268
269    /**
270     * Get the possibly-cached UserIdentity object for the specified username
271     *
272     * This will be null on most requests,
273     * except for load.php requests that have a 'user' parameter set.
274     *
275     * @since 1.38
276     * @return UserIdentity|null
277     */
278    public function getUserIdentity(): ?UserIdentity {
279        if ( $this->userIdentity === false ) {
280            $username = $this->getUser();
281            if ( $username === null ) {
282                // Anonymous user
283                $this->userIdentity = null;
284            } else {
285                // Use provided username if valid
286                $this->userIdentity = MediaWikiServices::getInstance()
287                    ->getUserFactory()
288                    ->newFromName( $username, UserRigorOptions::RIGOR_VALID );
289            }
290        }
291        return $this->userIdentity;
292    }
293
294    /**
295     * Get the possibly-cached User object for the specified username
296     *
297     * @since 1.25
298     * @return User
299     */
300    public function getUserObj(): User {
301        if ( $this->userObj === null ) {
302            $username = $this->getUser();
303            $userFactory = MediaWikiServices::getInstance()->getUserFactory();
304            if ( $username ) {
305                // Use provided username if valid, fallback to anonymous user
306                $this->userObj = $userFactory->newFromName( $username, UserRigorOptions::RIGOR_VALID );
307            }
308            // Anonymous user
309            $this->userObj ??= $userFactory->newAnonymous();
310        }
311
312        return $this->userObj;
313    }
314
315    public function getDebug(): int {
316        return $this->debug;
317    }
318
319    /**
320     * @return string|null
321     */
322    public function getOnly(): ?string {
323        return $this->only;
324    }
325
326    /**
327     * @see Module::getVersionHash
328     * @see ClientHtml::makeLoad
329     * @return string|null
330     */
331    public function getVersion(): ?string {
332        return $this->version;
333    }
334
335    public function getRaw(): bool {
336        return $this->raw;
337    }
338
339    /**
340     * @since 1.41
341     * @return bool
342     */
343    public function isSourceMap(): bool {
344        return $this->sourcemap;
345    }
346
347    /**
348     * @return string|null
349     */
350    public function getImage(): ?string {
351        return $this->image;
352    }
353
354    /**
355     * @return string|null
356     */
357    public function getVariant(): ?string {
358        return $this->variant;
359    }
360
361    /**
362     * @return string|null
363     */
364    public function getFormat(): ?string {
365        return $this->format;
366    }
367
368    /**
369     * If this is a request for an image, get the Image object.
370     *
371     * @since 1.25
372     * @return Image|false false if a valid object cannot be created
373     */
374    public function getImageObj() {
375        if ( $this->imageObj === null ) {
376            $this->imageObj = false;
377
378            if ( !$this->image ) {
379                return $this->imageObj;
380            }
381
382            $modules = $this->getModules();
383            if ( count( $modules ) !== 1 ) {
384                return $this->imageObj;
385            }
386
387            $module = $this->getResourceLoader()->getModule( $modules[0] );
388            if ( !$module || !$module instanceof ImageModule ) {
389                return $this->imageObj;
390            }
391
392            $image = $module->getImage( $this->image, $this );
393            if ( !$image ) {
394                return $this->imageObj;
395            }
396
397            $this->imageObj = $image;
398        }
399
400        return $this->imageObj;
401    }
402
403    /**
404     * Return the replaced-content mapping callback
405     *
406     * When editing a page that's used to generate the scripts or styles of a
407     * WikiModule, a preview should use the to-be-saved version of
408     * the page rather than the current version in the database. A context
409     * supporting such previews should return a callback to return these
410     * mappings here.
411     *
412     * @since 1.32
413     * @return callable|null Signature is `Content|null func( Title $t )`
414     */
415    public function getContentOverrideCallback() {
416        return null;
417    }
418
419    public function shouldIncludeScripts(): bool {
420        return $this->getOnly() === null || $this->getOnly() === 'scripts';
421    }
422
423    public function shouldIncludeStyles(): bool {
424        return $this->getOnly() === null || $this->getOnly() === 'styles';
425    }
426
427    public function shouldIncludeMessages(): bool {
428        return $this->getOnly() === null;
429    }
430
431    /**
432     * All factors that uniquely identify this request, except 'modules'.
433     *
434     * The list of modules is excluded here for legacy reasons as most callers already
435     * split up handling of individual modules. Including it here would massively fragment
436     * the cache and decrease its usefulness.
437     *
438     * E.g. Used by RequestFileCache to form a cache key for storing the response output.
439     *
440     * @return string
441     */
442    public function getHash(): string {
443        if ( $this->hash === null ) {
444            $this->hash = implode( '|', [
445                // Module content vary
446                $this->getLanguage(),
447                $this->getSkin(),
448                (string)$this->getDebug(),
449                $this->getUser() ?? '',
450                // Request vary
451                $this->getOnly() ?? '',
452                $this->getVersion() ?? '',
453                (string)$this->getRaw(),
454                $this->getImage() ?? '',
455                $this->getVariant() ?? '',
456                $this->getFormat() ?? '',
457            ] );
458        }
459        return $this->hash;
460    }
461
462    /**
463     * Get the request base parameters, omitting any defaults.
464     *
465     * @internal For use by StartUpModule only
466     * @return string[]
467     */
468    public function getReqBase(): array {
469        $reqBase = [];
470        $lang = $this->getLanguage();
471        if ( $lang !== self::DEFAULT_LANG ) {
472            $reqBase['lang'] = $lang;
473        }
474        $skin = $this->getSkin();
475        if ( $skin !== self::DEFAULT_SKIN ) {
476            $reqBase['skin'] = $skin;
477        }
478        $debug = $this->getDebug();
479        if ( $debug !== self::DEBUG_OFF ) {
480            $reqBase['debug'] = strval( $debug );
481        }
482        return $reqBase;
483    }
484
485    /**
486     * Wrapper around json_encode that avoids needless escapes,
487     * and pretty-prints in debug mode.
488     *
489     * @since 1.34
490     * @param mixed $data
491     * @return string|false JSON string, false on error
492     */
493    public function encodeJson( $data ) {
494        // Keep output as small as possible by disabling needless escape modes
495        // that PHP uses by default.
496        // However, while most module scripts are only served on HTTP responses
497        // for JavaScript, some modules can also be embedded in the HTML as inline
498        // scripts. This, and the fact that we sometimes need to export strings
499        // containing user-generated content and labels that may genuinely contain
500        // a sequences like "</script>", we need to encode either '/' or '<'.
501        // By default PHP escapes '/'. Let's escape '<' instead which is less common
502        // and allows URLs to mostly remain readable.
503        $jsonFlags = JSON_UNESCAPED_SLASHES |
504            JSON_UNESCAPED_UNICODE |
505            JSON_PARTIAL_OUTPUT_ON_ERROR |
506            JSON_HEX_TAG |
507            JSON_HEX_AMP;
508        if ( $this->getDebug() ) {
509            $jsonFlags |= JSON_PRETTY_PRINT;
510        }
511        $json = json_encode( $data, $jsonFlags );
512        if ( json_last_error() !== JSON_ERROR_NONE ) {
513            trigger_error( __METHOD__ . ' partially failed: ' . json_last_error_msg(), E_USER_WARNING );
514        }
515        return $json;
516    }
517}