MediaWiki  master
ResourceLoader.php
Go to the documentation of this file.
1 <?php
29 
44 class ResourceLoader implements LoggerAwareInterface {
46  protected $config;
48  protected $blobStore;
49 
51  private $logger;
52 
54  protected $modules = [];
56  protected $moduleInfos = [];
62  protected $testModuleNames = [];
64  protected $testSuiteModuleNames = [];
65 
67  protected $sources = [];
69  protected $errors = [];
71  protected $extraHeaders = [];
72 
74  protected static $debugMode = null;
75 
77  const CACHE_VERSION = 8;
78 
80  const FILTER_NOMIN = '/*@nomin*/';
81 
96  public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
97  if ( !$moduleNames ) {
98  // Or else Database*::select() will explode, plus it's cheaper!
99  return;
100  }
101  $dbr = wfGetDB( DB_REPLICA );
102  $lang = $context->getLanguage();
103 
104  // Batched version of ResourceLoaderModule::getFileDependencies
105  $vary = ResourceLoaderModule::getVary( $context );
106  $res = $dbr->select( 'module_deps', [ 'md_module', 'md_deps' ], [
107  'md_module' => $moduleNames,
108  'md_skin' => $vary,
109  ], __METHOD__
110  );
111 
112  // Prime in-object cache for file dependencies
113  $modulesWithDeps = [];
114  foreach ( $res as $row ) {
115  $module = $this->getModule( $row->md_module );
116  if ( $module ) {
117  $module->setFileDependencies( $context, ResourceLoaderModule::expandRelativePaths(
118  json_decode( $row->md_deps, true )
119  ) );
120  $modulesWithDeps[] = $row->md_module;
121  }
122  }
123  // Register the absence of a dependency row too
124  foreach ( array_diff( $moduleNames, $modulesWithDeps ) as $name ) {
125  $module = $this->getModule( $name );
126  if ( $module ) {
127  $this->getModule( $name )->setFileDependencies( $context, [] );
128  }
129  }
130 
131  // Batched version of ResourceLoaderWikiModule::getTitleInfo
132  ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
133 
134  // Prime in-object cache for message blobs for modules with messages
135  $modules = [];
136  foreach ( $moduleNames as $name ) {
137  $module = $this->getModule( $name );
138  if ( $module && $module->getMessages() ) {
139  $modules[$name] = $module;
140  }
141  }
142  $store = $this->getMessageBlobStore();
143  $blobs = $store->getBlobs( $modules, $lang );
144  foreach ( $blobs as $name => $blob ) {
145  $modules[$name]->setMessageBlob( $blob, $lang );
146  }
147  }
148 
166  public static function filter( $filter, $data, array $options = [] ) {
167  if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
168  return $data;
169  }
170 
171  if ( isset( $options['cache'] ) && $options['cache'] === false ) {
172  return self::applyFilter( $filter, $data );
173  }
174 
175  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
177 
178  $key = $cache->makeGlobalKey(
179  'resourceloader-filter',
180  $filter,
181  self::CACHE_VERSION,
182  md5( $data )
183  );
184 
185  $result = $cache->get( $key );
186  if ( $result === false ) {
187  $stats->increment( "resourceloader_cache.$filter.miss" );
188  $result = self::applyFilter( $filter, $data );
189  $cache->set( $key, $result, 24 * 3600 );
190  } else {
191  $stats->increment( "resourceloader_cache.$filter.hit" );
192  }
193  if ( $result === null ) {
194  // Cached failure
195  $result = $data;
196  }
197 
198  return $result;
199  }
200 
201  private static function applyFilter( $filter, $data ) {
202  $data = trim( $data );
203  if ( $data ) {
204  try {
205  $data = ( $filter === 'minify-css' )
206  ? CSSMin::minify( $data )
207  : JavaScriptMinifier::minify( $data );
208  } catch ( Exception $e ) {
210  return null;
211  }
212  }
213  return $data;
214  }
215 
221  public function __construct( Config $config = null, LoggerInterface $logger = null ) {
222  $this->logger = $logger ?: new NullLogger();
223 
224  if ( !$config ) {
225  wfDeprecated( __METHOD__ . ' without a Config instance', '1.34' );
226  $config = MediaWikiServices::getInstance()->getMainConfig();
227  }
228  $this->config = $config;
229 
230  // Add 'local' source first
231  $this->addSource( 'local', $config->get( 'LoadScript' ) );
232 
233  // Special module that always exists
234  $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
235 
236  $this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) );
237  }
238 
242  public function getConfig() {
243  return $this->config;
244  }
245 
250  public function setLogger( LoggerInterface $logger ) {
251  $this->logger = $logger;
252  }
253 
258  public function getLogger() {
259  return $this->logger;
260  }
261 
266  public function getMessageBlobStore() {
267  return $this->blobStore;
268  }
269 
275  $this->blobStore = $blobStore;
276  }
277 
289  public function register( $name, $info = null ) {
290  $moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
291 
292  // Allow multiple modules to be registered in one call
293  $registrations = is_array( $name ) ? $name : [ $name => $info ];
294  foreach ( $registrations as $name => $info ) {
295  // Warn on duplicate registrations
296  if ( isset( $this->moduleInfos[$name] ) ) {
297  // A module has already been registered by this name
298  $this->logger->warning(
299  'ResourceLoader duplicate registration warning. ' .
300  'Another module has already been registered as ' . $name
301  );
302  }
303 
304  // Check validity
305  if ( !self::isValidModuleName( $name ) ) {
306  throw new MWException( "ResourceLoader module name '$name' is invalid, "
307  . "see ResourceLoader::isValidModuleName()" );
308  }
309  if ( !is_array( $info ) ) {
310  throw new InvalidArgumentException(
311  'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
312  );
313  }
314 
315  // Attach module
316  $this->moduleInfos[$name] = $info;
317 
318  // Last-minute changes
319  // Apply custom skin-defined styles to existing modules.
320  if ( $this->isFileModule( $name ) ) {
321  foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
322  // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
323  if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
324  continue;
325  }
326 
327  // If $name is preceded with a '+', the defined style files will be added to 'default'
328  // skinStyles, otherwise 'default' will be ignored as it normally would be.
329  if ( isset( $skinStyles[$name] ) ) {
330  $paths = (array)$skinStyles[$name];
331  $styleFiles = [];
332  } elseif ( isset( $skinStyles['+' . $name] ) ) {
333  $paths = (array)$skinStyles['+' . $name];
334  $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
335  (array)$this->moduleInfos[$name]['skinStyles']['default'] :
336  [];
337  } else {
338  continue;
339  }
340 
341  // Add new file paths, remapping them to refer to our directories and not use settings
342  // from the module we're modifying, which come from the base definition.
343  list( $localBasePath, $remoteBasePath ) =
345 
346  foreach ( $paths as $path ) {
347  $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
348  }
349 
350  $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
351  }
352  }
353  }
354  }
355 
360  public function registerTestModules() {
361  global $IP;
362 
363  if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
364  throw new MWException( 'Attempt to register JavaScript test modules '
365  . 'but <code>$wgEnableJavaScriptTest</code> is false. '
366  . 'Edit your <code>LocalSettings.php</code> to enable it.' );
367  }
368 
369  // This has a 'qunit' key for compat with the below hook.
370  $testModulesMeta = [ 'qunit' => [] ];
371 
372  // Get test suites from extensions
373  // Avoid PHP 7.1 warning from passing $this by reference
374  $rl = $this;
375  Hooks::run( 'ResourceLoaderTestModules', [ &$testModulesMeta, &$rl ] );
376  $extRegistry = ExtensionRegistry::getInstance();
377  // In case of conflict, the deprecated hook has precedence.
378  $testModules = $testModulesMeta['qunit'] + $extRegistry->getAttribute( 'QUnitTestModules' );
379 
381  foreach ( $testModules as $name => &$module ) {
382  // Turn any single-module dependency into an array
383  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
384  $module['dependencies'] = [ $module['dependencies'] ];
385  }
386 
387  // Ensure the testrunner loads before any test suites
388  $module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
389 
390  // Keep track of the test suites to load on SpecialJavaScriptTest
391  $testSuiteModuleNames[] = $name;
392  }
393 
394  // Core test suites (their names have further precedence).
395  $testModules = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules;
396  $testSuiteModuleNames[] = 'test.mediawiki.qunit.suites';
397 
398  $this->register( $testModules );
399  $this->testSuiteModuleNames = $testSuiteModuleNames;
400  }
401 
412  public function addSource( $id, $loadUrl = null ) {
413  // Allow multiple sources to be registered in one call
414  if ( is_array( $id ) ) {
415  foreach ( $id as $key => $value ) {
416  $this->addSource( $key, $value );
417  }
418  return;
419  }
420 
421  // Disallow duplicates
422  if ( isset( $this->sources[$id] ) ) {
423  throw new MWException(
424  'ResourceLoader duplicate source addition error. ' .
425  'Another source has already been registered as ' . $id
426  );
427  }
428 
429  // Pre 1.24 backwards-compatibility
430  if ( is_array( $loadUrl ) ) {
431  if ( !isset( $loadUrl['loadScript'] ) ) {
432  throw new MWException(
433  __METHOD__ . ' was passed an array with no "loadScript" key.'
434  );
435  }
436 
437  $loadUrl = $loadUrl['loadScript'];
438  }
439 
440  $this->sources[$id] = $loadUrl;
441  }
442 
448  public function getModuleNames() {
449  return array_keys( $this->moduleInfos );
450  }
451 
459  public function getTestSuiteModuleNames() {
461  }
462 
470  public function isModuleRegistered( $name ) {
471  return isset( $this->moduleInfos[$name] );
472  }
473 
485  public function getModule( $name ) {
486  if ( !isset( $this->modules[$name] ) ) {
487  if ( !isset( $this->moduleInfos[$name] ) ) {
488  // No such module
489  return null;
490  }
491  // Construct the requested module object
492  $info = $this->moduleInfos[$name];
493  if ( isset( $info['factory'] ) ) {
495  $object = call_user_func( $info['factory'], $info );
496  } else {
497  $class = $info['class'] ?? ResourceLoaderFileModule::class;
499  $object = new $class( $info );
500  }
501  $object->setConfig( $this->getConfig() );
502  $object->setLogger( $this->logger );
503  $object->setName( $name );
504  $this->modules[$name] = $object;
505  }
506 
507  return $this->modules[$name];
508  }
509 
516  protected function isFileModule( $name ) {
517  if ( !isset( $this->moduleInfos[$name] ) ) {
518  return false;
519  }
520  $info = $this->moduleInfos[$name];
521  return !isset( $info['factory'] ) && (
522  // The implied default for 'class' is ResourceLoaderFileModule
523  !isset( $info['class'] ) ||
524  // Explicit default
525  $info['class'] === ResourceLoaderFileModule::class ||
526  is_subclass_of( $info['class'], ResourceLoaderFileModule::class )
527  );
528  }
529 
535  public function getSources() {
536  return $this->sources;
537  }
538 
548  public function getLoadScript( $source ) {
549  if ( !isset( $this->sources[$source] ) ) {
550  throw new MWException( "The $source source was never registered in ResourceLoader." );
551  }
552  return $this->sources[$source];
553  }
554 
558  const HASH_LENGTH = 5;
559 
622  public static function makeHash( $value ) {
623  $hash = hash( 'fnv132', $value );
624  // The base_convert will pad it (if too short),
625  // then substr() will trim it (if too long).
626  return substr(
627  Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
628  0,
629  self::HASH_LENGTH
630  );
631  }
632 
642  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
644  $this->logger->warning(
645  $msg,
646  $context + [ 'exception' => $e ]
647  );
648  $this->errors[] = self::formatExceptionNoComment( $e );
649  }
650 
659  public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
660  if ( !$moduleNames ) {
661  return '';
662  }
663  $hashes = array_map( function ( $module ) use ( $context ) {
664  try {
665  return $this->getModule( $module )->getVersionHash( $context );
666  } catch ( Exception $e ) {
667  // If modules fail to compute a version, don't fail the request (T152266)
668  // and still compute versions of other modules.
669  $this->outputErrorAndLog( $e,
670  'Calculating version for "{module}" failed: {exception}',
671  [
672  'module' => $module,
673  ]
674  );
675  return '';
676  }
677  }, $moduleNames );
678  return self::makeHash( implode( '', $hashes ) );
679  }
680 
695  public function makeVersionQuery( ResourceLoaderContext $context, array $modules = null ) {
696  if ( $modules === null ) {
697  wfDeprecated( __METHOD__ . ' without $modules', '1.34' );
698  $modules = $context->getModules();
699  }
700  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
701  // version hashes. There is no technical reason for this to be same, and for years the
702  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
703  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
704  // query parameter), then this method must continue to match the JS one.
705  $filtered = [];
706  foreach ( $modules as $name ) {
707  if ( !$this->getModule( $name ) ) {
708  // If a versioned request contains a missing module, the version is a mismatch
709  // as the client considered a module (and version) we don't have.
710  return '';
711  }
712  $filtered[] = $name;
713  }
714  return $this->getCombinedVersion( $context, $filtered );
715  }
716 
723  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
724  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
725  // is used: ob_clean() will clear the GZIP header in that case and it won't come
726  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
727  // the whole thing in our own output buffer to be sure the active buffer
728  // doesn't use ob_gzhandler.
729  // See https://bugs.php.net/bug.php?id=36514
730  ob_start();
731 
732  $this->measureResponseTime( RequestContext::getMain()->getTiming() );
733 
734  // Find out which modules are missing and instantiate the others
735  $modules = [];
736  $missing = [];
737  foreach ( $context->getModules() as $name ) {
738  $module = $this->getModule( $name );
739  if ( $module ) {
740  // Do not allow private modules to be loaded from the web.
741  // This is a security issue, see T36907.
742  if ( $module->getGroup() === 'private' ) {
743  // Not a serious error, just means something is trying to access it (T101806)
744  $this->logger->debug( "Request for private module '$name' denied" );
745  $this->errors[] = "Cannot build private module \"$name\"";
746  continue;
747  }
748  $modules[$name] = $module;
749  } else {
750  $missing[] = $name;
751  }
752  }
753 
754  try {
755  // Preload for getCombinedVersion() and for batch makeModuleResponse()
756  $this->preloadModuleInfo( array_keys( $modules ), $context );
757  } catch ( Exception $e ) {
758  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
759  }
760 
761  // Combine versions to propagate cache invalidation
762  $versionHash = '';
763  try {
764  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
765  } catch ( Exception $e ) {
766  $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
767  }
768 
769  // See RFC 2616 § 3.11 Entity Tags
770  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
771  $etag = 'W/"' . $versionHash . '"';
772 
773  // Try the client-side cache first
774  if ( $this->tryRespondNotModified( $context, $etag ) ) {
775  return; // output handled (buffers cleared)
776  }
777 
778  // Use file cache if enabled and available...
779  if ( $this->config->get( 'UseFileCache' ) ) {
780  $fileCache = ResourceFileCache::newFromContext( $context );
781  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
782  return; // output handled
783  }
784  } else {
785  $fileCache = null;
786  }
787 
788  // Generate a response
789  $response = $this->makeModuleResponse( $context, $modules, $missing );
790 
791  // Capture any PHP warnings from the output buffer and append them to the
792  // error list if we're in debug mode.
793  if ( $context->getDebug() ) {
794  $warnings = ob_get_contents();
795  if ( strlen( $warnings ) ) {
796  $this->errors[] = $warnings;
797  }
798  }
799 
800  // Consider saving the response to file cache (unless there are errors).
801  if ( $fileCache &&
802  !$this->errors &&
803  $missing === [] &&
805  ) {
806  if ( $fileCache->isCacheWorthy() ) {
807  // There were enough hits, save the response to the cache
808  $fileCache->saveText( $response );
809  } else {
810  $fileCache->incrMissesRecent( $context->getRequest() );
811  }
812  }
813 
814  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
815 
816  // Remove the output buffer and output the response
817  ob_end_clean();
818 
819  if ( $context->getImageObj() && $this->errors ) {
820  // We can't show both the error messages and the response when it's an image.
821  $response = implode( "\n\n", $this->errors );
822  } elseif ( $this->errors ) {
823  $errorText = implode( "\n\n", $this->errors );
824  $errorResponse = self::makeComment( $errorText );
825  if ( $context->shouldIncludeScripts() ) {
826  $errorResponse .= 'if (window.console && console.error) { console.error('
827  . $context->encodeJson( $errorText )
828  . "); }\n";
829  }
830 
831  // Prepend error info to the response
832  $response = $errorResponse . $response;
833  }
834 
835  $this->errors = [];
836  echo $response;
837  }
838 
839  protected function measureResponseTime( Timing $timing ) {
840  DeferredUpdates::addCallableUpdate( function () use ( $timing ) {
841  $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
842  if ( $measure !== false ) {
843  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
844  $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
845  }
846  } );
847  }
848 
860  protected function sendResponseHeaders(
861  ResourceLoaderContext $context, $etag, $errors, array $extra = []
862  ) {
864  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
865  // Use a short cache expiry so that updates propagate to clients quickly, if:
866  // - No version specified (shared resources, e.g. stylesheets)
867  // - There were errors (recover quickly)
868  // - Version mismatch (T117587, T47877)
869  if ( is_null( $context->getVersion() )
870  || $errors
871  || $context->getVersion() !== $this->makeVersionQuery( $context, $context->getModules() )
872  ) {
873  $maxage = $rlMaxage['unversioned']['client'];
874  $smaxage = $rlMaxage['unversioned']['server'];
875  // If a version was specified we can use a longer expiry time since changing
876  // version numbers causes cache misses
877  } else {
878  $maxage = $rlMaxage['versioned']['client'];
879  $smaxage = $rlMaxage['versioned']['server'];
880  }
881  if ( $context->getImageObj() ) {
882  // Output different headers if we're outputting textual errors.
883  if ( $errors ) {
884  header( 'Content-Type: text/plain; charset=utf-8' );
885  } else {
886  $context->getImageObj()->sendResponseHeaders( $context );
887  }
888  } elseif ( $context->getOnly() === 'styles' ) {
889  header( 'Content-Type: text/css; charset=utf-8' );
890  header( 'Access-Control-Allow-Origin: *' );
891  } else {
892  header( 'Content-Type: text/javascript; charset=utf-8' );
893  }
894  // See RFC 2616 § 14.19 ETag
895  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
896  header( 'ETag: ' . $etag );
897  if ( $context->getDebug() ) {
898  // Do not cache debug responses
899  header( 'Cache-Control: private, no-cache, must-revalidate' );
900  header( 'Pragma: no-cache' );
901  } else {
902  header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
903  $exp = min( $maxage, $smaxage );
904  header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
905  }
906  foreach ( $extra as $header ) {
907  header( $header );
908  }
909  }
910 
921  protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
922  // See RFC 2616 § 14.26 If-None-Match
923  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
924  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
925  // Never send 304s in debug mode
926  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
927  // There's another bug in ob_gzhandler (see also the comment at
928  // the top of this function) that causes it to gzip even empty
929  // responses, meaning it's impossible to produce a truly empty
930  // response (because the gzip header is always there). This is
931  // a problem because 304 responses have to be completely empty
932  // per the HTTP spec, and Firefox behaves buggily when they're not.
933  // See also https://bugs.php.net/bug.php?id=51579
934  // To work around this, we tear down all output buffering before
935  // sending the 304.
936  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
937 
938  HttpStatus::header( 304 );
939 
940  $this->sendResponseHeaders( $context, $etag, false );
941  return true;
942  }
943  return false;
944  }
945 
954  protected function tryRespondFromFileCache(
955  ResourceFileCache $fileCache,
957  $etag
958  ) {
959  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
960  // Buffer output to catch warnings.
961  ob_start();
962  // Get the maximum age the cache can be
963  $maxage = is_null( $context->getVersion() )
964  ? $rlMaxage['unversioned']['server']
965  : $rlMaxage['versioned']['server'];
966  // Minimum timestamp the cache file must have
967  $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
968  if ( !$good ) {
969  try { // RL always hits the DB on file cache miss...
970  wfGetDB( DB_REPLICA );
971  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
972  $good = $fileCache->isCacheGood(); // cache existence check
973  }
974  }
975  if ( $good ) {
976  $ts = $fileCache->cacheTimestamp();
977  // Send content type and cache headers
978  $this->sendResponseHeaders( $context, $etag, false );
979  $response = $fileCache->fetchText();
980  // Capture any PHP warnings from the output buffer and append them to the
981  // response in a comment if we're in debug mode.
982  if ( $context->getDebug() ) {
983  $warnings = ob_get_contents();
984  if ( strlen( $warnings ) ) {
985  $response = self::makeComment( $warnings ) . $response;
986  }
987  }
988  // Remove the output buffer and output the response
989  ob_end_clean();
990  echo $response . "\n/* Cached {$ts} */";
991  return true; // cache hit
992  }
993  // Clear buffer
994  ob_end_clean();
995 
996  return false; // cache miss
997  }
998 
1007  public static function makeComment( $text ) {
1008  $encText = str_replace( '*/', '* /', $text );
1009  return "/*\n$encText\n*/\n";
1010  }
1011 
1018  public static function formatException( $e ) {
1019  return self::makeComment( self::formatExceptionNoComment( $e ) );
1020  }
1021 
1029  protected static function formatExceptionNoComment( $e ) {
1030  global $wgShowExceptionDetails;
1031 
1032  if ( !$wgShowExceptionDetails ) {
1034  }
1035 
1036  return MWExceptionHandler::getLogMessage( $e ) .
1037  "\nBacktrace:\n" .
1039  }
1040 
1053  array $modules, array $missing = []
1054  ) {
1055  $out = '';
1056  $states = [];
1057 
1058  if ( $modules === [] && $missing === [] ) {
1059  return <<<MESSAGE
1060 /* This file is the Web entry point for MediaWiki's ResourceLoader:
1061  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1062  no modules were requested. Max made me put this here. */
1063 MESSAGE;
1064  }
1065 
1066  $image = $context->getImageObj();
1067  if ( $image ) {
1068  $data = $image->getImageData( $context );
1069  if ( $data === false ) {
1070  $data = '';
1071  $this->errors[] = 'Image generation failed';
1072  }
1073  return $data;
1074  }
1075 
1076  foreach ( $missing as $name ) {
1077  $states[$name] = 'missing';
1078  }
1079 
1080  $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js';
1081 
1082  foreach ( $modules as $name => $module ) {
1083  try {
1084  $content = $module->getModuleContent( $context );
1085  $implementKey = $name . '@' . $module->getVersionHash( $context );
1086  $strContent = '';
1087 
1088  if ( isset( $content['headers'] ) ) {
1089  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1090  }
1091 
1092  // Append output
1093  switch ( $context->getOnly() ) {
1094  case 'scripts':
1095  $scripts = $content['scripts'];
1096  if ( is_string( $scripts ) ) {
1097  // Load scripts raw...
1098  $strContent = $scripts;
1099  } elseif ( is_array( $scripts ) ) {
1100  // ...except when $scripts is an array of URLs or an associative array
1101  $strContent = self::makeLoaderImplementScript(
1102  $context,
1103  $implementKey,
1104  $scripts,
1105  [],
1106  [],
1107  []
1108  );
1109  }
1110  break;
1111  case 'styles':
1112  $styles = $content['styles'];
1113  // We no longer separate into media, they are all combined now with
1114  // custom media type groups into @media .. {} sections as part of the css string.
1115  // Module returns either an empty array or a numerical array with css strings.
1116  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1117  break;
1118  default:
1119  $scripts = $content['scripts'] ?? '';
1120  if ( is_string( $scripts ) ) {
1121  if ( $name === 'site' || $name === 'user' ) {
1122  // Legacy scripts that run in the global scope without a closure.
1123  // mw.loader.implement will use globalEval if scripts is a string.
1124  // Minify manually here, because general response minification is
1125  // not effective due it being a string literal, not a function.
1126  if ( !$context->getDebug() ) {
1127  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1128  }
1129  } else {
1130  $scripts = new XmlJsCode( $scripts );
1131  }
1132  }
1133  $strContent = self::makeLoaderImplementScript(
1134  $context,
1135  $implementKey,
1136  $scripts,
1137  $content['styles'] ?? [],
1138  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1139  $content['templates'] ?? []
1140  );
1141  break;
1142  }
1143 
1144  if ( !$context->getDebug() ) {
1145  $strContent = self::filter( $filter, $strContent );
1146  } else {
1147  // In debug mode, separate each response by a new line.
1148  // For example, between 'mw.loader.implement();' statements.
1149  $strContent = $this->ensureNewline( $strContent );
1150  }
1151 
1152  if ( $context->getOnly() === 'scripts' ) {
1153  // Use a linebreak between module scripts (T162719)
1154  $out .= $this->ensureNewline( $strContent );
1155  } else {
1156  $out .= $strContent;
1157  }
1158 
1159  } catch ( Exception $e ) {
1160  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1161 
1162  // Respond to client with error-state instead of module implementation
1163  $states[$name] = 'error';
1164  unset( $modules[$name] );
1165  }
1166  }
1167 
1168  // Update module states
1169  if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1170  if ( $modules && $context->getOnly() === 'scripts' ) {
1171  // Set the state of modules loaded as only scripts to ready as
1172  // they don't have an mw.loader.implement wrapper that sets the state
1173  foreach ( $modules as $name => $module ) {
1174  $states[$name] = 'ready';
1175  }
1176  }
1177 
1178  // Set the state of modules we didn't respond to with mw.loader.implement
1179  if ( $states ) {
1180  $stateScript = self::makeLoaderStateScript( $context, $states );
1181  if ( !$context->getDebug() ) {
1182  $stateScript = self::filter( 'minify-js', $stateScript );
1183  }
1184  // Use a linebreak between module script and state script (T162719)
1185  $out = $this->ensureNewline( $out ) . $stateScript;
1186  }
1187  } elseif ( $states ) {
1188  $this->errors[] = 'Problematic modules: '
1189  . $context->encodeJson( $states );
1190  }
1191 
1192  return $out;
1193  }
1194 
1200  private function ensureNewline( $str ) {
1201  $end = substr( $str, -1 );
1202  if ( $end === false || $end === '' || $end === "\n" ) {
1203  return $str;
1204  }
1205  return $str . "\n";
1206  }
1207 
1214  public function getModulesByMessage( $messageKey ) {
1215  $moduleNames = [];
1216  foreach ( $this->getModuleNames() as $moduleName ) {
1217  $module = $this->getModule( $moduleName );
1218  if ( in_array( $messageKey, $module->getMessages() ) ) {
1219  $moduleNames[] = $moduleName;
1220  }
1221  }
1222  return $moduleNames;
1223  }
1224 
1243  private static function makeLoaderImplementScript(
1244  ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates
1245  ) {
1246  if ( $scripts instanceof XmlJsCode ) {
1247  if ( $scripts->value === '' ) {
1248  $scripts = null;
1249  } elseif ( $context->getDebug() ) {
1250  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1251  } else {
1252  $scripts = new XmlJsCode( 'function($,jQuery,require,module){' . $scripts->value . '}' );
1253  }
1254  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1255  $files = $scripts['files'];
1256  foreach ( $files as $path => &$file ) {
1257  // $file is changed (by reference) from a descriptor array to the content of the file
1258  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1259  if ( $file['type'] === 'script' ) {
1260  // Multi-file modules only get two parameters ($ and jQuery are being phased out)
1261  if ( $context->getDebug() ) {
1262  $file = new XmlJsCode( "function ( require, module ) {\n{$file['content']}\n}" );
1263  } else {
1264  $file = new XmlJsCode( 'function(require,module){' . $file['content'] . '}' );
1265  }
1266  } else {
1267  $file = $file['content'];
1268  }
1269  }
1270  $scripts = XmlJsCode::encodeObject( [
1271  'main' => $scripts['main'],
1272  'files' => XmlJsCode::encodeObject( $files, $context->getDebug() )
1273  ], $context->getDebug() );
1274  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1275  throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
1276  }
1277 
1278  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1279  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1280  // of "{}". Force them to objects.
1281  $module = [
1282  $name,
1283  $scripts,
1284  (object)$styles,
1285  (object)$messages,
1286  (object)$templates
1287  ];
1288  self::trimArray( $module );
1289 
1290  return Xml::encodeJsCall( 'mw.loader.implement', $module, $context->getDebug() );
1291  }
1292 
1299  public static function makeMessageSetScript( $messages ) {
1300  return 'mw.messages.set('
1301  . self::encodeJsonForScript( (object)$messages )
1302  . ');';
1303  }
1304 
1312  public static function makeCombinedStyles( array $stylePairs ) {
1313  $out = [];
1314  foreach ( $stylePairs as $media => $styles ) {
1315  // ResourceLoaderFileModule::getStyle can return the styles
1316  // as a string or an array of strings. This is to allow separation in
1317  // the front-end.
1318  $styles = (array)$styles;
1319  foreach ( $styles as $style ) {
1320  $style = trim( $style );
1321  // Don't output an empty "@media print { }" block (T42498)
1322  if ( $style !== '' ) {
1323  // Transform the media type based on request params and config
1324  // The way that this relies on $wgRequest to propagate request params is slightly evil
1325  $media = OutputPage::transformCssMedia( $media );
1326 
1327  if ( $media === '' || $media == 'all' ) {
1328  $out[] = $style;
1329  } elseif ( is_string( $media ) ) {
1330  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1331  }
1332  // else: skip
1333  }
1334  }
1335  }
1336  return $out;
1337  }
1338 
1348  public static function encodeJsonForScript( $data ) {
1349  // Keep output as small as possible by disabling needless escape modes
1350  // that PHP uses by default.
1351  // However, while most module scripts are only served on HTTP responses
1352  // for JavaScript, some modules can also be embedded in the HTML as inline
1353  // scripts. This, and the fact that we sometimes need to export strings
1354  // containing user-generated content and labels that may genuinely contain
1355  // a sequences like "</script>", we need to encode either '/' or '<'.
1356  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1357  // and allows URLs to mostly remain readable.
1358  $jsonFlags = JSON_UNESCAPED_SLASHES |
1359  JSON_UNESCAPED_UNICODE |
1360  JSON_HEX_TAG |
1361  JSON_HEX_AMP;
1362  if ( self::inDebugMode() ) {
1363  $jsonFlags |= JSON_PRETTY_PRINT;
1364  }
1365  return json_encode( $data, $jsonFlags );
1366  }
1367 
1380  public static function makeLoaderStateScript(
1381  ResourceLoaderContext $context, array $states
1382  ) {
1383  return 'mw.loader.state('
1384  . $context->encodeJson( $states )
1385  . ');';
1386  }
1387 
1388  private static function isEmptyObject( stdClass $obj ) {
1389  foreach ( $obj as $key => $value ) {
1390  return false;
1391  }
1392  return true;
1393  }
1394 
1407  private static function trimArray( array &$array ) {
1408  $i = count( $array );
1409  while ( $i-- ) {
1410  if ( $array[$i] === null
1411  || $array[$i] === []
1412  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1413  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1414  ) {
1415  unset( $array[$i] );
1416  } else {
1417  break;
1418  }
1419  }
1420  }
1421 
1450  public static function makeLoaderRegisterScript(
1451  ResourceLoaderContext $context, array $modules
1452  ) {
1453  // Optimisation: Transform dependency names into indexes when possible
1454  // to produce smaller output. They are expanded by mw.loader.register on
1455  // the other end using resolveIndexedDependencies().
1456  $index = [];
1457  foreach ( $modules as $i => &$module ) {
1458  // Build module name index
1459  $index[$module[0]] = $i;
1460  }
1461  foreach ( $modules as &$module ) {
1462  if ( isset( $module[2] ) ) {
1463  foreach ( $module[2] as &$dependency ) {
1464  if ( isset( $index[$dependency] ) ) {
1465  // Replace module name in dependency list with index
1466  $dependency = $index[$dependency];
1467  }
1468  }
1469  }
1470  }
1471 
1472  array_walk( $modules, [ self::class, 'trimArray' ] );
1473 
1474  return 'mw.loader.register('
1475  . $context->encodeJson( $modules )
1476  . ');';
1477  }
1478 
1493  public static function makeLoaderSourcesScript(
1494  ResourceLoaderContext $context, array $sources
1495  ) {
1496  return 'mw.loader.addSource('
1497  . $context->encodeJson( $sources )
1498  . ');';
1499  }
1500 
1507  public static function makeLoaderConditionalScript( $script ) {
1508  // Adds a function to lazy-created RLQ
1509  return '(RLQ=window.RLQ||[]).push(function(){' .
1510  trim( $script ) . '});';
1511  }
1512 
1521  public static function makeInlineCodeWithModule( $modules, $script ) {
1522  // Adds an array to lazy-created RLQ
1523  return '(RLQ=window.RLQ||[]).push(['
1524  . self::encodeJsonForScript( $modules ) . ','
1525  . 'function(){' . trim( $script ) . '}'
1526  . ']);';
1527  }
1528 
1540  public static function makeInlineScript( $script, $nonce = null ) {
1541  $js = self::makeLoaderConditionalScript( $script );
1542  $escNonce = '';
1543  if ( $nonce === null ) {
1544  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1545  } elseif ( $nonce !== false ) {
1546  // If it was false, CSP is disabled, so no nonce attribute.
1547  // Nonce should be only base64 characters, so should be safe,
1548  // but better to be safely escaped than sorry.
1549  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1550  }
1551 
1552  return new WrappedString(
1553  Html::inlineScript( $js, $nonce ),
1554  "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1555  '});</script>'
1556  );
1557  }
1558 
1567  public static function makeConfigSetScript( array $configuration ) {
1568  $json = self::encodeJsonForScript( $configuration );
1569  if ( $json === false ) {
1570  $e = new Exception(
1571  'JSON serialization of config data failed. ' .
1572  'This usually means the config data is not valid UTF-8.'
1573  );
1575  return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1576  }
1577  return "mw.config.set($json);";
1578  }
1579 
1593  public static function makePackedModulesString( array $modules ) {
1594  $moduleMap = []; // [ prefix => [ suffixes ] ]
1595  foreach ( $modules as $module ) {
1596  $pos = strrpos( $module, '.' );
1597  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1598  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1599  $moduleMap[$prefix][] = $suffix;
1600  }
1601 
1602  $arr = [];
1603  foreach ( $moduleMap as $prefix => $suffixes ) {
1604  $p = $prefix === '' ? '' : $prefix . '.';
1605  $arr[] = $p . implode( ',', $suffixes );
1606  }
1607  return implode( '|', $arr );
1608  }
1609 
1621  public static function expandModuleNames( $modules ) {
1622  $retval = [];
1623  $exploded = explode( '|', $modules );
1624  foreach ( $exploded as $group ) {
1625  if ( strpos( $group, ',' ) === false ) {
1626  // This is not a set of modules in foo.bar,baz notation
1627  // but a single module
1628  $retval[] = $group;
1629  } else {
1630  // This is a set of modules in foo.bar,baz notation
1631  $pos = strrpos( $group, '.' );
1632  if ( $pos === false ) {
1633  // Prefixless modules, i.e. without dots
1634  $retval = array_merge( $retval, explode( ',', $group ) );
1635  } else {
1636  // We have a prefix and a bunch of suffixes
1637  $prefix = substr( $group, 0, $pos ); // 'foo'
1638  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1639  foreach ( $suffixes as $suffix ) {
1640  $retval[] = "$prefix.$suffix";
1641  }
1642  }
1643  }
1644  }
1645  return $retval;
1646  }
1647 
1653  public static function inDebugMode() {
1654  if ( self::$debugMode === null ) {
1656  self::$debugMode = $wgRequest->getFuzzyBool( 'debug',
1657  $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug )
1658  );
1659  }
1660  return self::$debugMode;
1661  }
1662 
1673  public static function clearCache() {
1674  self::$debugMode = null;
1675  }
1676 
1686  public function createLoaderURL( $source, ResourceLoaderContext $context,
1687  array $extraQuery = []
1688  ) {
1689  $query = self::createLoaderQuery( $context, $extraQuery );
1690  $script = $this->getLoadScript( $source );
1691 
1692  return wfAppendQuery( $script, $query );
1693  }
1694 
1704  protected static function createLoaderQuery(
1705  ResourceLoaderContext $context, array $extraQuery = []
1706  ) {
1707  return self::makeLoaderQuery(
1708  $context->getModules(),
1709  $context->getLanguage(),
1710  $context->getSkin(),
1711  $context->getUser(),
1712  $context->getVersion(),
1713  $context->getDebug(),
1714  $context->getOnly(),
1715  $context->getRequest()->getBool( 'printable' ),
1716  $context->getRequest()->getBool( 'handheld' ),
1717  $extraQuery
1718  );
1719  }
1720 
1737  public static function makeLoaderQuery( array $modules, $lang, $skin, $user = null,
1738  $version = null, $debug = false, $only = null, $printable = false,
1739  $handheld = false, array $extraQuery = []
1740  ) {
1741  $query = [
1742  'modules' => self::makePackedModulesString( $modules ),
1743  ];
1744  // Keep urls short by omitting query parameters that
1745  // match the defaults assumed by ResourceLoaderContext.
1746  // Note: This relies on the defaults either being insignificant or forever constant,
1747  // as otherwise cached urls could change in meaning when the defaults change.
1749  $query['lang'] = $lang;
1750  }
1751  if ( $skin !== ResourceLoaderContext::DEFAULT_SKIN ) {
1752  $query['skin'] = $skin;
1753  }
1754  if ( $debug === true ) {
1755  $query['debug'] = 'true';
1756  }
1757  if ( $user !== null ) {
1758  $query['user'] = $user;
1759  }
1760  if ( $version !== null ) {
1761  $query['version'] = $version;
1762  }
1763  if ( $only !== null ) {
1764  $query['only'] = $only;
1765  }
1766  if ( $printable ) {
1767  $query['printable'] = 1;
1768  }
1769  if ( $handheld ) {
1770  $query['handheld'] = 1;
1771  }
1772  $query += $extraQuery;
1773 
1774  // Make queries uniform in order
1775  ksort( $query );
1776  return $query;
1777  }
1778 
1788  public static function isValidModuleName( $moduleName ) {
1789  return strcspn( $moduleName, '!,|', 0, 255 ) === strlen( $moduleName );
1790  }
1791 
1802  public function getLessCompiler( $vars = [] ) {
1803  global $IP;
1804  // When called from the installer, it is possible that a required PHP extension
1805  // is missing (at least for now; see T49564). If this is the case, throw an
1806  // exception (caught by the installer) to prevent a fatal error later on.
1807  if ( !class_exists( 'Less_Parser' ) ) {
1808  throw new MWException( 'MediaWiki requires the less.php parser' );
1809  }
1810 
1811  $parser = new Less_Parser;
1812  $parser->ModifyVars( $vars );
1813  $parser->SetImportDirs( [
1814  "$IP/resources/src/mediawiki.less/" => '',
1815  ] );
1816  $parser->SetOption( 'relativeUrls', false );
1817 
1818  return $parser;
1819  }
1820 }
static getLogMessage( $e)
Get a message formatting the exception message and its origin.
This class generates message blobs for use by ResourceLoader.
getModuleNames()
Get a list of module names.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
isCacheGood( $timestamp='')
Check if up to date cache file exists.
$response
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
static filter( $filter, $data, array $options=[])
Run JavaScript or CSS data through a filter, caching the filtered result for future calls...
ResourceLoaderModule [] $modules
Map of (module name => ResourceLoaderModule)
getSources()
Get the list of sources.
$context
Definition: load.php:45
$IP
Definition: WebStart.php:41
if(!isset( $args[0])) $lang
array $testModuleNames
Associative array mapping framework ids to a list of names of test suite modules like [ &#39;qunit&#39; => [ ...
isModuleRegistered( $name)
Check whether a ResourceLoader module is registered.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
$source
static makeLoaderImplementScript(ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates)
Return JS code that calls mw.loader.implement with given module properties.
static minify( $css)
Removes whitespace from CSS data.
Definition: CSSMin.php:535
static makeConfigSetScript(array $configuration)
Returns JS code which will set the MediaWiki configuration array to the given value.
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
Definition: WebRequest.php:71
static newFromContext(ResourceLoaderContext $context)
Construct an ResourceFileCache from a context.
static makeLoaderQuery(array $modules, $lang, $skin, $user=null, $version=null, $debug=false, $only=null, $printable=false, $handheld=false, array $extraQuery=[])
Build a query array (array representation of query string) for load.php.
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:572
getTestSuiteModuleNames()
Get a list of module names with QUnit test suites.
static makeCombinedStyles(array $stylePairs)
Combines an associative array mapping media type to CSS into a single stylesheet with "@media" blocks...
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
cacheTimestamp()
Get the last-modified timestamp of the cache file.
static makeMessageSetScript( $messages)
Returns JS code which, when called, will register a given list of messages.
static formatException( $e)
Handle exception display.
setMessageBlob( $blob, $lang)
Set in-object cache for message blobs.
tryRespondNotModified(ResourceLoaderContext $context, $etag)
Respond with HTTP 304 Not Modified if appropiate.
getCombinedVersion(ResourceLoaderContext $context, array $moduleNames)
Helper method to get and combine versions of multiple modules.
static makePackedModulesString(array $modules)
Convert an array of module names to a packed query string.
static formatExceptionNoComment( $e)
Handle exception display.
__construct(Config $config=null, LoggerInterface $logger=null)
Register core modules and runs registration hooks.
makeVersionQuery(ResourceLoaderContext $context, array $modules=null)
Get the expected value of the &#39;version&#39; query parameter.
preloadModuleInfo(array $moduleNames, ResourceLoaderContext $context)
Load information stored in the database about modules.
static expandRelativePaths(array $filePaths)
Expand directories relative to $IP.
createLoaderURL( $source, ResourceLoaderContext $context, array $extraQuery=[])
Build a load.php URL.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
static encodeObject( $obj, $pretty=false)
Encode an object containing XmlJsCode objects.
Definition: XmlJsCode.php:58
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
MessageBlobStore $blobStore
static makeInlineCodeWithModule( $modules, $script)
Wraps JavaScript code to run after a required module.
static getMain()
Get the RequestContext object associated with the main request.
static getRedactedTraceAsString( $e)
Generate a string representation of an exception&#39;s stack trace.
static makeHash( $value)
Create a hash for module versioning purposes.
Interface for configuration instances.
Definition: Config.php:28
getLoadScript( $source)
Get the URL to the load.php endpoint for the given ResourceLoader source.
static makeLoaderSourcesScript(ResourceLoaderContext $context, array $sources)
Returns JS code which calls mw.loader.addSource() with the given parameters.
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:677
ResourceLoader request result caching in the file system.
setMessageBlobStore(MessageBlobStore $blobStore)
static makeLoaderRegisterScript(ResourceLoaderContext $context, array $modules)
Returns JS code which calls mw.loader.register with the given parameter.
static warnIfHeadersSent()
Log a warning message if headers have already been sent.
static preloadTitleInfo(ResourceLoaderContext $context, IDatabase $db, array $moduleNames)
A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to interpret a given string a...
Definition: XmlJsCode.php:40
static header( $code)
Output an HTTP status code header.
Definition: HttpStatus.php:96
$cache
Definition: mcc.php:33
static encodeJsonForScript( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode...
static logException( $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log an exception to the exception log (if enabled).
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode...
static createLoaderQuery(ResourceLoaderContext $context, array $extraQuery=[])
Helper for createLoaderURL()
An interface to help developers measure the performance of their applications.
Definition: Timing.php:45
$filter
getModulesByMessage( $messageKey)
Get names of modules that use a certain message.
$header
measureResponseTime(Timing $timing)
tryRespondFromFileCache(ResourceFileCache $fileCache, ResourceLoaderContext $context, $etag)
Send out code for a response from file cache if possible.
fetchText()
Get the uncompressed text from the cache.
static makeInlineScript( $script, $nonce=null)
Returns an HTML script tag that runs given JS code after startup and base modules.
array $errors
Errors accumulated during current respond() call.
static isValidModuleName( $moduleName)
Check a module name for validity.
getLessCompiler( $vars=[])
Returns LESS compiler set up for use with MediaWiki.
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path...
static clearCache()
Reset static members used for caching.
static inDebugMode()
Determine whether debug mode was requested Order of priority is 1) request param, 2) cookie...
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
isFileModule( $name)
Whether the module is a ResourceLoaderFileModule (including subclasses).
sendResponseHeaders(ResourceLoaderContext $context, $etag, $errors, array $extra=[])
Send main response headers to the client.
$debug
Definition: Setup.php:760
static bool $debugMode
static useFileCache(ResourceLoaderContext $context)
Check if an RL request can be cached.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
array $sources
Map of (source => path); E.g.
string [] $testSuiteModuleNames
List of module names that contain QUnit test suites.
const CACHE_ANYTHING
Definition: Defines.php:81
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information. ...
setLogger(LoggerInterface $logger)
LoggerInterface $logger
static isEmptyObject(stdClass $obj)
static expandModuleNames( $modules)
Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to an array of module names like `[ &#39;jq...
string [] $extraHeaders
Extra HTTP response headers from modules loaded in makeModuleResponse()
outputErrorAndLog(Exception $e, $msg, array $context=[])
Add an error to the &#39;errors&#39; array and log it.
respond(ResourceLoaderContext $context)
Output a response to a load request, including the content-type header.
addSource( $id, $loadUrl=null)
Add a foreign source of modules.
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:728
getImageObj()
If this is a request for an image, get the ResourceLoaderImage object.
$wgResourceLoaderDebug
The default debug mode (on/off) for of ResourceLoader requests.
const DB_REPLICA
Definition: defines.php:25
$content
Definition: router.php:78
static makeLoaderStateScript(ResourceLoaderContext $context, array $states)
Returns a JS call to mw.loader.state, which sets the state of modules to a given value: ...
ensureNewline( $str)
Ensure the string is either empty or ends in a line break.
static getVary(ResourceLoaderContext $context)
Get vary string.
getModule( $name)
Get the ResourceLoaderModule object for a given module name.
static makeLoaderConditionalScript( $script)
Wraps JavaScript code to run after the startup module.
measure( $measureName, $startMark='requestStart', $endMark=null)
This method stores the duration between two marks along with the associated name (a "measure")...
Definition: Timing.php:124
makeModuleResponse(ResourceLoaderContext $context, array $modules, array $missing=[])
Generate code for a response.
static getPublicLogMessage( $e)
static applyFilter( $filter, $data)
return true
Definition: router.php:92
static minify( $s)
Returns minified JavaScript code.
array [] $moduleInfos
Map of (module name => associative info array)
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
static trimArray(array &$array)
Remove empty values from the end of an array.
static makeComment( $text)
Generate a CSS or JS comment block.
Context object that contains information about the state of a specific ResourceLoader web request...