[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/resourceloader/ -> ResourceLoader.php (source)

   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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1