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 |
73.25 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
5 | |||
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 Psr\Log\LoggerInterface; |
36 | use Wikimedia\Message\MessageParam; |
37 | use 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 | */ |
46 | class 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 | } |