[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Abstraction for resource loader modules. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @author Trevor Parscal 22 * @author Roan Kattouw 23 */ 24 25 /** 26 * Abstraction for resource loader modules, with name registration and maxage functionality. 27 */ 28 abstract class ResourceLoaderModule { 29 # Type of resource 30 const TYPE_SCRIPTS = 'scripts'; 31 const TYPE_STYLES = 'styles'; 32 const TYPE_MESSAGES = 'messages'; 33 const TYPE_COMBINED = 'combined'; 34 35 # sitewide core module like a skin file or jQuery component 36 const ORIGIN_CORE_SITEWIDE = 1; 37 38 # per-user module generated by the software 39 const ORIGIN_CORE_INDIVIDUAL = 2; 40 41 # sitewide module generated from user-editable files, like MediaWiki:Common.js, or 42 # modules accessible to multiple users, such as those generated by the Gadgets extension. 43 const ORIGIN_USER_SITEWIDE = 3; 44 45 # per-user module generated from user-editable files, like User:Me/vector.js 46 const ORIGIN_USER_INDIVIDUAL = 4; 47 48 # an access constant; make sure this is kept as the largest number in this group 49 const ORIGIN_ALL = 10; 50 51 # script and style modules form a hierarchy of trustworthiness, with core modules like 52 # skins and jQuery as most trustworthy, and user scripts as least trustworthy. We can 53 # limit the types of scripts and styles we allow to load on, say, sensitive special 54 # pages like Special:UserLogin and Special:Preferences 55 protected $origin = self::ORIGIN_CORE_SITEWIDE; 56 57 /* Protected Members */ 58 59 protected $name = null; 60 protected $targets = array( 'desktop' ); 61 62 // In-object cache for file dependencies 63 protected $fileDeps = array(); 64 // In-object cache for message blob mtime 65 protected $msgBlobMtime = array(); 66 67 /** 68 * @var Config 69 */ 70 protected $config; 71 72 /* Methods */ 73 74 /** 75 * Get this module's name. This is set when the module is registered 76 * with ResourceLoader::register() 77 * 78 * @return string|null Name (string) or null if no name was set 79 */ 80 public function getName() { 81 return $this->name; 82 } 83 84 /** 85 * Set this module's name. This is called by ResourceLoader::register() 86 * when registering the module. Other code should not call this. 87 * 88 * @param string $name Name 89 */ 90 public function setName( $name ) { 91 $this->name = $name; 92 } 93 94 /** 95 * Get this module's origin. This is set when the module is registered 96 * with ResourceLoader::register() 97 * 98 * @return int ResourceLoaderModule class constant, the subclass default 99 * if not set manually 100 */ 101 public function getOrigin() { 102 return $this->origin; 103 } 104 105 /** 106 * Set this module's origin. This is called by ResourceLoader::register() 107 * when registering the module. Other code should not call this. 108 * 109 * @param int $origin Origin 110 */ 111 public function setOrigin( $origin ) { 112 $this->origin = $origin; 113 } 114 115 /** 116 * @param ResourceLoaderContext $context 117 * @return bool 118 */ 119 public function getFlip( $context ) { 120 global $wgContLang; 121 122 return $wgContLang->getDir() !== $context->getDirection(); 123 } 124 125 /** 126 * Get all JS for this module for a given language and skin. 127 * Includes all relevant JS except loader scripts. 128 * 129 * @param ResourceLoaderContext $context 130 * @return string JavaScript code 131 */ 132 public function getScript( ResourceLoaderContext $context ) { 133 // Stub, override expected 134 return ''; 135 } 136 137 /** 138 * @return Config 139 * @since 1.24 140 */ 141 public function getConfig() { 142 if ( $this->config === null ) { 143 // Ugh, fall back to default 144 $this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); 145 } 146 147 return $this->config; 148 } 149 150 /** 151 * @param Config $config 152 * @since 1.24 153 */ 154 public function setConfig( Config $config ) { 155 $this->config = $config; 156 } 157 158 /** 159 * Get the URL or URLs to load for this module's JS in debug mode. 160 * The default behavior is to return a load.php?only=scripts URL for 161 * the module, but file-based modules will want to override this to 162 * load the files directly. 163 * 164 * This function is called only when 1) we're in debug mode, 2) there 165 * is no only= parameter and 3) supportsURLLoading() returns true. 166 * #2 is important to prevent an infinite loop, therefore this function 167 * MUST return either an only= URL or a non-load.php URL. 168 * 169 * @param ResourceLoaderContext $context 170 * @return array Array of URLs 171 */ 172 public function getScriptURLsForDebug( ResourceLoaderContext $context ) { 173 $resourceLoader = $context->getResourceLoader(); 174 $derivative = new DerivativeResourceLoaderContext( $context ); 175 $derivative->setModules( array( $this->getName() ) ); 176 $derivative->setOnly( 'scripts' ); 177 $derivative->setDebug( true ); 178 179 $url = $resourceLoader->createLoaderURL( 180 $this->getSource(), 181 $derivative 182 ); 183 184 return array( $url ); 185 } 186 187 /** 188 * Whether this module supports URL loading. If this function returns false, 189 * getScript() will be used even in cases (debug mode, no only param) where 190 * getScriptURLsForDebug() would normally be used instead. 191 * @return bool 192 */ 193 public function supportsURLLoading() { 194 return true; 195 } 196 197 /** 198 * Get all CSS for this module for a given skin. 199 * 200 * @param ResourceLoaderContext $context 201 * @return array List of CSS strings or array of CSS strings keyed by media type. 202 * like array( 'screen' => '.foo { width: 0 }' ); 203 * or array( 'screen' => array( '.foo { width: 0 }' ) ); 204 */ 205 public function getStyles( ResourceLoaderContext $context ) { 206 // Stub, override expected 207 return array(); 208 } 209 210 /** 211 * Get the URL or URLs to load for this module's CSS in debug mode. 212 * The default behavior is to return a load.php?only=styles URL for 213 * the module, but file-based modules will want to override this to 214 * load the files directly. See also getScriptURLsForDebug() 215 * 216 * @param ResourceLoaderContext $context 217 * @return array Array( mediaType => array( URL1, URL2, ... ), ... ) 218 */ 219 public function getStyleURLsForDebug( ResourceLoaderContext $context ) { 220 $resourceLoader = $context->getResourceLoader(); 221 $derivative = new DerivativeResourceLoaderContext( $context ); 222 $derivative->setModules( array( $this->getName() ) ); 223 $derivative->setOnly( 'styles' ); 224 $derivative->setDebug( true ); 225 226 $url = $resourceLoader->createLoaderURL( 227 $this->getSource(), 228 $derivative 229 ); 230 231 return array( 'all' => array( $url ) ); 232 } 233 234 /** 235 * Get the messages needed for this module. 236 * 237 * To get a JSON blob with messages, use MessageBlobStore::get() 238 * 239 * @return array List of message keys. Keys may occur more than once 240 */ 241 public function getMessages() { 242 // Stub, override expected 243 return array(); 244 } 245 246 /** 247 * Get the group this module is in. 248 * 249 * @return string Group name 250 */ 251 public function getGroup() { 252 // Stub, override expected 253 return null; 254 } 255 256 /** 257 * Get the origin of this module. Should only be overridden for foreign modules. 258 * 259 * @return string Origin name, 'local' for local modules 260 */ 261 public function getSource() { 262 // Stub, override expected 263 return 'local'; 264 } 265 266 /** 267 * Where on the HTML page should this module's JS be loaded? 268 * - 'top': in the "<head>" 269 * - 'bottom': at the bottom of the "<body>" 270 * 271 * @return string 272 */ 273 public function getPosition() { 274 return 'bottom'; 275 } 276 277 /** 278 * Whether this module's JS expects to work without the client-side ResourceLoader module. 279 * Returning true from this function will prevent mw.loader.state() call from being 280 * appended to the bottom of the script. 281 * 282 * @return bool 283 */ 284 public function isRaw() { 285 return false; 286 } 287 288 /** 289 * Get the loader JS for this module, if set. 290 * 291 * @return mixed JavaScript loader code as a string or boolean false if no custom loader set 292 */ 293 public function getLoaderScript() { 294 // Stub, override expected 295 return false; 296 } 297 298 /** 299 * Get a list of modules this module depends on. 300 * 301 * Dependency information is taken into account when loading a module 302 * on the client side. 303 * 304 * To add dependencies dynamically on the client side, use a custom 305 * loader script, see getLoaderScript() 306 * @return array List of module names as strings 307 */ 308 public function getDependencies() { 309 // Stub, override expected 310 return array(); 311 } 312 313 /** 314 * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile'] 315 * 316 * @return array Array of strings 317 */ 318 public function getTargets() { 319 return $this->targets; 320 } 321 322 /** 323 * Get the skip function. 324 * 325 * Modules that provide fallback functionality can provide a "skip function". This 326 * function, if provided, will be passed along to the module registry on the client. 327 * When this module is loaded (either directly or as a dependency of another module), 328 * then this function is executed first. If the function returns true, the module will 329 * instantly be considered "ready" without requesting the associated module resources. 330 * 331 * The value returned here must be valid javascript for execution in a private function. 332 * It must not contain the "function () {" and "}" wrapper though. 333 * 334 * @return string|null A JavaScript function body returning a boolean value, or null 335 */ 336 public function getSkipFunction() { 337 return null; 338 } 339 340 /** 341 * Get the files this module depends on indirectly for a given skin. 342 * Currently these are only image files referenced by the module's CSS. 343 * 344 * @param string $skin Skin name 345 * @return array List of files 346 */ 347 public function getFileDependencies( $skin ) { 348 // Try in-object cache first 349 if ( isset( $this->fileDeps[$skin] ) ) { 350 return $this->fileDeps[$skin]; 351 } 352 353 $dbr = wfGetDB( DB_SLAVE ); 354 $deps = $dbr->selectField( 'module_deps', 'md_deps', array( 355 'md_module' => $this->getName(), 356 'md_skin' => $skin, 357 ), __METHOD__ 358 ); 359 if ( !is_null( $deps ) ) { 360 $this->fileDeps[$skin] = (array)FormatJson::decode( $deps, true ); 361 } else { 362 $this->fileDeps[$skin] = array(); 363 } 364 return $this->fileDeps[$skin]; 365 } 366 367 /** 368 * Set preloaded file dependency information. Used so we can load this 369 * information for all modules at once. 370 * @param string $skin Skin name 371 * @param array $deps Array of file names 372 */ 373 public function setFileDependencies( $skin, $deps ) { 374 $this->fileDeps[$skin] = $deps; 375 } 376 377 /** 378 * Get the last modification timestamp of the message blob for this 379 * module in a given language. 380 * @param string $lang Language code 381 * @return int UNIX timestamp, or 0 if the module doesn't have messages 382 */ 383 public function getMsgBlobMtime( $lang ) { 384 if ( !isset( $this->msgBlobMtime[$lang] ) ) { 385 if ( !count( $this->getMessages() ) ) { 386 return 0; 387 } 388 389 $dbr = wfGetDB( DB_SLAVE ); 390 $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array( 391 'mr_resource' => $this->getName(), 392 'mr_lang' => $lang 393 ), __METHOD__ 394 ); 395 // If no blob was found, but the module does have messages, that means we need 396 // to regenerate it. Return NOW 397 if ( $msgBlobMtime === false ) { 398 $msgBlobMtime = wfTimestampNow(); 399 } 400 $this->msgBlobMtime[$lang] = wfTimestamp( TS_UNIX, $msgBlobMtime ); 401 } 402 return $this->msgBlobMtime[$lang]; 403 } 404 405 /** 406 * Set a preloaded message blob last modification timestamp. Used so we 407 * can load this information for all modules at once. 408 * @param string $lang Language code 409 * @param int $mtime UNIX timestamp or 0 if there is no such blob 410 */ 411 public function setMsgBlobMtime( $lang, $mtime ) { 412 $this->msgBlobMtime[$lang] = $mtime; 413 } 414 415 /* Abstract Methods */ 416 417 /** 418 * Get this module's last modification timestamp for a given 419 * combination of language, skin and debug mode flag. This is typically 420 * the highest of each of the relevant components' modification 421 * timestamps. Whenever anything happens that changes the module's 422 * contents for these parameters, the mtime should increase. 423 * 424 * NOTE: The mtime of the module's messages is NOT automatically included. 425 * If you want this to happen, you'll need to call getMsgBlobMtime() 426 * yourself and take its result into consideration. 427 * 428 * NOTE: The mtime of the module's hash is NOT automatically included. 429 * If your module provides a getModifiedHash() method, you'll need to call getHashMtime() 430 * yourself and take its result into consideration. 431 * 432 * @param ResourceLoaderContext $context Context object 433 * @return int UNIX timestamp 434 */ 435 public function getModifiedTime( ResourceLoaderContext $context ) { 436 // 0 would mean now 437 return 1; 438 } 439 440 /** 441 * Helper method for calculating when the module's hash (if it has one) changed. 442 * 443 * @param ResourceLoaderContext $context 444 * @return int UNIX timestamp or 0 if no hash was provided 445 * by getModifiedHash() 446 */ 447 public function getHashMtime( ResourceLoaderContext $context ) { 448 $hash = $this->getModifiedHash( $context ); 449 if ( !is_string( $hash ) ) { 450 return 0; 451 } 452 453 $cache = wfGetCache( CACHE_ANYTHING ); 454 $key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName(), $hash ); 455 456 $data = $cache->get( $key ); 457 if ( is_array( $data ) && $data['hash'] === $hash ) { 458 // Hash is still the same, re-use the timestamp of when we first saw this hash. 459 return $data['timestamp']; 460 } 461 462 $timestamp = wfTimestamp(); 463 $cache->set( $key, array( 464 'hash' => $hash, 465 'timestamp' => $timestamp, 466 ) ); 467 468 return $timestamp; 469 } 470 471 /** 472 * Get the hash for whatever this module may contain. 473 * 474 * This is the method subclasses should implement if they want to make 475 * use of getHashMTime() inside getModifiedTime(). 476 * 477 * @param ResourceLoaderContext $context 478 * @return string|null Hash 479 */ 480 public function getModifiedHash( ResourceLoaderContext $context ) { 481 return null; 482 } 483 484 /** 485 * Helper method for calculating when this module's definition summary was last changed. 486 * 487 * @since 1.23 488 * 489 * @param ResourceLoaderContext $context 490 * @return int UNIX timestamp or 0 if no definition summary was provided 491 * by getDefinitionSummary() 492 */ 493 public function getDefinitionMtime( ResourceLoaderContext $context ) { 494 wfProfileIn( __METHOD__ ); 495 $summary = $this->getDefinitionSummary( $context ); 496 if ( $summary === null ) { 497 wfProfileOut( __METHOD__ ); 498 return 0; 499 } 500 501 $hash = md5( json_encode( $summary ) ); 502 503 $cache = wfGetCache( CACHE_ANYTHING ); 504 505 // Embed the hash itself in the cache key. This allows for a few nifty things: 506 // - During deployment, servers with old and new versions of the code communicating 507 // with the same memcached will not override the same key repeatedly increasing 508 // the timestamp. 509 // - In case of the definition changing and then changing back in a short period of time 510 // (e.g. in case of a revert or a corrupt server) the old timestamp and client-side cache 511 // url will be re-used. 512 // - If different context-combinations (e.g. same skin, same language or some combination 513 // thereof) result in the same definition, they will use the same hash and timestamp. 514 $key = wfMemcKey( 'resourceloader', 'moduledefinition', $this->getName(), $hash ); 515 516 $data = $cache->get( $key ); 517 if ( is_int( $data ) && $data > 0 ) { 518 // We've seen this hash before, re-use the timestamp of when we first saw it. 519 wfProfileOut( __METHOD__ ); 520 return $data; 521 } 522 523 wfDebugLog( 'resourceloader', __METHOD__ . ": New definition hash for module " 524 . "{$this->getName()} in context {$context->getHash()}: $hash." ); 525 526 $timestamp = time(); 527 $cache->set( $key, $timestamp ); 528 529 wfProfileOut( __METHOD__ ); 530 return $timestamp; 531 } 532 533 /** 534 * Get the definition summary for this module. 535 * 536 * This is the method subclasses should implement if they want to make 537 * use of getDefinitionMTime() inside getModifiedTime(). 538 * 539 * Return an array containing values from all significant properties of this 540 * module's definition. Be sure to include things that are explicitly ordered, 541 * in their actaul order (bug 37812). 542 * 543 * Avoid including things that are insiginificant (e.g. order of message 544 * keys is insignificant and should be sorted to avoid unnecessary cache 545 * invalidation). 546 * 547 * Avoid including things already considered by other methods inside your 548 * getModifiedTime(), such as file mtime timestamps. 549 * 550 * Serialisation is done using json_encode, which means object state is not 551 * taken into account when building the hash. This data structure must only 552 * contain arrays and scalars as values (avoid object instances) which means 553 * it requires abstraction. 554 * 555 * @since 1.23 556 * 557 * @param ResourceLoaderContext $context 558 * @return array|null 559 */ 560 public function getDefinitionSummary( ResourceLoaderContext $context ) { 561 return array( 562 'class' => get_class( $this ), 563 ); 564 } 565 566 /** 567 * Check whether this module is known to be empty. If a child class 568 * has an easy and cheap way to determine that this module is 569 * definitely going to be empty, it should override this method to 570 * return true in that case. Callers may optimize the request for this 571 * module away if this function returns true. 572 * @param ResourceLoaderContext $context 573 * @return bool 574 */ 575 public function isKnownEmpty( ResourceLoaderContext $context ) { 576 return false; 577 } 578 579 /** @var JSParser Lazy-initialized; use self::javaScriptParser() */ 580 private static $jsParser; 581 private static $parseCacheVersion = 1; 582 583 /** 584 * Validate a given script file; if valid returns the original source. 585 * If invalid, returns replacement JS source that throws an exception. 586 * 587 * @param string $fileName 588 * @param string $contents 589 * @return string JS with the original, or a replacement error 590 */ 591 protected function validateScriptFile( $fileName, $contents ) { 592 if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) { 593 // Try for cache hit 594 // Use CACHE_ANYTHING since filtering is very slow compared to DB queries 595 $key = wfMemcKey( 'resourceloader', 'jsparse', self::$parseCacheVersion, md5( $contents ) ); 596 $cache = wfGetCache( CACHE_ANYTHING ); 597 $cacheEntry = $cache->get( $key ); 598 if ( is_string( $cacheEntry ) ) { 599 return $cacheEntry; 600 } 601 602 $parser = self::javaScriptParser(); 603 try { 604 $parser->parse( $contents, $fileName, 1 ); 605 $result = $contents; 606 } catch ( Exception $e ) { 607 // We'll save this to cache to avoid having to validate broken JS over and over... 608 $err = $e->getMessage(); 609 $result = "throw new Error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");"; 610 } 611 612 $cache->set( $key, $result ); 613 return $result; 614 } else { 615 return $contents; 616 } 617 } 618 619 /** 620 * @return JSParser 621 */ 622 protected static function javaScriptParser() { 623 if ( !self::$jsParser ) { 624 self::$jsParser = new JSParser(); 625 } 626 return self::$jsParser; 627 } 628 629 /** 630 * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist 631 * but returns 1 instead. 632 * @param string $filename File name 633 * @return int UNIX timestamp, or 1 if the file doesn't exist 634 */ 635 protected static function safeFilemtime( $filename ) { 636 if ( file_exists( $filename ) ) { 637 return filemtime( $filename ); 638 } else { 639 // We only ever map this function on an array if we're gonna call max() after, 640 // so return our standard minimum timestamps here. This is 1, not 0, because 641 // wfTimestamp(0) == NOW 642 return 1; 643 } 644 } 645 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |