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