MediaWiki  master
ResourceLoader.php
Go to the documentation of this file.
1 <?php
28 use Psr\Log\LoggerAwareInterface;
29 use Psr\Log\LoggerInterface;
30 use Psr\Log\NullLogger;
33 use Wikimedia\Minify\CSSMin;
34 use Wikimedia\Minify\JavaScriptMinifier;
36 use Wikimedia\RequestTimeout\TimeoutException;
37 use Wikimedia\ScopedCallback;
38 use Wikimedia\Timestamp\ConvertibleTimestamp;
39 use Wikimedia\WrappedString;
40 
61 class ResourceLoader implements LoggerAwareInterface {
63  public const CACHE_VERSION = 9;
65  public const FILTER_NOMIN = '/*@nomin*/';
66 
68  private const RL_DEP_STORE_PREFIX = 'ResourceLoaderModule';
70  private const RL_MODULE_DEP_TTL = BagOStuff::TTL_WEEK;
71 
73  protected static $debugMode = null;
74 
76  private $config;
78  private $blobStore;
80  private $depStore;
82  private $logger;
84  private $hookContainer;
86  private $hookRunner;
87 
89  private $modules = [];
91  private $moduleInfos = [];
93  private $testSuiteModuleNames = [];
95  private $sources = [];
97  protected $errors = [];
102  protected $extraHeaders = [];
104  private $depStoreUpdateBuffer = [];
109  private $moduleSkinStyles = [];
110 
120  public function __construct(
121  Config $config,
122  LoggerInterface $logger = null,
124  ) {
125  $this->config = $config;
126  $this->logger = $logger ?: new NullLogger();
127 
128  $services = MediaWikiServices::getInstance();
129  $this->hookContainer = $services->getHookContainer();
130  $this->hookRunner = new HookRunner( $this->hookContainer );
131 
132  // Add 'local' source first
133  $this->addSource( 'local', $config->get( MainConfigNames::LoadScript ) );
134 
135  // Special module that always exists
136  $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
137 
138  $this->setMessageBlobStore(
139  new MessageBlobStore( $this, $this->logger, $services->getMainWANObjectCache() )
140  );
141 
143  $this->setDependencyStore( $tracker );
144  }
145 
149  public function getConfig() {
150  return $this->config;
151  }
152 
157  public function setLogger( LoggerInterface $logger ) {
158  $this->logger = $logger;
159  }
160 
165  public function getLogger() {
166  return $this->logger;
167  }
168 
173  public function getMessageBlobStore() {
174  return $this->blobStore;
175  }
176 
182  $this->blobStore = $blobStore;
183  }
184 
190  $this->depStore = $tracker;
191  }
192 
197  public function setModuleSkinStyles( array $moduleSkinStyles ) {
198  $this->moduleSkinStyles = $moduleSkinStyles;
199  }
200 
212  public function register( $name, array $info = null ) {
213  // Allow multiple modules to be registered in one call
214  $registrations = is_array( $name ) ? $name : [ $name => $info ];
215  foreach ( $registrations as $name => $info ) {
216  // Warn on duplicate registrations
217  if ( isset( $this->moduleInfos[$name] ) ) {
218  // A module has already been registered by this name
219  $this->logger->warning(
220  'ResourceLoader duplicate registration warning. ' .
221  'Another module has already been registered as ' . $name
222  );
223  }
224 
225  // Check validity
226  if ( !self::isValidModuleName( $name ) ) {
227  throw new InvalidArgumentException( "ResourceLoader module name '$name' is invalid, "
228  . "see ResourceLoader::isValidModuleName()" );
229  }
230  if ( !is_array( $info ) ) {
231  throw new InvalidArgumentException(
232  'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
233  );
234  }
235 
236  // Attach module
237  $this->moduleInfos[$name] = $info;
238  }
239  }
240 
245  public function registerTestModules(): void {
246  global $IP;
247 
248  if ( $this->config->get( MainConfigNames::EnableJavaScriptTest ) !== true ) {
249  throw new MWException( 'Attempt to register JavaScript test modules '
250  . 'but <code>$wgEnableJavaScriptTest</code> is false. '
251  . 'Edit your <code>LocalSettings.php</code> to enable it.' );
252  }
253 
254  // This has a 'qunit' key for compat with the below hook.
255  $testModulesMeta = [ 'qunit' => [] ];
256 
257  $this->hookRunner->onResourceLoaderTestModules( $testModulesMeta, $this );
258  $extRegistry = ExtensionRegistry::getInstance();
259  // In case of conflict, the deprecated hook has precedence.
260  $testModules = $testModulesMeta['qunit']
261  + $extRegistry->getAttribute( 'QUnitTestModules' );
262 
264  foreach ( $testModules as $name => &$module ) {
265  // Turn any single-module dependency into an array
266  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
267  $module['dependencies'] = [ $module['dependencies'] ];
268  }
269 
270  // Ensure the testrunner loads before any test suites
271  $module['dependencies'][] = 'mediawiki.qunit-testrunner';
272 
273  // Keep track of the test suites to load on SpecialJavaScriptTest
274  $testSuiteModuleNames[] = $name;
275  }
276 
277  // Core test suites (their names have further precedence).
278  $testModules = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules;
279  $testSuiteModuleNames[] = 'test.MediaWiki';
280 
281  $this->register( $testModules );
282  $this->testSuiteModuleNames = $testSuiteModuleNames;
283  }
284 
295  public function addSource( $sources, $loadUrl = null ) {
296  if ( !is_array( $sources ) ) {
297  $sources = [ $sources => $loadUrl ];
298  }
299  foreach ( $sources as $id => $source ) {
300  // Disallow duplicates
301  if ( isset( $this->sources[$id] ) ) {
302  throw new RuntimeException( 'Cannot register source ' . $id . ' twice' );
303  }
304 
305  // Support: MediaWiki 1.24 and earlier
306  if ( is_array( $source ) ) {
307  if ( !isset( $source['loadScript'] ) ) {
308  throw new InvalidArgumentException( 'Each source must have a "loadScript" key' );
309  }
310  $source = $source['loadScript'];
311  }
312 
313  $this->sources[$id] = $source;
314  }
315  }
316 
320  public function getModuleNames() {
321  return array_keys( $this->moduleInfos );
322  }
323 
331  public function getTestSuiteModuleNames() {
332  return $this->testSuiteModuleNames;
333  }
334 
342  public function isModuleRegistered( $name ) {
343  return isset( $this->moduleInfos[$name] );
344  }
345 
357  public function getModule( $name ) {
358  if ( !isset( $this->modules[$name] ) ) {
359  if ( !isset( $this->moduleInfos[$name] ) ) {
360  // No such module
361  return null;
362  }
363  // Construct the requested module object
364  $info = $this->moduleInfos[$name];
365  if ( isset( $info['factory'] ) ) {
367  $object = call_user_func( $info['factory'], $info );
368  } else {
369  $class = $info['class'] ?? ResourceLoaderFileModule::class;
371  $object = new $class( $info );
372  }
373  $object->setConfig( $this->getConfig() );
374  $object->setLogger( $this->logger );
375  $object->setHookContainer( $this->hookContainer );
376  $object->setName( $name );
377  $object->setDependencyAccessCallbacks(
378  [ $this, 'loadModuleDependenciesInternal' ],
379  [ $this, 'saveModuleDependenciesInternal' ]
380  );
381  $object->setSkinStylesOverride( $this->moduleSkinStyles );
382  $this->modules[$name] = $object;
383  }
384 
385  return $this->modules[$name];
386  }
387 
394  public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
395  // Load all tracked indirect file dependencies for the modules
396  $vary = ResourceLoaderModule::getVary( $context );
397  $entitiesByModule = [];
398  foreach ( $moduleNames as $moduleName ) {
399  $entitiesByModule[$moduleName] = "$moduleName|$vary";
400  }
401  $depsByEntity = $this->depStore->retrieveMulti(
402  self::RL_DEP_STORE_PREFIX,
403  $entitiesByModule
404  );
405  // Inject the indirect file dependencies for all the modules
406  foreach ( $moduleNames as $moduleName ) {
407  $module = $this->getModule( $moduleName );
408  if ( $module ) {
409  $entity = $entitiesByModule[$moduleName];
410  $deps = $depsByEntity[$entity];
411  $paths = ResourceLoaderModule::expandRelativePaths( $deps['paths'] );
412  $module->setFileDependencies( $context, $paths );
413  }
414  }
415 
416  // Batched version of ResourceLoaderWikiModule::getTitleInfo
417  $dbr = wfGetDB( DB_REPLICA );
418  ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
419 
420  // Prime in-object cache for message blobs for modules with messages
421  $modulesWithMessages = [];
422  foreach ( $moduleNames as $moduleName ) {
423  $module = $this->getModule( $moduleName );
424  if ( $module && $module->getMessages() ) {
425  $modulesWithMessages[$moduleName] = $module;
426  }
427  }
428  // Prime in-object cache for message blobs for modules with messages
429  $lang = $context->getLanguage();
430  $store = $this->getMessageBlobStore();
431  $blobs = $store->getBlobs( $modulesWithMessages, $lang );
432  foreach ( $blobs as $moduleName => $blob ) {
433  $modulesWithMessages[$moduleName]->setMessageBlob( $blob, $lang );
434  }
435  }
436 
443  public function loadModuleDependenciesInternal( $moduleName, $variant ) {
444  $deps = $this->depStore->retrieve( self::RL_DEP_STORE_PREFIX, "$moduleName|$variant" );
445 
446  return ResourceLoaderModule::expandRelativePaths( $deps['paths'] );
447  }
448 
456  public function saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths ) {
457  $hasPendingUpdate = (bool)$this->depStoreUpdateBuffer;
458  $entity = "$moduleName|$variant";
459 
460  if ( array_diff( $paths, $priorPaths ) || array_diff( $priorPaths, $paths ) ) {
461  // Dependency store needs to be updated with the new path list
462  if ( $paths ) {
463  $deps = $this->depStore->newEntityDependencies( $paths, time() );
464  $this->depStoreUpdateBuffer[$entity] = $deps;
465  } else {
466  $this->depStoreUpdateBuffer[$entity] = null;
467  }
468  } elseif ( $priorPaths ) {
469  // Dependency store needs to store the existing path list for longer
470  $this->depStoreUpdateBuffer[$entity] = '*';
471  }
472 
473  // Use a DeferrableUpdate to flush the buffered dependency updates...
474  if ( !$hasPendingUpdate ) {
476  $updatesByEntity = $this->depStoreUpdateBuffer;
477  $this->depStoreUpdateBuffer = []; // consume
479 
480  $scopeLocks = [];
481  $depsByEntity = [];
482  $entitiesUnreg = [];
483  $entitiesRenew = [];
484  foreach ( $updatesByEntity as $entity => $update ) {
485  $lockKey = $cache->makeKey( 'rl-deps', $entity );
486  $scopeLocks[$entity] = $cache->getScopedLock( $lockKey, 0 );
487  if ( !$scopeLocks[$entity] ) {
488  // avoid duplicate write request slams (T124649)
489  // the lock must be specific to the current wiki (T247028)
490  continue;
491  }
492  if ( $update === null ) {
493  $entitiesUnreg[] = $entity;
494  } elseif ( $update === '*' ) {
495  $entitiesRenew[] = $entity;
496  } else {
497  $depsByEntity[$entity] = $update;
498  }
499  }
500 
501  $ttl = self::RL_MODULE_DEP_TTL;
502  $this->depStore->storeMulti( self::RL_DEP_STORE_PREFIX, $depsByEntity, $ttl );
503  $this->depStore->remove( self::RL_DEP_STORE_PREFIX, $entitiesUnreg );
504  $this->depStore->renew( self::RL_DEP_STORE_PREFIX, $entitiesRenew, $ttl );
505  } );
506  }
507  }
508 
514  public function getSources() {
515  return $this->sources;
516  }
517 
526  public function getLoadScript( $source ) {
527  if ( !isset( $this->sources[$source] ) ) {
528  throw new UnexpectedValueException( "Unknown source '$source'" );
529  }
530  return $this->sources[$source];
531  }
532 
536  public const HASH_LENGTH = 5;
537 
600  public static function makeHash( $value ) {
601  $hash = hash( 'fnv132', $value );
602  // The base_convert will pad it (if too short),
603  // then substr() will trim it (if too long).
604  return substr(
605  Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
606  0,
607  self::HASH_LENGTH
608  );
609  }
610 
620  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
622  $this->logger->warning(
623  $msg,
624  $context + [ 'exception' => $e ]
625  );
626  $this->errors[] = self::formatExceptionNoComment( $e );
627  }
628 
637  public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
638  if ( !$moduleNames ) {
639  return '';
640  }
641  $hashes = array_map( function ( $module ) use ( $context ) {
642  try {
643  return $this->getModule( $module )->getVersionHash( $context );
644  } catch ( TimeoutException $e ) {
645  throw $e;
646  } catch ( Exception $e ) {
647  // If modules fail to compute a version, don't fail the request (T152266)
648  // and still compute versions of other modules.
649  $this->outputErrorAndLog( $e,
650  'Calculating version for "{module}" failed: {exception}',
651  [
652  'module' => $module,
653  ]
654  );
655  return '';
656  }
657  }, $moduleNames );
658  return self::makeHash( implode( '', $hashes ) );
659  }
660 
675  public function makeVersionQuery( ResourceLoaderContext $context, array $modules ) {
676  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
677  // version hashes. There is no technical reason for this to be same, and for years the
678  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
679  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
680  // query parameter), then this method must continue to match the JS one.
681  $filtered = [];
682  foreach ( $modules as $name ) {
683  if ( !$this->getModule( $name ) ) {
684  // If a versioned request contains a missing module, the version is a mismatch
685  // as the client considered a module (and version) we don't have.
686  return '';
687  }
688  $filtered[] = $name;
689  }
690  return $this->getCombinedVersion( $context, $filtered );
691  }
692 
698  public function respond( ResourceLoaderContext $context ) {
699  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
700  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
701  // is used: ob_clean() will clear the GZIP header in that case and it won't come
702  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
703  // the whole thing in our own output buffer to be sure the active buffer
704  // doesn't use ob_gzhandler.
705  // See https://bugs.php.net/bug.php?id=36514
706  ob_start();
707 
708  $responseTime = $this->measureResponseTime();
709 
710  $response = '';
711  try { // TimeoutException
712  // Find out which modules are missing and instantiate the others
713  $modules = [];
714  $missing = [];
715  foreach ( $context->getModules() as $name ) {
716  $module = $this->getModule( $name );
717  if ( $module ) {
718  // Do not allow private modules to be loaded from the web.
719  // This is a security issue, see T36907.
720  if ( $module->getGroup() === ResourceLoaderModule::GROUP_PRIVATE ) {
721  // Not a serious error, just means something is trying to access it (T101806)
722  $this->logger->debug( "Request for private module '$name' denied" );
723  $this->errors[] = "Cannot build private module \"$name\"";
724  continue;
725  }
726  $modules[$name] = $module;
727  } else {
728  $missing[] = $name;
729  }
730  }
731 
732  try {
733  // Preload for getCombinedVersion() and for batch makeModuleResponse()
734  $this->preloadModuleInfo( array_keys( $modules ), $context );
735  }
736  catch ( TimeoutException $e ) {
737  throw $e;
738  }
739  catch ( Exception $e ) {
740  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
741  }
742 
743  // Combine versions to propagate cache invalidation
744  $versionHash = '';
745  try {
746  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
747  }
748  catch ( TimeoutException $e ) {
749  throw $e;
750  }
751  catch ( Exception $e ) {
752  $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
753  }
754 
755  // See RFC 2616 § 3.11 Entity Tags
756  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
757  $etag = 'W/"' . $versionHash . '"';
758 
759  // Try the client-side cache first
760  if ( $this->tryRespondNotModified( $context, $etag ) ) {
761  return; // output handled (buffers cleared)
762  }
763 
764  // Use file cache if enabled and available...
765  if ( $this->config->get( MainConfigNames::UseFileCache ) ) {
766  $fileCache = ResourceFileCache::newFromContext( $context );
767  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
768  return; // output handled
769  }
770  } else {
771  $fileCache = null;
772  }
773 
774  // Generate a response
775  $response = $this->makeModuleResponse( $context, $modules, $missing );
776 
777  // Capture any PHP warnings from the output buffer and append them to the
778  // error list if we're in debug mode.
779  if ( $context->getDebug() ) {
780  $warnings = ob_get_contents();
781  if ( strlen( $warnings ) ) {
782  $this->errors[] = $warnings;
783  }
784  }
785 
786  // Consider saving the response to file cache (unless there are errors).
787  if ( $fileCache && !$this->errors && $missing === [] &&
788  ResourceFileCache::useFileCache( $context ) ) {
789  if ( $fileCache->isCacheWorthy() ) {
790  // There were enough hits, save the response to the cache
791  $fileCache->saveText( $response );
792  } else {
793  $fileCache->incrMissesRecent( $context->getRequest() );
794  }
795  }
796  } catch ( TimeoutException $e ) {
797  $this->outputErrorAndLog( $e, "Request timed out" );
798  }
799 
800  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
801 
802  // Remove the output buffer and output the response
803  ob_end_clean();
804 
805  if ( $context->getImageObj() && $this->errors ) {
806  // We can't show both the error messages and the response when it's an image.
807  $response = implode( "\n\n", $this->errors );
808  } elseif ( $this->errors ) {
809  $errorText = implode( "\n\n", $this->errors );
810  $errorResponse = self::makeComment( $errorText );
811  if ( $context->shouldIncludeScripts() ) {
812  $errorResponse .= 'if (window.console && console.error) { console.error('
813  . $context->encodeJson( $errorText )
814  . "); }\n";
815  }
816 
817  // Prepend error info to the response
818  $response = $errorResponse . $response;
819  }
820 
821  $this->errors = [];
822  // @phan-suppress-next-line SecurityCheck-XSS
823  echo $response;
824  }
825 
830  protected function measureResponseTime() {
831  $statStart = $_SERVER['REQUEST_TIME_FLOAT'];
832  return new ScopedCallback( static function () use ( $statStart ) {
833  $statTiming = microtime( true ) - $statStart;
834  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
835  $stats->timing( 'resourceloader.responseTime', $statTiming * 1000 );
836  } );
837  }
838 
849  protected function sendResponseHeaders(
850  ResourceLoaderContext $context, $etag, $errors, array $extra = []
851  ): void {
852  HeaderCallback::warnIfHeadersSent();
853  $rlMaxage = $this->config->get( MainConfigNames::ResourceLoaderMaxage );
854  // Use a short cache expiry so that updates propagate to clients quickly, if:
855  // - No version specified (shared resources, e.g. stylesheets)
856  // - There were errors (recover quickly)
857  // - Version mismatch (T117587, T47877)
858  if ( $context->getVersion() === null
859  || $errors
860  || $context->getVersion() !== $this->makeVersionQuery( $context, $context->getModules() )
861  ) {
862  $maxage = $rlMaxage['unversioned'];
863  // If a version was specified we can use a longer expiry time since changing
864  // version numbers causes cache misses
865  } else {
866  $maxage = $rlMaxage['versioned'];
867  }
868  if ( $context->getImageObj() ) {
869  // Output different headers if we're outputting textual errors.
870  if ( $errors ) {
871  header( 'Content-Type: text/plain; charset=utf-8' );
872  } else {
873  $context->getImageObj()->sendResponseHeaders( $context );
874  }
875  } elseif ( $context->getOnly() === 'styles' ) {
876  header( 'Content-Type: text/css; charset=utf-8' );
877  header( 'Access-Control-Allow-Origin: *' );
878  } else {
879  header( 'Content-Type: text/javascript; charset=utf-8' );
880  }
881  // See RFC 2616 § 14.19 ETag
882  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
883  header( 'ETag: ' . $etag );
884  if ( $context->getDebug() ) {
885  // Do not cache debug responses
886  header( 'Cache-Control: private, no-cache, must-revalidate' );
887  header( 'Pragma: no-cache' );
888  } else {
889  header( "Cache-Control: public, max-age=$maxage, s-maxage=$maxage" );
890  header( 'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
891  }
892  foreach ( $extra as $header ) {
893  header( $header );
894  }
895  }
896 
907  protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
908  // See RFC 2616 § 14.26 If-None-Match
909  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
910  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
911  // Never send 304s in debug mode
912  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
913  // There's another bug in ob_gzhandler (see also the comment at
914  // the top of this function) that causes it to gzip even empty
915  // responses, meaning it's impossible to produce a truly empty
916  // response (because the gzip header is always there). This is
917  // a problem because 304 responses have to be completely empty
918  // per the HTTP spec, and Firefox behaves buggily when they're not.
919  // See also https://bugs.php.net/bug.php?id=51579
920  // To work around this, we tear down all output buffering before
921  // sending the 304.
922  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
923 
924  HttpStatus::header( 304 );
925 
926  $this->sendResponseHeaders( $context, $etag, false );
927  return true;
928  }
929  return false;
930  }
931 
940  protected function tryRespondFromFileCache(
941  ResourceFileCache $fileCache,
942  ResourceLoaderContext $context,
943  $etag
944  ) {
945  $rlMaxage = $this->config->get( MainConfigNames::ResourceLoaderMaxage );
946  // Buffer output to catch warnings.
947  ob_start();
948  // Get the maximum age the cache can be
949  $maxage = $context->getVersion() === null
950  ? $rlMaxage['unversioned']
951  : $rlMaxage['versioned'];
952  // Minimum timestamp the cache file must have
953  $minTime = time() - $maxage;
954  $good = $fileCache->isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
955  if ( !$good ) {
956  try { // RL always hits the DB on file cache miss...
957  wfGetDB( DB_REPLICA );
958  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
959  $good = $fileCache->isCacheGood(); // cache existence check
960  }
961  }
962  if ( $good ) {
963  $ts = $fileCache->cacheTimestamp();
964  // Send content type and cache headers
965  $this->sendResponseHeaders( $context, $etag, false );
966  $response = $fileCache->fetchText();
967  // Capture any PHP warnings from the output buffer and append them to the
968  // response in a comment if we're in debug mode.
969  if ( $context->getDebug() ) {
970  $warnings = ob_get_contents();
971  if ( strlen( $warnings ) ) {
972  $response = self::makeComment( $warnings ) . $response;
973  }
974  }
975  // Remove the output buffer and output the response
976  ob_end_clean();
977  echo $response . "\n/* Cached {$ts} */";
978  return true; // cache hit
979  }
980  // Clear buffer
981  ob_end_clean();
982 
983  return false; // cache miss
984  }
985 
994  public static function makeComment( $text ) {
995  $encText = str_replace( '*/', '* /', $text );
996  return "/*\n$encText\n*/\n";
997  }
998 
1005  public static function formatException( Throwable $e ) {
1006  return self::makeComment( self::formatExceptionNoComment( $e ) );
1007  }
1008 
1016  protected static function formatExceptionNoComment( Throwable $e ) {
1019  }
1020 
1021  return MWExceptionHandler::getLogMessage( $e ) .
1022  "\nBacktrace:\n" .
1024  }
1025 
1037  public function makeModuleResponse( ResourceLoaderContext $context,
1038  array $modules, array $missing = []
1039  ) {
1040  $out = '';
1041  $states = [];
1042 
1043  if ( $modules === [] && $missing === [] ) {
1044  return <<<MESSAGE
1045 /* This file is the Web entry point for MediaWiki's ResourceLoader:
1046  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1047  no modules were requested. Max made me put this here. */
1048 MESSAGE;
1049  }
1050 
1051  $image = $context->getImageObj();
1052  if ( $image ) {
1053  $data = $image->getImageData( $context );
1054  if ( $data === false ) {
1055  $data = '';
1056  $this->errors[] = 'Image generation failed';
1057  }
1058  return $data;
1059  }
1060 
1061  foreach ( $missing as $name ) {
1062  $states[$name] = 'missing';
1063  }
1064 
1065  $only = $context->getOnly();
1066  $filter = $only === 'styles' ? 'minify-css' : 'minify-js';
1067  $debug = (bool)$context->getDebug();
1068 
1069  foreach ( $modules as $name => $module ) {
1070  try {
1071  $content = $module->getModuleContent( $context );
1072  $implementKey = $name . '@' . $module->getVersionHash( $context );
1073  $strContent = '';
1074 
1075  if ( isset( $content['headers'] ) ) {
1076  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1077  }
1078 
1079  // Append output
1080  switch ( $only ) {
1081  case 'scripts':
1082  $scripts = $content['scripts'];
1083  if ( is_string( $scripts ) ) {
1084  // Load scripts raw...
1085  $strContent = $scripts;
1086  } elseif ( is_array( $scripts ) ) {
1087  // ...except when $scripts is an array of URLs or an associative array
1088  $strContent = self::makeLoaderImplementScript(
1089  $context,
1090  $implementKey,
1091  $scripts,
1092  [],
1093  [],
1094  []
1095  );
1096  }
1097  break;
1098  case 'styles':
1099  $styles = $content['styles'];
1100  // We no longer separate into media, they are all combined now with
1101  // custom media type groups into @media .. {} sections as part of the css string.
1102  // Module returns either an empty array or a numerical array with css strings.
1103  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1104  break;
1105  default:
1106  $scripts = $content['scripts'] ?? '';
1107  if ( is_string( $scripts ) ) {
1108  if ( $name === 'site' || $name === 'user' ) {
1109  // Legacy scripts that run in the global scope without a closure.
1110  // mw.loader.implement will use eval if scripts is a string.
1111  // Minify manually here, because general response minification is
1112  // not effective due it being a string literal, not a function.
1113  if ( !$debug ) {
1114  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1115  }
1116  } else {
1117  $scripts = new XmlJsCode( $scripts );
1118  }
1119  }
1120  $strContent = self::makeLoaderImplementScript(
1121  $context,
1122  $implementKey,
1123  $scripts,
1124  $content['styles'] ?? [],
1125  // @phan-suppress-next-line SecurityCheck-XSS
1126  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1127  $content['templates'] ?? []
1128  );
1129  break;
1130  }
1131 
1132  if ( !$debug ) {
1133  $strContent = self::filter( $filter, $strContent, [
1134  // Important: Do not cache minifications of embedded modules
1135  // This is especially for the private 'user.options' module,
1136  // which varies on every pageview and would explode the cache (T84960)
1137  'cache' => !$module->shouldEmbedModule( $context )
1138  ] );
1139  } else {
1140  // In debug mode, separate each response by a new line.
1141  // For example, between 'mw.loader.implement();' statements.
1142  $strContent = self::ensureNewline( $strContent );
1143  }
1144 
1145  if ( $only === 'scripts' ) {
1146  // Use a linebreak between module scripts (T162719)
1147  $out .= self::ensureNewline( $strContent );
1148  } else {
1149  $out .= $strContent;
1150  }
1151 
1152  } catch ( TimeoutException $e ) {
1153  throw $e;
1154  } catch ( Exception $e ) {
1155  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1156 
1157  // Respond to client with error-state instead of module implementation
1158  $states[$name] = 'error';
1159  unset( $modules[$name] );
1160  }
1161  }
1162 
1163  // Update module states
1164  if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1165  if ( $modules && $only === 'scripts' ) {
1166  // Set the state of modules loaded as only scripts to ready as
1167  // they don't have an mw.loader.implement wrapper that sets the state
1168  foreach ( $modules as $name => $module ) {
1169  $states[$name] = 'ready';
1170  }
1171  }
1172 
1173  // Set the state of modules we didn't respond to with mw.loader.implement
1174  if ( $states ) {
1175  $stateScript = self::makeLoaderStateScript( $context, $states );
1176  if ( !$debug ) {
1177  $stateScript = self::filter( 'minify-js', $stateScript );
1178  }
1179  // Use a linebreak between module script and state script (T162719)
1180  $out = self::ensureNewline( $out ) . $stateScript;
1181  }
1182  } elseif ( $states ) {
1183  $this->errors[] = 'Problematic modules: '
1184  . $context->encodeJson( $states );
1185  }
1186 
1187  return $out;
1188  }
1189 
1196  public static function ensureNewline( $str ) {
1197  $end = substr( $str, -1 );
1198  if ( $end === false || $end === '' || $end === "\n" ) {
1199  return $str;
1200  }
1201  return $str . "\n";
1202  }
1203 
1210  public function getModulesByMessage( $messageKey ) {
1211  $moduleNames = [];
1212  foreach ( $this->getModuleNames() as $moduleName ) {
1213  $module = $this->getModule( $moduleName );
1214  if ( in_array( $messageKey, $module->getMessages() ) ) {
1215  $moduleNames[] = $moduleName;
1216  }
1217  }
1218  return $moduleNames;
1219  }
1220 
1238  private static function makeLoaderImplementScript(
1239  ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates
1240  ) {
1241  if ( $scripts instanceof XmlJsCode ) {
1242  if ( $scripts->value === '' ) {
1243  $scripts = null;
1244  } else {
1245  // @phan-suppress-next-line SecurityCheck-XSS
1246  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1247  }
1248  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1249  $files = $scripts['files'];
1250  foreach ( $files as $path => &$file ) {
1251  // $file is changed (by reference) from a descriptor array to the content of the file
1252  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1253  if ( $file['type'] === 'script' ) {
1254  // Ensure that the script has a newline at the end to close any comment in the
1255  // last line.
1256  $content = self::ensureNewline( $file['content'] );
1257  // Provide CJS `exports` (in addition to CJS2 `module.exports`) to package modules (T284511).
1258  // $/jQuery are simply used as globals instead.
1259  // TODO: Remove $/jQuery param from traditional module closure too (and bump caching)
1260  $file = new XmlJsCode( "function ( require, module, exports ) {\n$content}" );
1261  } else {
1262  $file = $file['content'];
1263  }
1264  }
1265  $scripts = XmlJsCode::encodeObject( [
1266  'main' => $scripts['main'],
1267  'files' => XmlJsCode::encodeObject( $files, true )
1268  ], true );
1269  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1270  throw new InvalidArgumentException( 'Script must be a string or an array of URLs' );
1271  }
1272 
1273  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1274  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1275  // of "{}". Force them to objects.
1276  $module = [
1277  $name,
1278  $scripts,
1279  (object)$styles,
1280  (object)$messages,
1281  (object)$templates
1282  ];
1283  self::trimArray( $module );
1284 
1285  // We use pretty output unconditionally to make this method simpler.
1286  // Minification is taken care of closer to the output.
1287  return Xml::encodeJsCall( 'mw.loader.implement', $module, true );
1288  }
1289 
1296  public static function makeMessageSetScript( $messages ) {
1297  return 'mw.messages.set('
1298  . self::encodeJsonForScript( (object)$messages )
1299  . ');';
1300  }
1301 
1309  public static function makeCombinedStyles( array $stylePairs ) {
1310  $out = [];
1311  foreach ( $stylePairs as $media => $styles ) {
1312  // ResourceLoaderFileModule::getStyle can return the styles
1313  // as a string or an array of strings. This is to allow separation in
1314  // the front-end.
1315  $styles = (array)$styles;
1316  foreach ( $styles as $style ) {
1317  $style = trim( $style );
1318  // Don't output an empty "@media print { }" block (T42498)
1319  if ( $style !== '' ) {
1320  // Transform the media type based on request params and config
1321  // The way that this relies on $wgRequest to propagate request params is slightly evil
1322  $media = OutputPage::transformCssMedia( $media );
1323 
1324  if ( $media === '' || $media == 'all' ) {
1325  $out[] = $style;
1326  } elseif ( is_string( $media ) ) {
1327  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1328  }
1329  // else: skip
1330  }
1331  }
1332  }
1333  return $out;
1334  }
1335 
1345  public static function encodeJsonForScript( $data ) {
1346  // Keep output as small as possible by disabling needless escape modes
1347  // that PHP uses by default.
1348  // However, while most module scripts are only served on HTTP responses
1349  // for JavaScript, some modules can also be embedded in the HTML as inline
1350  // scripts. This, and the fact that we sometimes need to export strings
1351  // containing user-generated content and labels that may genuinely contain
1352  // a sequences like "</script>", we need to encode either '/' or '<'.
1353  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1354  // and allows URLs to mostly remain readable.
1355  $jsonFlags = JSON_UNESCAPED_SLASHES |
1356  JSON_UNESCAPED_UNICODE |
1357  JSON_HEX_TAG |
1358  JSON_HEX_AMP;
1359  if ( self::inDebugMode() ) {
1360  $jsonFlags |= JSON_PRETTY_PRINT;
1361  }
1362  return json_encode( $data, $jsonFlags );
1363  }
1364 
1377  public static function makeLoaderStateScript(
1378  ResourceLoaderContext $context, array $states
1379  ) {
1380  return 'mw.loader.state('
1381  . $context->encodeJson( $states )
1382  . ');';
1383  }
1384 
1385  private static function isEmptyObject( stdClass $obj ) {
1386  foreach ( $obj as $key => $value ) {
1387  return false;
1388  }
1389  return true;
1390  }
1391 
1405  private static function trimArray( array &$array ): void {
1406  $i = count( $array );
1407  while ( $i-- ) {
1408  if ( $array[$i] === null
1409  || $array[$i] === []
1410  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1411  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1412  ) {
1413  unset( $array[$i] );
1414  } else {
1415  break;
1416  }
1417  }
1418  }
1419 
1445  public static function makeLoaderRegisterScript(
1446  ResourceLoaderContext $context, array $modules
1447  ) {
1448  // Optimisation: Transform dependency names into indexes when possible
1449  // to produce smaller output. They are expanded by mw.loader.register on
1450  // the other end.
1451  $index = [];
1452  foreach ( $modules as $i => &$module ) {
1453  // Build module name index
1454  $index[$module[0]] = $i;
1455  }
1456  foreach ( $modules as &$module ) {
1457  if ( isset( $module[2] ) ) {
1458  foreach ( $module[2] as &$dependency ) {
1459  if ( isset( $index[$dependency] ) ) {
1460  // Replace module name in dependency list with index
1461  $dependency = $index[$dependency];
1462  }
1463  }
1464  }
1465  }
1466 
1467  array_walk( $modules, [ self::class, 'trimArray' ] );
1468 
1469  return 'mw.loader.register('
1470  . $context->encodeJson( $modules )
1471  . ');';
1472  }
1473 
1487  public static function makeLoaderSourcesScript(
1488  ResourceLoaderContext $context, array $sources
1489  ) {
1490  return 'mw.loader.addSource('
1491  . $context->encodeJson( $sources )
1492  . ');';
1493  }
1494 
1501  public static function makeLoaderConditionalScript( $script ) {
1502  // Adds a function to lazy-created RLQ
1503  return '(RLQ=window.RLQ||[]).push(function(){' .
1504  trim( $script ) . '});';
1505  }
1506 
1515  public static function makeInlineCodeWithModule( $modules, $script ) {
1516  // Adds an array to lazy-created RLQ
1517  return '(RLQ=window.RLQ||[]).push(['
1518  . self::encodeJsonForScript( $modules ) . ','
1519  . 'function(){' . trim( $script ) . '}'
1520  . ']);';
1521  }
1522 
1534  public static function makeInlineScript( $script, $nonce = null ) {
1535  $js = self::makeLoaderConditionalScript( $script );
1536  $escNonce = '';
1537  if ( $nonce === null ) {
1538  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1539  } elseif ( $nonce !== false ) {
1540  // If it was false, CSP is disabled, so no nonce attribute.
1541  // Nonce should be only base64 characters, so should be safe,
1542  // but better to be safely escaped than sorry.
1543  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1544  }
1545 
1546  return new WrappedString(
1547  Html::inlineScript( $js, $nonce ),
1548  "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1549  '});</script>'
1550  );
1551  }
1552 
1561  public static function makeConfigSetScript( array $configuration ) {
1562  $json = self::encodeJsonForScript( $configuration );
1563  if ( $json === false ) {
1564  $e = new Exception(
1565  'JSON serialization of config data failed. ' .
1566  'This usually means the config data is not valid UTF-8.'
1567  );
1569  return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1570  }
1571  return "mw.config.set($json);";
1572  }
1573 
1587  public static function makePackedModulesString( array $modules ) {
1588  $moduleMap = []; // [ prefix => [ suffixes ] ]
1589  foreach ( $modules as $module ) {
1590  $pos = strrpos( $module, '.' );
1591  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1592  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1593  $moduleMap[$prefix][] = $suffix;
1594  }
1595 
1596  $arr = [];
1597  foreach ( $moduleMap as $prefix => $suffixes ) {
1598  $p = $prefix === '' ? '' : $prefix . '.';
1599  $arr[] = $p . implode( ',', $suffixes );
1600  }
1601  return implode( '|', $arr );
1602  }
1603 
1615  public static function expandModuleNames( $modules ) {
1616  $retval = [];
1617  $exploded = explode( '|', $modules );
1618  foreach ( $exploded as $group ) {
1619  if ( strpos( $group, ',' ) === false ) {
1620  // This is not a set of modules in foo.bar,baz notation
1621  // but a single module
1622  $retval[] = $group;
1623  } else {
1624  // This is a set of modules in foo.bar,baz notation
1625  $pos = strrpos( $group, '.' );
1626  if ( $pos === false ) {
1627  // Prefixless modules, i.e. without dots
1628  $retval = array_merge( $retval, explode( ',', $group ) );
1629  } else {
1630  // We have a prefix and a bunch of suffixes
1631  $prefix = substr( $group, 0, $pos ); // 'foo'
1632  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1633  foreach ( $suffixes as $suffix ) {
1634  $retval[] = "$prefix.$suffix";
1635  }
1636  }
1637  }
1638  }
1639  return $retval;
1640  }
1641 
1652  public static function inDebugMode() {
1653  if ( self::$debugMode === null ) {
1654  global $wgRequest;
1655  $resourceLoaderDebug = MediaWikiServices::getInstance()->getMainConfig()->get(
1656  MainConfigNames::ResourceLoaderDebug );
1657  $str = $wgRequest->getRawVal( 'debug',
1658  $wgRequest->getCookie( 'resourceLoaderDebug', '', $resourceLoaderDebug ? 'true' : '' )
1659  );
1660  self::$debugMode = ResourceLoaderContext::debugFromString( $str );
1661  }
1662  return self::$debugMode;
1663  }
1664 
1675  public static function clearCache() {
1676  self::$debugMode = null;
1677  }
1678 
1688  public function createLoaderURL( $source, ResourceLoaderContext $context,
1689  array $extraQuery = []
1690  ) {
1691  $query = self::createLoaderQuery( $context, $extraQuery );
1692  $script = $this->getLoadScript( $source );
1693 
1694  return wfAppendQuery( $script, $query );
1695  }
1696 
1706  protected static function createLoaderQuery(
1707  ResourceLoaderContext $context, array $extraQuery = []
1708  ) {
1709  return self::makeLoaderQuery(
1710  $context->getModules(),
1711  $context->getLanguage(),
1712  $context->getSkin(),
1713  $context->getUser(),
1714  $context->getVersion(),
1715  $context->getDebug(),
1716  $context->getOnly(),
1717  $context->getRequest()->getBool( 'printable' ),
1718  null,
1719  $extraQuery
1720  );
1721  }
1722 
1739  public static function makeLoaderQuery( array $modules, $lang, $skin, $user = null,
1740  $version = null, $debug = ResourceLoaderContext::DEBUG_OFF, $only = null,
1741  $printable = false, $handheld = null, array $extraQuery = []
1742  ) {
1743  $query = [
1744  'modules' => self::makePackedModulesString( $modules ),
1745  ];
1746  // Keep urls short by omitting query parameters that
1747  // match the defaults assumed by ResourceLoaderContext.
1748  // Note: This relies on the defaults either being insignificant or forever constant,
1749  // as otherwise cached urls could change in meaning when the defaults change.
1751  $query['lang'] = $lang;
1752  }
1753  if ( $skin !== ResourceLoaderContext::DEFAULT_SKIN ) {
1754  $query['skin'] = $skin;
1755  }
1757  $query['debug'] = strval( $debug );
1758  }
1759  if ( $user !== null ) {
1760  $query['user'] = $user;
1761  }
1762  if ( $version !== null ) {
1763  $query['version'] = $version;
1764  }
1765  if ( $only !== null ) {
1766  $query['only'] = $only;
1767  }
1768  if ( $printable ) {
1769  $query['printable'] = 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  $len = strlen( $moduleName );
1789  return $len <= 255 && strcspn( $moduleName, '!,|', 0, $len ) === $len;
1790  }
1791 
1803  public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1804  global $IP;
1805  // When called from the installer, it is possible that a required PHP extension
1806  // is missing (at least for now; see T49564). If this is the case, throw an
1807  // exception (caught by the installer) to prevent a fatal error later on.
1808  if ( !class_exists( Less_Parser::class ) ) {
1809  throw new MWException( 'MediaWiki requires the less.php parser' );
1810  }
1811 
1812  $importDirs[] = "$IP/resources/src/mediawiki.less";
1813 
1814  $parser = new Less_Parser;
1815  $parser->ModifyVars( $vars );
1816  // SetImportDirs expects an array like [ 'path1' => '', 'path2' => '' ]
1817  $parser->SetImportDirs( array_fill_keys( $importDirs, '' ) );
1818  $parser->SetOption( 'relativeUrls', false );
1819 
1820  return $parser;
1821  }
1822 
1836  public function expandUrl( string $base, string $url ): string {
1837  // Net_URL2::resolve() doesn't allow protocol-relative URLs, but we do.
1838  $isProtoRelative = strpos( $base, '//' ) === 0;
1839  if ( $isProtoRelative ) {
1840  $base = "https:$base";
1841  }
1842  // Net_URL2::resolve() takes care of throwing if $base doesn't have a server.
1843  $baseUrl = new Net_URL2( $base );
1844  $ret = $baseUrl->resolve( $url );
1845  if ( $isProtoRelative ) {
1846  $ret->setScheme( false );
1847  }
1848  return $ret->getURL();
1849  }
1850 
1868  public static function filter( $filter, $data, array $options = [] ) {
1869  if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
1870  return $data;
1871  }
1872 
1873  if ( isset( $options['cache'] ) && $options['cache'] === false ) {
1874  return self::applyFilter( $filter, $data ) ?? $data;
1875  }
1876 
1877  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1879 
1880  $key = $cache->makeGlobalKey(
1881  'resourceloader-filter',
1882  $filter,
1883  self::CACHE_VERSION,
1884  md5( $data )
1885  );
1886 
1887  $incKey = "resourceloader_cache.$filter.hit";
1888  $result = $cache->getWithSetCallback(
1889  $key,
1890  BagOStuff::TTL_DAY,
1891  function () use ( $filter, $data, &$incKey ) {
1892  $incKey = "resourceloader_cache.$filter.miss";
1893  return self::applyFilter( $filter, $data );
1894  }
1895  );
1896  $stats->increment( $incKey );
1897  if ( $result === null ) {
1898  // Cached failure
1899  $result = $data;
1900  }
1901 
1902  return $result;
1903  }
1904 
1910  private static function applyFilter( $filter, $data ) {
1911  $data = trim( $data );
1912  if ( $data ) {
1913  try {
1914  $data = ( $filter === 'minify-css' )
1915  ? CSSMin::minify( $data )
1916  : JavaScriptMinifier::minify( $data );
1917  } catch ( TimeoutException $e ) {
1918  throw $e;
1919  } catch ( Exception $e ) {
1921  return null;
1922  }
1923  }
1924  return $data;
1925  }
1926 
1934  public static function getUserDefaults( ResourceLoaderContext $context ): array {
1935  // TODO inject
1936  $defaultOptions = MediaWikiServices::getInstance()->getUserOptionsLookup()->getDefaultOptions();
1937  $keysToExclude = [];
1938  $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
1939  $hookRunner->onResourceLoaderExcludeUserOptions( $keysToExclude, $context );
1940  foreach ( $keysToExclude as $excludedKey ) {
1941  unset( $defaultOptions[ $excludedKey ] );
1942  }
1943  return $defaultOptions;
1944  }
1945 
1954  public static function getSiteConfigSettings(
1955  ResourceLoaderContext $context, Config $conf
1956  ): array {
1957  // Namespace related preparation
1958  // - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
1959  // - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
1960  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1961  $namespaceIds = $contLang->getNamespaceIds();
1962  $caseSensitiveNamespaces = [];
1963  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1964  foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
1965  $namespaceIds[$contLang->lc( $name )] = $index;
1966  if ( !$nsInfo->isCapitalized( $index ) ) {
1967  $caseSensitiveNamespaces[] = $index;
1968  }
1969  }
1970 
1971  $illegalFileChars = $conf->get( MainConfigNames::IllegalFileChars );
1972 
1973  // Build list of variables
1974  $skin = $context->getSkin();
1975 
1976  // Start of supported and stable config vars (for use by extensions/gadgets).
1977  $vars = [
1978  'debug' => $context->getDebug(),
1979  'skin' => $skin,
1980  'stylepath' => $conf->get( MainConfigNames::StylePath ),
1981  'wgArticlePath' => $conf->get( MainConfigNames::ArticlePath ),
1982  'wgScriptPath' => $conf->get( MainConfigNames::ScriptPath ),
1983  'wgScript' => $conf->get( MainConfigNames::Script ),
1984  'wgSearchType' => $conf->get( MainConfigNames::SearchType ),
1985  'wgVariantArticlePath' => $conf->get( MainConfigNames::VariantArticlePath ),
1986  'wgServer' => $conf->get( MainConfigNames::Server ),
1987  'wgServerName' => $conf->get( MainConfigNames::ServerName ),
1988  'wgUserLanguage' => $context->getLanguage(),
1989  'wgContentLanguage' => $contLang->getCode(),
1990  'wgVersion' => MW_VERSION,
1991  'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
1992  'wgNamespaceIds' => $namespaceIds,
1993  'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
1994  'wgSiteName' => $conf->get( MainConfigNames::Sitename ),
1995  'wgDBname' => $conf->get( MainConfigNames::DBname ),
1996  'wgWikiID' => WikiMap::getCurrentWikiId(),
1997  'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
1998  'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
1999  'wgExtensionAssetsPath' => $conf->get( MainConfigNames::ExtensionAssetsPath ),
2000  ];
2001  // End of stable config vars.
2002 
2003  // Internal variables for use by MediaWiki core and/or ResourceLoader.
2004  $vars += [
2005  // @internal For mediawiki.widgets
2006  'wgUrlProtocols' => wfUrlProtocols(),
2007  // @internal For mediawiki.page.watch
2008  // Force object to avoid "empty" associative array from
2009  // becoming [] instead of {} in JS (T36604)
2010  'wgActionPaths' => (object)$conf->get( MainConfigNames::ActionPaths ),
2011  // @internal For mediawiki.language
2012  'wgTranslateNumerals' => $conf->get( MainConfigNames::TranslateNumerals ),
2013  // @internal For mediawiki.Title
2014  'wgExtraSignatureNamespaces' => $conf->get( MainConfigNames::ExtraSignatureNamespaces ),
2015  'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
2016  'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
2017  ];
2018 
2019  Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );
2020 
2021  return $vars;
2022  }
2023 }
const CACHE_ANYTHING
Definition: Defines.php:85
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:36
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
$modules
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Definition: Setup.php:90
global $wgRequest
Definition: Setup.php:366
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
isCacheGood( $timestamp='')
Check if up to date cache file exists.
fetchText()
Get the uncompressed text from the cache.
cacheTimestamp()
Get the last-modified timestamp of the cache file.
Simple store for keeping values in an associative array for the current process.
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:593
static header( $code)
Output an HTTP status code header.
Definition: HttpStatus.php:96
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
static getPublicLogMessage(Throwable $e)
static getLogMessage(Throwable $e)
Get a message formatting the throwable message and its origin.
MediaWiki exception.
Definition: MWException.php:29
A class containing constants representing the names of configuration variables.
MediaWikiServices is the service locator for the application scope of MediaWiki.
onResourceLoaderExcludeUserOptions(array &$keysToExclude, ResourceLoaderContext $context)
Exclude a user option from the preloaded data for client-side mw.user.options.
Definition: HookRunner.php:29
This class generates message blobs for use by ResourceLoader.
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from configuration)
static getLocalClusterInstance()
Get the main cluster-local cache object.
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
ResourceLoader request result caching in the file system.
static useFileCache(ResourceLoaderContext $context)
Check if an RL request can be cached.
static newFromContext(ResourceLoaderContext $context)
Construct an ResourceFileCache from a context.
Context object that contains information about the state of a specific ResourceLoader web request.
static debugFromString(?string $debug)
getImageObj()
If this is a request for an image, get the ResourceLoaderImage object.
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
static expandRelativePaths(array $filePaths)
Expand directories relative to $IP.
static getVary(ResourceLoaderContext $context)
Get vary string.
static preloadTitleInfo(ResourceLoaderContext $context, IDatabase $db, array $moduleNames)
ResourceLoader is a loading system for JavaScript and CSS resources.
setDependencyStore(DependencyStore $tracker)
addSource( $sources, $loadUrl=null)
Add a foreign source of modules.
__construct(Config $config, LoggerInterface $logger=null, DependencyStore $tracker=null)
static formatException(Throwable $e)
Handle exception display.
tryRespondFromFileCache(ResourceFileCache $fileCache, ResourceLoaderContext $context, $etag)
Send out code for a response from file cache if possible.
HookRunner $hookRunner
setMessageBlobStore(MessageBlobStore $blobStore)
makeModuleResponse(ResourceLoaderContext $context, array $modules, array $missing=[])
Generate code for a response.
LoggerInterface $logger
setLogger(LoggerInterface $logger)
HookContainer $hookContainer
getLoadScript( $source)
Get the URL to the load.php endpoint for the given ResourceLoader source.
sendResponseHeaders(ResourceLoaderContext $context, $etag, $errors, array $extra=[])
Send main response headers to the client.
array[] $moduleInfos
Map of (module name => associative info array)
tryRespondNotModified(ResourceLoaderContext $context, $etag)
Respond with HTTP 304 Not Modified if appropriate.
getSources()
Get the list of sources.
loadModuleDependenciesInternal( $moduleName, $variant)
measureResponseTime()
Send stats about the time used to build the response.
DependencyStore $depStore
static int null $debugMode
string[] $extraHeaders
Buffer for extra response headers during a makeModuleResponse() call.
outputErrorAndLog(Exception $e, $msg, array $context=[])
Add an error to the 'errors' array and log it.
getTestSuiteModuleNames()
Get a list of module names with QUnit test suites.
saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths)
string[] $testSuiteModuleNames
List of module names that contain QUnit test suites.
getModule( $name)
Get the ResourceLoaderModule object for a given module name.
isModuleRegistered( $name)
Check whether a ResourceLoader module is registered.
setModuleSkinStyles(array $moduleSkinStyles)
ResourceLoaderModule[] $modules
Map of (module name => ResourceLoaderModule)
static formatExceptionNoComment(Throwable $e)
Handle exception display.
array $moduleSkinStyles
Styles that are skin-specific and supplement or replace the default skinStyles of a FileModule.
array $depStoreUpdateBuffer
Map of (module-variant => buffered DependencyStore updates)
MessageBlobStore $blobStore
array $errors
Errors accumulated during a respond() call.
static makeComment( $text)
Generate a CSS or JS comment block.
getCombinedVersion(ResourceLoaderContext $context, array $moduleNames)
Helper method to get and combine versions of multiple modules.
respond(ResourceLoaderContext $context)
Output a response to a load request, including the content-type header.
static makeHash( $value)
Create a hash for module versioning purposes.
makeVersionQuery(ResourceLoaderContext $context, array $modules)
Get the expected value of the 'version' query parameter.
string[] $sources
Map of (source => path); E.g.
preloadModuleInfo(array $moduleNames, ResourceLoaderContext $context)
Load information stored in the database and dependency tracking store about modules.
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:733
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition: Title.php:747
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
Definition: WebRequest.php:73
static getCurrentWikiId()
Definition: WikiMap.php:303
Class for tracking per-entity dependency path lists that are expensive to mass compute.
Lightweight class for tracking path dependencies lists via an object cache instance.
A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to interpret a given string a...
Definition: XmlJsCode.php:40
static encodeObject( $obj, $pretty=false)
Encode an object containing XmlJsCode objects.
Definition: XmlJsCode.php:59
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:696
Interface for configuration instances.
Definition: Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$debug
Definition: mcc.php:31
$cache
Definition: mcc.php:33
$source
const DB_REPLICA
Definition: defines.php:25
$content
Definition: router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
if(!isset( $args[0])) $lang
if(count( $args)< 1) $tracker
Definition: trackBlobs.php:37
$header