Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
84.56% |
115 / 136 |
|
83.33% |
25 / 30 |
CRAP | |
0.00% |
0 / 1 |
Context | |
84.56% |
115 / 136 |
|
83.33% |
25 / 30 |
71.82 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
4 | |||
debugFromString | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
4.59 | |||
newDummyContext | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getResourceLoader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getModules | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLanguage | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getDirection | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getSkin | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
msg | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getUserIdentity | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getUserObj | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getDebug | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getVersion | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRaw | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSourceMap | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getImage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getVariant | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFormat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getImageObj | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
56 | |||
getContentOverrideCallback | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
shouldIncludeScripts | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
shouldIncludeStyles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
shouldIncludeMessages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHash | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 | |||
getReqBase | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
4.10 | |||
encodeJson | |
90.91% |
10 / 11 |
|
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 | |
23 | namespace MediaWiki\ResourceLoader; |
24 | |
25 | use MediaWiki\Logger\LoggerFactory; |
26 | use MediaWiki\MediaWikiServices; |
27 | use MediaWiki\Message\Message; |
28 | use MediaWiki\Page\PageReferenceValue; |
29 | use MediaWiki\Request\FauxRequest; |
30 | use MediaWiki\Request\WebRequest; |
31 | use MediaWiki\User\User; |
32 | use MediaWiki\User\UserIdentity; |
33 | use MediaWiki\User\UserRigorOptions; |
34 | use MessageLocalizer; |
35 | use MessageSpecifier; |
36 | use 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 | */ |
45 | class 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 | } |