[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Base class for resource loading system. 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 Roan Kattouw 22 * @author Trevor Parscal 23 */ 24 25 /** 26 * Dynamic JavaScript and CSS resource loading system. 27 * 28 * Most of the documentation is on the MediaWiki documentation wiki starting at: 29 * https://www.mediawiki.org/wiki/ResourceLoader 30 */ 31 class ResourceLoader { 32 /** @var int */ 33 protected static $filterCacheVersion = 7; 34 35 /** @var bool */ 36 protected static $debugMode = null; 37 38 /** @var array Module name/ResourceLoaderModule object pairs */ 39 protected $modules = array(); 40 41 /** @var array Associative array mapping module name to info associative array */ 42 protected $moduleInfos = array(); 43 44 /** @var Config $config */ 45 private $config; 46 47 /** 48 * @var array Associative array mapping framework ids to a list of names of test suite modules 49 * like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) 50 */ 51 protected $testModuleNames = array(); 52 53 /** @var array E.g. array( 'source-id' => 'http://.../load.php' ) */ 54 protected $sources = array(); 55 56 /** @var bool */ 57 protected $hasErrors = false; 58 59 /** 60 * Load information stored in the database about modules. 61 * 62 * This method grabs modules dependencies from the database and updates modules 63 * objects. 64 * 65 * This is not inside the module code because it is much faster to 66 * request all of the information at once than it is to have each module 67 * requests its own information. This sacrifice of modularity yields a substantial 68 * performance improvement. 69 * 70 * @param array $modules List of module names to preload information for 71 * @param ResourceLoaderContext $context Context to load the information within 72 */ 73 public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) { 74 if ( !count( $modules ) ) { 75 // Or else Database*::select() will explode, plus it's cheaper! 76 return; 77 } 78 $dbr = wfGetDB( DB_SLAVE ); 79 $skin = $context->getSkin(); 80 $lang = $context->getLanguage(); 81 82 // Get file dependency information 83 $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array( 84 'md_module' => $modules, 85 'md_skin' => $skin 86 ), __METHOD__ 87 ); 88 89 // Set modules' dependencies 90 $modulesWithDeps = array(); 91 foreach ( $res as $row ) { 92 $module = $this->getModule( $row->md_module ); 93 if ( $module ) { 94 $module->setFileDependencies( $skin, FormatJson::decode( $row->md_deps, true ) ); 95 $modulesWithDeps[] = $row->md_module; 96 } 97 } 98 99 // Register the absence of a dependency row too 100 foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) { 101 $module = $this->getModule( $name ); 102 if ( $module ) { 103 $this->getModule( $name )->setFileDependencies( $skin, array() ); 104 } 105 } 106 107 // Get message blob mtimes. Only do this for modules with messages 108 $modulesWithMessages = array(); 109 foreach ( $modules as $name ) { 110 $module = $this->getModule( $name ); 111 if ( $module && count( $module->getMessages() ) ) { 112 $modulesWithMessages[] = $name; 113 } 114 } 115 $modulesWithoutMessages = array_flip( $modules ); // Will be trimmed down by the loop below 116 if ( count( $modulesWithMessages ) ) { 117 $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array( 118 'mr_resource' => $modulesWithMessages, 119 'mr_lang' => $lang 120 ), __METHOD__ 121 ); 122 foreach ( $res as $row ) { 123 $module = $this->getModule( $row->mr_resource ); 124 if ( $module ) { 125 $module->setMsgBlobMtime( $lang, wfTimestamp( TS_UNIX, $row->mr_timestamp ) ); 126 unset( $modulesWithoutMessages[$row->mr_resource] ); 127 } 128 } 129 } 130 foreach ( array_keys( $modulesWithoutMessages ) as $name ) { 131 $module = $this->getModule( $name ); 132 if ( $module ) { 133 $module->setMsgBlobMtime( $lang, 0 ); 134 } 135 } 136 } 137 138 /** 139 * Run JavaScript or CSS data through a filter, caching the filtered result for future calls. 140 * 141 * Available filters are: 142 * 143 * - minify-js \see JavaScriptMinifier::minify 144 * - minify-css \see CSSMin::minify 145 * 146 * If $data is empty, only contains whitespace or the filter was unknown, 147 * $data is returned unmodified. 148 * 149 * @param string $filter Name of filter to run 150 * @param string $data Text to filter, such as JavaScript or CSS text 151 * @param string $cacheReport Whether to include the cache key report 152 * @return string Filtered data, or a comment containing an error message 153 */ 154 public function filter( $filter, $data, $cacheReport = true ) { 155 wfProfileIn( __METHOD__ ); 156 157 // For empty/whitespace-only data or for unknown filters, don't perform 158 // any caching or processing 159 if ( trim( $data ) === '' || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) { 160 wfProfileOut( __METHOD__ ); 161 return $data; 162 } 163 164 // Try for cache hit 165 // Use CACHE_ANYTHING since filtering is very slow compared to DB queries 166 $key = wfMemcKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) ); 167 $cache = wfGetCache( CACHE_ANYTHING ); 168 $cacheEntry = $cache->get( $key ); 169 if ( is_string( $cacheEntry ) ) { 170 wfIncrStats( "rl-$filter-cache-hits" ); 171 wfProfileOut( __METHOD__ ); 172 return $cacheEntry; 173 } 174 175 $result = ''; 176 // Run the filter - we've already verified one of these will work 177 try { 178 wfIncrStats( "rl-$filter-cache-misses" ); 179 switch ( $filter ) { 180 case 'minify-js': 181 $result = JavaScriptMinifier::minify( $data, 182 $this->config->get( 'ResourceLoaderMinifierStatementsOnOwnLine' ), 183 $this->config->get( 'ResourceLoaderMinifierMaxLineLength' ) 184 ); 185 if ( $cacheReport ) { 186 $result .= "\n/* cache key: $key */"; 187 } 188 break; 189 case 'minify-css': 190 $result = CSSMin::minify( $data ); 191 if ( $cacheReport ) { 192 $result .= "\n/* cache key: $key */"; 193 } 194 break; 195 } 196 197 // Save filtered text to Memcached 198 $cache->set( $key, $result ); 199 } catch ( Exception $e ) { 200 MWExceptionHandler::logException( $e ); 201 wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" ); 202 $this->hasErrors = true; 203 // Return exception as a comment 204 $result = self::formatException( $e ); 205 } 206 207 wfProfileOut( __METHOD__ ); 208 209 return $result; 210 } 211 212 /* Methods */ 213 214 /** 215 * Register core modules and runs registration hooks. 216 * @param Config|null $config 217 */ 218 public function __construct( Config $config = null ) { 219 global $IP; 220 221 wfProfileIn( __METHOD__ ); 222 223 if ( $config === null ) { 224 wfDebug( __METHOD__ . ' was called without providing a Config instance' ); 225 $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); 226 } 227 228 $this->config = $config; 229 230 // Add 'local' source first 231 $this->addSource( 'local', wfScript( 'load' ) ); 232 233 // Add other sources 234 $this->addSource( $config->get( 'ResourceLoaderSources' ) ); 235 236 // Register core modules 237 $this->register( include "$IP/resources/Resources.php" ); 238 // Register extension modules 239 wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) ); 240 $this->register( $config->get( 'ResourceModules' ) ); 241 242 if ( $config->get( 'EnableJavaScriptTest' ) === true ) { 243 $this->registerTestModules(); 244 } 245 246 wfProfileOut( __METHOD__ ); 247 } 248 249 /** 250 * @return Config 251 */ 252 public function getConfig() { 253 return $this->config; 254 } 255 256 /** 257 * Register a module with the ResourceLoader system. 258 * 259 * @param mixed $name Name of module as a string or List of name/object pairs as an array 260 * @param array $info Module info array. For backwards compatibility with 1.17alpha, 261 * this may also be a ResourceLoaderModule object. Optional when using 262 * multiple-registration calling style. 263 * @throws MWException If a duplicate module registration is attempted 264 * @throws MWException If a module name contains illegal characters (pipes or commas) 265 * @throws MWException If something other than a ResourceLoaderModule is being registered 266 * @return bool False if there were any errors, in which case one or more modules were 267 * not registered 268 */ 269 public function register( $name, $info = null ) { 270 wfProfileIn( __METHOD__ ); 271 272 // Allow multiple modules to be registered in one call 273 $registrations = is_array( $name ) ? $name : array( $name => $info ); 274 foreach ( $registrations as $name => $info ) { 275 // Disallow duplicate registrations 276 if ( isset( $this->moduleInfos[$name] ) ) { 277 wfProfileOut( __METHOD__ ); 278 // A module has already been registered by this name 279 throw new MWException( 280 'ResourceLoader duplicate registration error. ' . 281 'Another module has already been registered as ' . $name 282 ); 283 } 284 285 // Check $name for validity 286 if ( !self::isValidModuleName( $name ) ) { 287 wfProfileOut( __METHOD__ ); 288 throw new MWException( "ResourceLoader module name '$name' is invalid, " 289 . "see ResourceLoader::isValidModuleName()" ); 290 } 291 292 // Attach module 293 if ( $info instanceof ResourceLoaderModule ) { 294 $this->moduleInfos[$name] = array( 'object' => $info ); 295 $info->setName( $name ); 296 $this->modules[$name] = $info; 297 } elseif ( is_array( $info ) ) { 298 // New calling convention 299 $this->moduleInfos[$name] = $info; 300 } else { 301 wfProfileOut( __METHOD__ ); 302 throw new MWException( 303 'ResourceLoader module info type error for module \'' . $name . 304 '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')' 305 ); 306 } 307 308 // Last-minute changes 309 310 // Apply custom skin-defined styles to existing modules. 311 if ( $this->isFileModule( $name ) ) { 312 foreach ( $this->config->get( 'ResourceModuleSkinStyles' ) as $skinName => $skinStyles ) { 313 // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles. 314 if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) { 315 continue; 316 } 317 318 // If $name is preceded with a '+', the defined style files will be added to 'default' 319 // skinStyles, otherwise 'default' will be ignored as it normally would be. 320 if ( isset( $skinStyles[$name] ) ) { 321 $paths = (array)$skinStyles[$name]; 322 $styleFiles = array(); 323 } elseif ( isset( $skinStyles['+' . $name] ) ) { 324 $paths = (array)$skinStyles['+' . $name]; 325 $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ? 326 $this->moduleInfos[$name]['skinStyles']['default'] : 327 array(); 328 } else { 329 continue; 330 } 331 332 // Add new file paths, remapping them to refer to our directories and not use settings 333 // from the module we're modifying. These can come from the base definition or be defined 334 // for each module. 335 list( $localBasePath, $remoteBasePath ) = 336 ResourceLoaderFileModule::extractBasePaths( $skinStyles ); 337 list( $localBasePath, $remoteBasePath ) = 338 ResourceLoaderFileModule::extractBasePaths( $paths, $localBasePath, $remoteBasePath ); 339 340 foreach ( $paths as $path ) { 341 $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath ); 342 } 343 344 $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles; 345 } 346 } 347 } 348 349 wfProfileOut( __METHOD__ ); 350 } 351 352 /** 353 */ 354 public function registerTestModules() { 355 global $IP; 356 357 if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) { 358 throw new MWException( 'Attempt to register JavaScript test modules ' 359 . 'but <code>$wgEnableJavaScriptTest</code> is false. ' 360 . 'Edit your <code>LocalSettings.php</code> to enable it.' ); 361 } 362 363 wfProfileIn( __METHOD__ ); 364 365 // Get core test suites 366 $testModules = array(); 367 $testModules['qunit'] = array(); 368 // Get other test suites (e.g. from extensions) 369 wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) ); 370 371 // Add the testrunner (which configures QUnit) to the dependencies. 372 // Since it must be ready before any of the test suites are executed. 373 foreach ( $testModules['qunit'] as &$module ) { 374 // Make sure all test modules are top-loading so that when QUnit starts 375 // on document-ready, it will run once and finish. If some tests arrive 376 // later (possibly after QUnit has already finished) they will be ignored. 377 $module['position'] = 'top'; 378 $module['dependencies'][] = 'test.mediawiki.qunit.testrunner'; 379 } 380 381 $testModules['qunit'] = 382 ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit']; 383 384 foreach ( $testModules as $id => $names ) { 385 // Register test modules 386 $this->register( $testModules[$id] ); 387 388 // Keep track of their names so that they can be loaded together 389 $this->testModuleNames[$id] = array_keys( $testModules[$id] ); 390 } 391 392 wfProfileOut( __METHOD__ ); 393 } 394 395 /** 396 * Add a foreign source of modules. 397 * 398 * @param array|string $id Source ID (string), or array( id1 => loadUrl, id2 => loadUrl, ... ) 399 * @param string|array $loadUrl load.php url (string), or array with loadUrl key for 400 * backwards-compatibility. 401 * @throws MWException 402 */ 403 public function addSource( $id, $loadUrl = null ) { 404 // Allow multiple sources to be registered in one call 405 if ( is_array( $id ) ) { 406 foreach ( $id as $key => $value ) { 407 $this->addSource( $key, $value ); 408 } 409 return; 410 } 411 412 // Disallow duplicates 413 if ( isset( $this->sources[$id] ) ) { 414 throw new MWException( 415 'ResourceLoader duplicate source addition error. ' . 416 'Another source has already been registered as ' . $id 417 ); 418 } 419 420 // Pre 1.24 backwards-compatibility 421 if ( is_array( $loadUrl ) ) { 422 if ( !isset( $loadUrl['loadScript'] ) ) { 423 throw new MWException( 424 __METHOD__ . ' was passed an array with no "loadScript" key.' 425 ); 426 } 427 428 $loadUrl = $loadUrl['loadScript']; 429 } 430 431 $this->sources[$id] = $loadUrl; 432 } 433 434 /** 435 * Get a list of module names. 436 * 437 * @return array List of module names 438 */ 439 public function getModuleNames() { 440 return array_keys( $this->moduleInfos ); 441 } 442 443 /** 444 * Get a list of test module names for one (or all) frameworks. 445 * 446 * If the given framework id is unknkown, or if the in-object variable is not an array, 447 * then it will return an empty array. 448 * 449 * @param string $framework Get only the test module names for one 450 * particular framework (optional) 451 * @return array 452 */ 453 public function getTestModuleNames( $framework = 'all' ) { 454 /** @todo api siteinfo prop testmodulenames modulenames */ 455 if ( $framework == 'all' ) { 456 return $this->testModuleNames; 457 } elseif ( isset( $this->testModuleNames[$framework] ) 458 && is_array( $this->testModuleNames[$framework] ) 459 ) { 460 return $this->testModuleNames[$framework]; 461 } else { 462 return array(); 463 } 464 } 465 466 /** 467 * Get the ResourceLoaderModule object for a given module name. 468 * 469 * If an array of module parameters exists but a ResourceLoaderModule object has not 470 * yet been instantiated, this method will instantiate and cache that object such that 471 * subsequent calls simply return the same object. 472 * 473 * @param string $name Module name 474 * @return ResourceLoaderModule|null If module has been registered, return a 475 * ResourceLoaderModule instance. Otherwise, return null. 476 */ 477 public function getModule( $name ) { 478 if ( !isset( $this->modules[$name] ) ) { 479 if ( !isset( $this->moduleInfos[$name] ) ) { 480 // No such module 481 return null; 482 } 483 // Construct the requested object 484 $info = $this->moduleInfos[$name]; 485 /** @var ResourceLoaderModule $object */ 486 if ( isset( $info['object'] ) ) { 487 // Object given in info array 488 $object = $info['object']; 489 } else { 490 if ( !isset( $info['class'] ) ) { 491 $class = 'ResourceLoaderFileModule'; 492 } else { 493 $class = $info['class']; 494 } 495 /** @var ResourceLoaderModule $object */ 496 $object = new $class( $info ); 497 $object->setConfig( $this->getConfig() ); 498 } 499 $object->setName( $name ); 500 $this->modules[$name] = $object; 501 } 502 503 return $this->modules[$name]; 504 } 505 506 /** 507 * Return whether the definition of a module corresponds to a simple ResourceLoaderFileModule. 508 * 509 * @param string $name Module name 510 * @return bool 511 */ 512 protected function isFileModule( $name ) { 513 if ( !isset( $this->moduleInfos[$name] ) ) { 514 return false; 515 } 516 $info = $this->moduleInfos[$name]; 517 if ( isset( $info['object'] ) || isset( $info['class'] ) ) { 518 return false; 519 } 520 return true; 521 } 522 523 /** 524 * Get the list of sources. 525 * 526 * @return array Like array( id => load.php url, .. ) 527 */ 528 public function getSources() { 529 return $this->sources; 530 } 531 532 /** 533 * Get the URL to the load.php endpoint for the given 534 * ResourceLoader source 535 * 536 * @since 1.24 537 * @param string $source 538 * @throws MWException On an invalid $source name 539 * @return string 540 */ 541 public function getLoadScript( $source ) { 542 if ( !isset( $this->sources[$source] ) ) { 543 throw new MWException( "The $source source was never registered in ResourceLoader." ); 544 } 545 return $this->sources[$source]; 546 } 547 548 /** 549 * Output a response to a load request, including the content-type header. 550 * 551 * @param ResourceLoaderContext $context Context in which a response should be formed 552 */ 553 public function respond( ResourceLoaderContext $context ) { 554 // Use file cache if enabled and available... 555 if ( $this->config->get( 'UseFileCache' ) ) { 556 $fileCache = ResourceFileCache::newFromContext( $context ); 557 if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) { 558 return; // output handled 559 } 560 } 561 562 // Buffer output to catch warnings. Normally we'd use ob_clean() on the 563 // top-level output buffer to clear warnings, but that breaks when ob_gzhandler 564 // is used: ob_clean() will clear the GZIP header in that case and it won't come 565 // back for subsequent output, resulting in invalid GZIP. So we have to wrap 566 // the whole thing in our own output buffer to be sure the active buffer 567 // doesn't use ob_gzhandler. 568 // See http://bugs.php.net/bug.php?id=36514 569 ob_start(); 570 571 wfProfileIn( __METHOD__ ); 572 $errors = ''; 573 574 // Find out which modules are missing and instantiate the others 575 $modules = array(); 576 $missing = array(); 577 foreach ( $context->getModules() as $name ) { 578 $module = $this->getModule( $name ); 579 if ( $module ) { 580 // Do not allow private modules to be loaded from the web. 581 // This is a security issue, see bug 34907. 582 if ( $module->getGroup() === 'private' ) { 583 wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" ); 584 $this->hasErrors = true; 585 // Add exception to the output as a comment 586 $errors .= self::makeComment( "Cannot show private module \"$name\"" ); 587 588 continue; 589 } 590 $modules[$name] = $module; 591 } else { 592 $missing[] = $name; 593 } 594 } 595 596 // Preload information needed to the mtime calculation below 597 try { 598 $this->preloadModuleInfo( array_keys( $modules ), $context ); 599 } catch ( Exception $e ) { 600 MWExceptionHandler::logException( $e ); 601 wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" ); 602 $this->hasErrors = true; 603 // Add exception to the output as a comment 604 $errors .= self::formatException( $e ); 605 } 606 607 wfProfileIn( __METHOD__ . '-getModifiedTime' ); 608 609 // To send Last-Modified and support If-Modified-Since, we need to detect 610 // the last modified time 611 $mtime = wfTimestamp( TS_UNIX, $this->config->get( 'CacheEpoch' ) ); 612 foreach ( $modules as $module ) { 613 /** 614 * @var $module ResourceLoaderModule 615 */ 616 try { 617 // Calculate maximum modified time 618 $mtime = max( $mtime, $module->getModifiedTime( $context ) ); 619 } catch ( Exception $e ) { 620 MWExceptionHandler::logException( $e ); 621 wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" ); 622 $this->hasErrors = true; 623 // Add exception to the output as a comment 624 $errors .= self::formatException( $e ); 625 } 626 } 627 628 wfProfileOut( __METHOD__ . '-getModifiedTime' ); 629 630 // If there's an If-Modified-Since header, respond with a 304 appropriately 631 if ( $this->tryRespondLastModified( $context, $mtime ) ) { 632 wfProfileOut( __METHOD__ ); 633 return; // output handled (buffers cleared) 634 } 635 636 // Generate a response 637 $response = $this->makeModuleResponse( $context, $modules, $missing ); 638 639 // Prepend comments indicating exceptions 640 $response = $errors . $response; 641 642 // Capture any PHP warnings from the output buffer and append them to the 643 // response in a comment if we're in debug mode. 644 if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) { 645 $response = self::makeComment( $warnings ) . $response; 646 $this->hasErrors = true; 647 } 648 649 // Save response to file cache unless there are errors 650 if ( isset( $fileCache ) && !$errors && !count( $missing ) ) { 651 // Cache single modules...and other requests if there are enough hits 652 if ( ResourceFileCache::useFileCache( $context ) ) { 653 if ( $fileCache->isCacheWorthy() ) { 654 $fileCache->saveText( $response ); 655 } else { 656 $fileCache->incrMissesRecent( $context->getRequest() ); 657 } 658 } 659 } 660 661 // Send content type and cache related headers 662 $this->sendResponseHeaders( $context, $mtime, $this->hasErrors ); 663 664 // Remove the output buffer and output the response 665 ob_end_clean(); 666 echo $response; 667 668 wfProfileOut( __METHOD__ ); 669 } 670 671 /** 672 * Send content type and last modified headers to the client. 673 * @param ResourceLoaderContext $context 674 * @param string $mtime TS_MW timestamp to use for last-modified 675 * @param bool $errors Whether there are commented-out errors in the response 676 * @return void 677 */ 678 protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) { 679 $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); 680 // If a version wasn't specified we need a shorter expiry time for updates 681 // to propagate to clients quickly 682 // If there were errors, we also need a shorter expiry time so we can recover quickly 683 if ( is_null( $context->getVersion() ) || $errors ) { 684 $maxage = $rlMaxage['unversioned']['client']; 685 $smaxage = $rlMaxage['unversioned']['server']; 686 // If a version was specified we can use a longer expiry time since changing 687 // version numbers causes cache misses 688 } else { 689 $maxage = $rlMaxage['versioned']['client']; 690 $smaxage = $rlMaxage['versioned']['server']; 691 } 692 if ( $context->getOnly() === 'styles' ) { 693 header( 'Content-Type: text/css; charset=utf-8' ); 694 header( 'Access-Control-Allow-Origin: *' ); 695 } else { 696 header( 'Content-Type: text/javascript; charset=utf-8' ); 697 } 698 header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) ); 699 if ( $context->getDebug() ) { 700 // Do not cache debug responses 701 header( 'Cache-Control: private, no-cache, must-revalidate' ); 702 header( 'Pragma: no-cache' ); 703 } else { 704 header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" ); 705 $exp = min( $maxage, $smaxage ); 706 header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) ); 707 } 708 } 709 710 /** 711 * Respond with 304 Last Modified if appropiate. 712 * 713 * If there's an If-Modified-Since header, respond with a 304 appropriately 714 * and clear out the output buffer. If the client cache is too old then do nothing. 715 * 716 * @param ResourceLoaderContext $context 717 * @param string $mtime The TS_MW timestamp to check the header against 718 * @return bool True if 304 header sent and output handled 719 */ 720 protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) { 721 // If there's an If-Modified-Since header, respond with a 304 appropriately 722 // Some clients send "timestamp;length=123". Strip the part after the first ';' 723 // so we get a valid timestamp. 724 $ims = $context->getRequest()->getHeader( 'If-Modified-Since' ); 725 // Never send 304s in debug mode 726 if ( $ims !== false && !$context->getDebug() ) { 727 $imsTS = strtok( $ims, ';' ); 728 if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) { 729 // There's another bug in ob_gzhandler (see also the comment at 730 // the top of this function) that causes it to gzip even empty 731 // responses, meaning it's impossible to produce a truly empty 732 // response (because the gzip header is always there). This is 733 // a problem because 304 responses have to be completely empty 734 // per the HTTP spec, and Firefox behaves buggily when they're not. 735 // See also http://bugs.php.net/bug.php?id=51579 736 // To work around this, we tear down all output buffering before 737 // sending the 304. 738 wfResetOutputBuffers( /* $resetGzipEncoding = */ true ); 739 740 header( 'HTTP/1.0 304 Not Modified' ); 741 header( 'Status: 304 Not Modified' ); 742 return true; 743 } 744 } 745 return false; 746 } 747 748 /** 749 * Send out code for a response from file cache if possible. 750 * 751 * @param ResourceFileCache $fileCache Cache object for this request URL 752 * @param ResourceLoaderContext $context Context in which to generate a response 753 * @return bool If this found a cache file and handled the response 754 */ 755 protected function tryRespondFromFileCache( 756 ResourceFileCache $fileCache, ResourceLoaderContext $context 757 ) { 758 $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); 759 // Buffer output to catch warnings. 760 ob_start(); 761 // Get the maximum age the cache can be 762 $maxage = is_null( $context->getVersion() ) 763 ? $rlMaxage['unversioned']['server'] 764 : $rlMaxage['versioned']['server']; 765 // Minimum timestamp the cache file must have 766 $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) ); 767 if ( !$good ) { 768 try { // RL always hits the DB on file cache miss... 769 wfGetDB( DB_SLAVE ); 770 } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache 771 $good = $fileCache->isCacheGood(); // cache existence check 772 } 773 } 774 if ( $good ) { 775 $ts = $fileCache->cacheTimestamp(); 776 // Send content type and cache headers 777 $this->sendResponseHeaders( $context, $ts, false ); 778 // If there's an If-Modified-Since header, respond with a 304 appropriately 779 if ( $this->tryRespondLastModified( $context, $ts ) ) { 780 return false; // output handled (buffers cleared) 781 } 782 $response = $fileCache->fetchText(); 783 // Capture any PHP warnings from the output buffer and append them to the 784 // response in a comment if we're in debug mode. 785 if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) { 786 $response = "/*\n$warnings\n*/\n" . $response; 787 } 788 // Remove the output buffer and output the response 789 ob_end_clean(); 790 echo $response . "\n/* Cached {$ts} */"; 791 return true; // cache hit 792 } 793 // Clear buffer 794 ob_end_clean(); 795 796 return false; // cache miss 797 } 798 799 /** 800 * Generate a CSS or JS comment block. 801 * 802 * Only use this for public data, not error message details. 803 * 804 * @param string $text 805 * @return string 806 */ 807 public static function makeComment( $text ) { 808 $encText = str_replace( '*/', '* /', $text ); 809 return "/*\n$encText\n*/\n"; 810 } 811 812 /** 813 * Handle exception display. 814 * 815 * @param Exception $e Exception to be shown to the user 816 * @return string Sanitized text that can be returned to the user 817 */ 818 public static function formatException( $e ) { 819 global $wgShowExceptionDetails; 820 821 if ( $wgShowExceptionDetails ) { 822 return self::makeComment( $e->__toString() ); 823 } else { 824 return self::makeComment( wfMessage( 'internalerror' )->text() ); 825 } 826 } 827 828 /** 829 * Generate code for a response. 830 * 831 * @param ResourceLoaderContext $context Context in which to generate a response 832 * @param array $modules List of module objects keyed by module name 833 * @param array $missing List of requested module names that are unregistered (optional) 834 * @return string Response data 835 */ 836 public function makeModuleResponse( ResourceLoaderContext $context, 837 array $modules, array $missing = array() 838 ) { 839 $out = ''; 840 $exceptions = ''; 841 $states = array(); 842 843 if ( !count( $modules ) && !count( $missing ) ) { 844 return "/* This file is the Web entry point for MediaWiki's ResourceLoader: 845 <https://www.mediawiki.org/wiki/ResourceLoader>. In this request, 846 no modules were requested. Max made me put this here. */"; 847 } 848 849 wfProfileIn( __METHOD__ ); 850 851 // Pre-fetch blobs 852 if ( $context->shouldIncludeMessages() ) { 853 try { 854 $blobs = MessageBlobStore::getInstance()->get( $this, $modules, $context->getLanguage() ); 855 } catch ( Exception $e ) { 856 MWExceptionHandler::logException( $e ); 857 wfDebugLog( 858 'resourceloader', 859 __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" 860 ); 861 $this->hasErrors = true; 862 // Add exception to the output as a comment 863 $exceptions .= self::formatException( $e ); 864 } 865 } else { 866 $blobs = array(); 867 } 868 869 foreach ( $missing as $name ) { 870 $states[$name] = 'missing'; 871 } 872 873 // Generate output 874 $isRaw = false; 875 foreach ( $modules as $name => $module ) { 876 /** 877 * @var $module ResourceLoaderModule 878 */ 879 880 wfProfileIn( __METHOD__ . '-' . $name ); 881 try { 882 $scripts = ''; 883 if ( $context->shouldIncludeScripts() ) { 884 // If we are in debug mode, we'll want to return an array of URLs if possible 885 // However, we can't do this if the module doesn't support it 886 // We also can't do this if there is an only= parameter, because we have to give 887 // the module a way to return a load.php URL without causing an infinite loop 888 if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) { 889 $scripts = $module->getScriptURLsForDebug( $context ); 890 } else { 891 $scripts = $module->getScript( $context ); 892 // rtrim() because there are usually a few line breaks 893 // after the last ';'. A new line at EOF, a new line 894 // added by ResourceLoaderFileModule::readScriptFiles, etc. 895 if ( is_string( $scripts ) 896 && strlen( $scripts ) 897 && substr( rtrim( $scripts ), -1 ) !== ';' 898 ) { 899 // Append semicolon to prevent weird bugs caused by files not 900 // terminating their statements right (bug 27054) 901 $scripts .= ";\n"; 902 } 903 } 904 } 905 // Styles 906 $styles = array(); 907 if ( $context->shouldIncludeStyles() ) { 908 // Don't create empty stylesheets like array( '' => '' ) for modules 909 // that don't *have* any stylesheets (bug 38024). 910 $stylePairs = $module->getStyles( $context ); 911 if ( count( $stylePairs ) ) { 912 // If we are in debug mode without &only= set, we'll want to return an array of URLs 913 // See comment near shouldIncludeScripts() for more details 914 if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) { 915 $styles = array( 916 'url' => $module->getStyleURLsForDebug( $context ) 917 ); 918 } else { 919 // Minify CSS before embedding in mw.loader.implement call 920 // (unless in debug mode) 921 if ( !$context->getDebug() ) { 922 foreach ( $stylePairs as $media => $style ) { 923 // Can be either a string or an array of strings. 924 if ( is_array( $style ) ) { 925 $stylePairs[$media] = array(); 926 foreach ( $style as $cssText ) { 927 if ( is_string( $cssText ) ) { 928 $stylePairs[$media][] = $this->filter( 'minify-css', $cssText ); 929 } 930 } 931 } elseif ( is_string( $style ) ) { 932 $stylePairs[$media] = $this->filter( 'minify-css', $style ); 933 } 934 } 935 } 936 // Wrap styles into @media groups as needed and flatten into a numerical array 937 $styles = array( 938 'css' => self::makeCombinedStyles( $stylePairs ) 939 ); 940 } 941 } 942 } 943 944 // Messages 945 $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}'; 946 947 // Append output 948 switch ( $context->getOnly() ) { 949 case 'scripts': 950 if ( is_string( $scripts ) ) { 951 // Load scripts raw... 952 $out .= $scripts; 953 } elseif ( is_array( $scripts ) ) { 954 // ...except when $scripts is an array of URLs 955 $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() ); 956 } 957 break; 958 case 'styles': 959 // We no longer seperate into media, they are all combined now with 960 // custom media type groups into @media .. {} sections as part of the css string. 961 // Module returns either an empty array or a numerical array with css strings. 962 $out .= isset( $styles['css'] ) ? implode( '', $styles['css'] ) : ''; 963 break; 964 case 'messages': 965 $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) ); 966 break; 967 default: 968 $out .= self::makeLoaderImplementScript( 969 $name, 970 $scripts, 971 $styles, 972 new XmlJsCode( $messagesBlob ) 973 ); 974 break; 975 } 976 } catch ( Exception $e ) { 977 MWExceptionHandler::logException( $e ); 978 wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" ); 979 $this->hasErrors = true; 980 // Add exception to the output as a comment 981 $exceptions .= self::formatException( $e ); 982 983 // Respond to client with error-state instead of module implementation 984 $states[$name] = 'error'; 985 unset( $modules[$name] ); 986 } 987 $isRaw |= $module->isRaw(); 988 wfProfileOut( __METHOD__ . '-' . $name ); 989 } 990 991 // Update module states 992 if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) { 993 if ( count( $modules ) && $context->getOnly() === 'scripts' ) { 994 // Set the state of modules loaded as only scripts to ready as 995 // they don't have an mw.loader.implement wrapper that sets the state 996 foreach ( $modules as $name => $module ) { 997 $states[$name] = 'ready'; 998 } 999 } 1000 1001 // Set the state of modules we didn't respond to with mw.loader.implement 1002 if ( count( $states ) ) { 1003 $out .= self::makeLoaderStateScript( $states ); 1004 } 1005 } else { 1006 if ( count( $states ) ) { 1007 $exceptions .= self::makeComment( 1008 'Problematic modules: ' . FormatJson::encode( $states, ResourceLoader::inDebugMode() ) 1009 ); 1010 } 1011 } 1012 1013 if ( !$context->getDebug() ) { 1014 if ( $context->getOnly() === 'styles' ) { 1015 $out = $this->filter( 'minify-css', $out ); 1016 } else { 1017 $out = $this->filter( 'minify-js', $out ); 1018 } 1019 } 1020 1021 wfProfileOut( __METHOD__ ); 1022 return $exceptions . $out; 1023 } 1024 1025 /* Static Methods */ 1026 1027 /** 1028 * Return JS code that calls mw.loader.implement with given module properties. 1029 * 1030 * @param string $name Module name 1031 * @param mixed $scripts List of URLs to JavaScript files or String of JavaScript code 1032 * @param mixed $styles Array of CSS strings keyed by media type, or an array of lists of URLs 1033 * to CSS files keyed by media type 1034 * @param mixed $messages List of messages associated with this module. May either be an 1035 * associative array mapping message key to value, or a JSON-encoded message blob containing 1036 * the same data, wrapped in an XmlJsCode object. 1037 * @throws MWException 1038 * @return string 1039 */ 1040 public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { 1041 if ( is_string( $scripts ) ) { 1042 $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" ); 1043 } elseif ( !is_array( $scripts ) ) { 1044 throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' ); 1045 } 1046 return Xml::encodeJsCall( 1047 'mw.loader.implement', 1048 array( 1049 $name, 1050 $scripts, 1051 // Force objects. mw.loader.implement requires them to be javascript objects. 1052 // Although these variables are associative arrays, which become javascript 1053 // objects through json_encode. In many cases they will be empty arrays, and 1054 // PHP/json_encode() consider empty arrays to be numerical arrays and 1055 // output javascript "[]" instead of "{}". This fixes that. 1056 (object)$styles, 1057 (object)$messages 1058 ), 1059 ResourceLoader::inDebugMode() 1060 ); 1061 } 1062 1063 /** 1064 * Returns JS code which, when called, will register a given list of messages. 1065 * 1066 * @param mixed $messages Either an associative array mapping message key to value, or a 1067 * JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object. 1068 * @return string 1069 */ 1070 public static function makeMessageSetScript( $messages ) { 1071 return Xml::encodeJsCall( 1072 'mw.messages.set', 1073 array( (object)$messages ), 1074 ResourceLoader::inDebugMode() 1075 ); 1076 } 1077 1078 /** 1079 * Combines an associative array mapping media type to CSS into a 1080 * single stylesheet with "@media" blocks. 1081 * 1082 * @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings 1083 * @return array 1084 */ 1085 public static function makeCombinedStyles( array $stylePairs ) { 1086 $out = array(); 1087 foreach ( $stylePairs as $media => $styles ) { 1088 // ResourceLoaderFileModule::getStyle can return the styles 1089 // as a string or an array of strings. This is to allow separation in 1090 // the front-end. 1091 $styles = (array)$styles; 1092 foreach ( $styles as $style ) { 1093 $style = trim( $style ); 1094 // Don't output an empty "@media print { }" block (bug 40498) 1095 if ( $style !== '' ) { 1096 // Transform the media type based on request params and config 1097 // The way that this relies on $wgRequest to propagate request params is slightly evil 1098 $media = OutputPage::transformCssMedia( $media ); 1099 1100 if ( $media === '' || $media == 'all' ) { 1101 $out[] = $style; 1102 } elseif ( is_string( $media ) ) { 1103 $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}"; 1104 } 1105 // else: skip 1106 } 1107 } 1108 } 1109 return $out; 1110 } 1111 1112 /** 1113 * Returns a JS call to mw.loader.state, which sets the state of a 1114 * module or modules to a given value. Has two calling conventions: 1115 * 1116 * - ResourceLoader::makeLoaderStateScript( $name, $state ): 1117 * Set the state of a single module called $name to $state 1118 * 1119 * - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ): 1120 * Set the state of modules with the given names to the given states 1121 * 1122 * @param string $name 1123 * @param string $state 1124 * @return string 1125 */ 1126 public static function makeLoaderStateScript( $name, $state = null ) { 1127 if ( is_array( $name ) ) { 1128 return Xml::encodeJsCall( 1129 'mw.loader.state', 1130 array( $name ), 1131 ResourceLoader::inDebugMode() 1132 ); 1133 } else { 1134 return Xml::encodeJsCall( 1135 'mw.loader.state', 1136 array( $name, $state ), 1137 ResourceLoader::inDebugMode() 1138 ); 1139 } 1140 } 1141 1142 /** 1143 * Returns JS code which calls the script given by $script. The script will 1144 * be called with local variables name, version, dependencies and group, 1145 * which will have values corresponding to $name, $version, $dependencies 1146 * and $group as supplied. 1147 * 1148 * @param string $name Module name 1149 * @param int $version Module version number as a timestamp 1150 * @param array $dependencies List of module names on which this module depends 1151 * @param string $group Group which the module is in. 1152 * @param string $source Source of the module, or 'local' if not foreign. 1153 * @param string $script JavaScript code 1154 * @return string 1155 */ 1156 public static function makeCustomLoaderScript( $name, $version, $dependencies, 1157 $group, $source, $script 1158 ) { 1159 $script = str_replace( "\n", "\n\t", trim( $script ) ); 1160 return Xml::encodeJsCall( 1161 "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )", 1162 array( $name, $version, $dependencies, $group, $source ), 1163 ResourceLoader::inDebugMode() 1164 ); 1165 } 1166 1167 /** 1168 * Returns JS code which calls mw.loader.register with the given 1169 * parameters. Has three calling conventions: 1170 * 1171 * - ResourceLoader::makeLoaderRegisterScript( $name, $version, 1172 * $dependencies, $group, $source, $skip 1173 * ): 1174 * Register a single module. 1175 * 1176 * - ResourceLoader::makeLoaderRegisterScript( array( $name1, $name2 ) ): 1177 * Register modules with the given names. 1178 * 1179 * - ResourceLoader::makeLoaderRegisterScript( array( 1180 * array( $name1, $version1, $dependencies1, $group1, $source1, $skip1 ), 1181 * array( $name2, $version2, $dependencies1, $group2, $source2, $skip2 ), 1182 * ... 1183 * ) ): 1184 * Registers modules with the given names and parameters. 1185 * 1186 * @param string $name Module name 1187 * @param int $version Module version number as a timestamp 1188 * @param array $dependencies List of module names on which this module depends 1189 * @param string $group Group which the module is in 1190 * @param string $source Source of the module, or 'local' if not foreign 1191 * @param string $skip Script body of the skip function 1192 * @return string 1193 */ 1194 public static function makeLoaderRegisterScript( $name, $version = null, 1195 $dependencies = null, $group = null, $source = null, $skip = null 1196 ) { 1197 if ( is_array( $name ) ) { 1198 return Xml::encodeJsCall( 1199 'mw.loader.register', 1200 array( $name ), 1201 ResourceLoader::inDebugMode() 1202 ); 1203 } else { 1204 $version = (int)$version > 1 ? (int)$version : 1; 1205 return Xml::encodeJsCall( 1206 'mw.loader.register', 1207 array( $name, $version, $dependencies, $group, $source, $skip ), 1208 ResourceLoader::inDebugMode() 1209 ); 1210 } 1211 } 1212 1213 /** 1214 * Returns JS code which calls mw.loader.addSource() with the given 1215 * parameters. Has two calling conventions: 1216 * 1217 * - ResourceLoader::makeLoaderSourcesScript( $id, $properties ): 1218 * Register a single source 1219 * 1220 * - ResourceLoader::makeLoaderSourcesScript( array( $id1 => $loadUrl, $id2 => $loadUrl, ... ) ); 1221 * Register sources with the given IDs and properties. 1222 * 1223 * @param string $id Source ID 1224 * @param array $properties Source properties (see addSource()) 1225 * @return string 1226 */ 1227 public static function makeLoaderSourcesScript( $id, $properties = null ) { 1228 if ( is_array( $id ) ) { 1229 return Xml::encodeJsCall( 1230 'mw.loader.addSource', 1231 array( $id ), 1232 ResourceLoader::inDebugMode() 1233 ); 1234 } else { 1235 return Xml::encodeJsCall( 1236 'mw.loader.addSource', 1237 array( $id, $properties ), 1238 ResourceLoader::inDebugMode() 1239 ); 1240 } 1241 } 1242 1243 /** 1244 * Returns JS code which runs given JS code if the client-side framework is 1245 * present. 1246 * 1247 * @param string $script JavaScript code 1248 * @return string 1249 */ 1250 public static function makeLoaderConditionalScript( $script ) { 1251 return "if(window.mw){\n" . trim( $script ) . "\n}"; 1252 } 1253 1254 /** 1255 * Returns JS code which will set the MediaWiki configuration array to 1256 * the given value. 1257 * 1258 * @param array $configuration List of configuration values keyed by variable name 1259 * @return string 1260 */ 1261 public static function makeConfigSetScript( array $configuration ) { 1262 return Xml::encodeJsCall( 1263 'mw.config.set', 1264 array( $configuration ), 1265 ResourceLoader::inDebugMode() 1266 ); 1267 } 1268 1269 /** 1270 * Convert an array of module names to a packed query string. 1271 * 1272 * For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ) 1273 * becomes 'foo.bar,baz|bar.baz,quux' 1274 * @param array $modules List of module names (strings) 1275 * @return string Packed query string 1276 */ 1277 public static function makePackedModulesString( $modules ) { 1278 $groups = array(); // array( prefix => array( suffixes ) ) 1279 foreach ( $modules as $module ) { 1280 $pos = strrpos( $module, '.' ); 1281 $prefix = $pos === false ? '' : substr( $module, 0, $pos ); 1282 $suffix = $pos === false ? $module : substr( $module, $pos + 1 ); 1283 $groups[$prefix][] = $suffix; 1284 } 1285 1286 $arr = array(); 1287 foreach ( $groups as $prefix => $suffixes ) { 1288 $p = $prefix === '' ? '' : $prefix . '.'; 1289 $arr[] = $p . implode( ',', $suffixes ); 1290 } 1291 $str = implode( '|', $arr ); 1292 return $str; 1293 } 1294 1295 /** 1296 * Determine whether debug mode was requested 1297 * Order of priority is 1) request param, 2) cookie, 3) $wg setting 1298 * @return bool 1299 */ 1300 public static function inDebugMode() { 1301 if ( self::$debugMode === null ) { 1302 global $wgRequest, $wgResourceLoaderDebug; 1303 self::$debugMode = $wgRequest->getFuzzyBool( 'debug', 1304 $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) 1305 ); 1306 } 1307 return self::$debugMode; 1308 } 1309 1310 /** 1311 * Reset static members used for caching. 1312 * 1313 * Global state and $wgRequest are evil, but we're using it right 1314 * now and sometimes we need to be able to force ResourceLoader to 1315 * re-evaluate the context because it has changed (e.g. in the test suite). 1316 */ 1317 public static function clearCache() { 1318 self::$debugMode = null; 1319 } 1320 1321 /** 1322 * Build a load.php URL 1323 * 1324 * @since 1.24 1325 * @param string $source Name of the ResourceLoader source 1326 * @param ResourceLoaderContext $context 1327 * @param array $extraQuery 1328 * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative) 1329 */ 1330 public function createLoaderURL( $source, ResourceLoaderContext $context, 1331 $extraQuery = array() 1332 ) { 1333 $query = self::createLoaderQuery( $context, $extraQuery ); 1334 $script = $this->getLoadScript( $source ); 1335 1336 // Prevent the IE6 extension check from being triggered (bug 28840) 1337 // by appending a character that's invalid in Windows extensions ('*') 1338 return wfExpandUrl( wfAppendQuery( $script, $query ) . '&*', PROTO_RELATIVE ); 1339 } 1340 1341 /** 1342 * Build a load.php URL 1343 * @deprecated since 1.24, use createLoaderURL instead 1344 * @param array $modules Array of module names (strings) 1345 * @param string $lang Language code 1346 * @param string $skin Skin name 1347 * @param string|null $user User name. If null, the &user= parameter is omitted 1348 * @param string|null $version Versioning timestamp 1349 * @param bool $debug Whether the request should be in debug mode 1350 * @param string|null $only &only= parameter 1351 * @param bool $printable Printable mode 1352 * @param bool $handheld Handheld mode 1353 * @param array $extraQuery Extra query parameters to add 1354 * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative) 1355 */ 1356 public static function makeLoaderURL( $modules, $lang, $skin, $user = null, 1357 $version = null, $debug = false, $only = null, $printable = false, 1358 $handheld = false, $extraQuery = array() 1359 ) { 1360 global $wgLoadScript; 1361 1362 $query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug, 1363 $only, $printable, $handheld, $extraQuery 1364 ); 1365 1366 // Prevent the IE6 extension check from being triggered (bug 28840) 1367 // by appending a character that's invalid in Windows extensions ('*') 1368 return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE ); 1369 } 1370 1371 /** 1372 * Helper for createLoaderURL() 1373 * 1374 * @since 1.24 1375 * @see makeLoaderQuery 1376 * @param ResourceLoaderContext $context 1377 * @param array $extraQuery 1378 * @return array 1379 */ 1380 public static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = array() ) { 1381 return self::makeLoaderQuery( 1382 $context->getModules(), 1383 $context->getLanguage(), 1384 $context->getSkin(), 1385 $context->getUser(), 1386 $context->getVersion(), 1387 $context->getDebug(), 1388 $context->getOnly(), 1389 $context->getRequest()->getBool( 'printable' ), 1390 $context->getRequest()->getBool( 'handheld' ), 1391 $extraQuery 1392 ); 1393 } 1394 1395 /** 1396 * Build a query array (array representation of query string) for load.php. Helper 1397 * function for makeLoaderURL(). 1398 * 1399 * @param array $modules 1400 * @param string $lang 1401 * @param string $skin 1402 * @param string $user 1403 * @param string $version 1404 * @param bool $debug 1405 * @param string $only 1406 * @param bool $printable 1407 * @param bool $handheld 1408 * @param array $extraQuery 1409 * 1410 * @return array 1411 */ 1412 public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, 1413 $version = null, $debug = false, $only = null, $printable = false, 1414 $handheld = false, $extraQuery = array() 1415 ) { 1416 $query = array( 1417 'modules' => self::makePackedModulesString( $modules ), 1418 'lang' => $lang, 1419 'skin' => $skin, 1420 'debug' => $debug ? 'true' : 'false', 1421 ); 1422 if ( $user !== null ) { 1423 $query['user'] = $user; 1424 } 1425 if ( $version !== null ) { 1426 $query['version'] = $version; 1427 } 1428 if ( $only !== null ) { 1429 $query['only'] = $only; 1430 } 1431 if ( $printable ) { 1432 $query['printable'] = 1; 1433 } 1434 if ( $handheld ) { 1435 $query['handheld'] = 1; 1436 } 1437 $query += $extraQuery; 1438 1439 // Make queries uniform in order 1440 ksort( $query ); 1441 return $query; 1442 } 1443 1444 /** 1445 * Check a module name for validity. 1446 * 1447 * Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be 1448 * at most 255 bytes. 1449 * 1450 * @param string $moduleName Module name to check 1451 * @return bool Whether $moduleName is a valid module name 1452 */ 1453 public static function isValidModuleName( $moduleName ) { 1454 return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255; 1455 } 1456 1457 /** 1458 * Returns LESS compiler set up for use with MediaWiki 1459 * 1460 * @param Config $config 1461 * @throws MWException 1462 * @since 1.22 1463 * @return lessc 1464 */ 1465 public static function getLessCompiler( Config $config ) { 1466 // When called from the installer, it is possible that a required PHP extension 1467 // is missing (at least for now; see bug 47564). If this is the case, throw an 1468 // exception (caught by the installer) to prevent a fatal error later on. 1469 if ( !function_exists( 'ctype_digit' ) ) { 1470 throw new MWException( 'lessc requires the Ctype extension' ); 1471 } 1472 1473 $less = new lessc(); 1474 $less->setPreserveComments( true ); 1475 $less->setVariables( self::getLessVars( $config ) ); 1476 $less->setImportDir( $config->get( 'ResourceLoaderLESSImportPaths' ) ); 1477 foreach ( $config->get( 'ResourceLoaderLESSFunctions' ) as $name => $func ) { 1478 $less->registerFunction( $name, $func ); 1479 } 1480 return $less; 1481 } 1482 1483 /** 1484 * Get global LESS variables. 1485 * 1486 * @param Config $config 1487 * @since 1.22 1488 * @return array Map of variable names to string CSS values. 1489 */ 1490 public static function getLessVars( Config $config ) { 1491 $lessVars = $config->get( 'ResourceLoaderLESSVars' ); 1492 // Sort by key to ensure consistent hashing for cache lookups. 1493 ksort( $lessVars ); 1494 return $lessVars; 1495 } 1496 }
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 |