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