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