Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
85.50% |
112 / 131 |
|
86.67% |
26 / 30 |
CRAP | |
0.00% |
0 / 1 |
| Context | |
85.50% |
112 / 131 |
|
86.67% |
26 / 30 |
68.26 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
5 | |||
| debugFromString | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| 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% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| 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 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | * @author Trevor Parscal |
| 6 | * @author Roan Kattouw |
| 7 | */ |
| 8 | |
| 9 | namespace MediaWiki\ResourceLoader; |
| 10 | |
| 11 | use MediaWiki\Language\MessageLocalizer; |
| 12 | use MediaWiki\Logger\LoggerFactory; |
| 13 | use MediaWiki\MediaWikiServices; |
| 14 | use MediaWiki\Message\Message; |
| 15 | use MediaWiki\Page\PageReferenceValue; |
| 16 | use MediaWiki\Request\FauxRequest; |
| 17 | use MediaWiki\Request\WebRequest; |
| 18 | use MediaWiki\User\User; |
| 19 | use MediaWiki\User\UserIdentity; |
| 20 | use MediaWiki\User\UserRigorOptions; |
| 21 | use Psr\Log\LoggerInterface; |
| 22 | use Wikimedia\Message\MessageParam; |
| 23 | use 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 | */ |
| 32 | class 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 | } |