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