MediaWiki  master
ResourceLoader.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\ResourceLoader;
24 
25 use BagOStuff;
26 use Config;
27 use DeferredUpdates;
28 use Exception;
30 use HashBagOStuff;
31 use Hooks;
32 use Html;
33 use HttpStatus;
34 use InvalidArgumentException;
35 use Less_Parser;
42 use MWException;
45 use Net_URL2;
46 use ObjectCache;
47 use OutputPage;
48 use Psr\Log\LoggerAwareInterface;
49 use Psr\Log\LoggerInterface;
50 use Psr\Log\NullLogger;
52 use RuntimeException;
53 use stdClass;
54 use Throwable;
55 use Title;
56 use UnexpectedValueException;
57 use WebRequest;
58 use WikiMap;
61 use Wikimedia\Minify\CSSMin;
62 use Wikimedia\Minify\JavaScriptMinifier;
64 use Wikimedia\RequestTimeout\TimeoutException;
65 use Wikimedia\ScopedCallback;
66 use Wikimedia\Timestamp\ConvertibleTimestamp;
67 use Wikimedia\WrappedString;
68 use Xml;
69 use XmlJsCode;
70 
91 class ResourceLoader implements LoggerAwareInterface {
93  public const CACHE_VERSION = 9;
95  public const FILTER_NOMIN = '/*@nomin*/';
96 
98  private const RL_DEP_STORE_PREFIX = 'ResourceLoaderModule';
100  private const RL_MODULE_DEP_TTL = BagOStuff::TTL_YEAR;
102  private const MAXAGE_RECOVER = 60;
103 
105  protected static $debugMode = null;
106 
108  private $config;
110  private $blobStore;
112  private $depStore;
114  private $logger;
116  private $hookContainer;
118  private $hookRunner;
120  private $loadScript;
122  private $maxageVersioned;
124  private $maxageUnversioned;
126  private $useFileCache;
127 
129  private $modules = [];
131  private $moduleInfos = [];
133  private $testModuleNames = [];
135  private $sources = [];
137  protected $errors = [];
142  protected $extraHeaders = [];
144  private $depStoreUpdateBuffer = [];
149  private $moduleSkinStyles = [];
150 
174  public function __construct(
175  Config $config,
176  LoggerInterface $logger = null,
177  DependencyStore $tracker = null,
178  array $params = []
179  ) {
180  $this->loadScript = $params['loadScript'] ?? '/load.php';
181  $this->maxageVersioned = $params['maxageVersioned'] ?? 30 * 24 * 60 * 60;
182  $this->maxageUnversioned = $params['maxageUnversioned'] ?? 5 * 60;
183  $this->useFileCache = $params['useFileCache'] ?? false;
184 
185  $this->config = $config;
186  $this->logger = $logger ?: new NullLogger();
187 
188  $services = MediaWikiServices::getInstance();
189  $this->hookContainer = $services->getHookContainer();
190  $this->hookRunner = new HookRunner( $this->hookContainer );
191 
192  // Add 'local' source first
193  $this->addSource( 'local', $this->loadScript );
194 
195  // Special module that always exists
196  $this->register( 'startup', [ 'class' => StartUpModule::class ] );
197 
198  $this->setMessageBlobStore(
199  new MessageBlobStore( $this, $this->logger, $services->getMainWANObjectCache() )
200  );
201 
203  $this->setDependencyStore( $tracker );
204  }
205 
209  public function getConfig() {
210  return $this->config;
211  }
212 
217  public function setLogger( LoggerInterface $logger ) {
218  $this->logger = $logger;
219  }
220 
225  public function getLogger() {
226  return $this->logger;
227  }
228 
233  public function getMessageBlobStore() {
234  return $this->blobStore;
235  }
236 
241  public function setMessageBlobStore( MessageBlobStore $blobStore ) {
242  $this->blobStore = $blobStore;
243  }
244 
250  $this->depStore = $tracker;
251  }
252 
257  public function setModuleSkinStyles( array $moduleSkinStyles ) {
258  $this->moduleSkinStyles = $moduleSkinStyles;
259  }
260 
272  public function register( $name, array $info = null ) {
273  // Allow multiple modules to be registered in one call
274  $registrations = is_array( $name ) ? $name : [ $name => $info ];
275  foreach ( $registrations as $name => $info ) {
276  // Warn on duplicate registrations
277  if ( isset( $this->moduleInfos[$name] ) ) {
278  // A module has already been registered by this name
279  $this->logger->warning(
280  'ResourceLoader duplicate registration warning. ' .
281  'Another module has already been registered as ' . $name
282  );
283  }
284 
285  // Check validity
286  if ( !self::isValidModuleName( $name ) ) {
287  throw new InvalidArgumentException( "ResourceLoader module name '$name' is invalid, "
288  . "see ResourceLoader::isValidModuleName()" );
289  }
290  if ( !is_array( $info ) ) {
291  throw new InvalidArgumentException(
292  'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
293  );
294  }
295 
296  // Attach module
297  $this->moduleInfos[$name] = $info;
298  }
299  }
300 
305  public function registerTestModules(): void {
306  $extRegistry = ExtensionRegistry::getInstance();
307  $testModules = $extRegistry->getAttribute( 'QUnitTestModules' );
308 
309  $testModuleNames = [];
310  foreach ( $testModules as $name => &$module ) {
311  // Turn any single-module dependency into an array
312  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
313  $module['dependencies'] = [ $module['dependencies'] ];
314  }
315 
316  // Ensure the testrunner loads before any tests
317  $module['dependencies'][] = 'mediawiki.qunit-testrunner';
318 
319  // Keep track of the modules to load on SpecialJavaScriptTest
320  $testModuleNames[] = $name;
321  }
322 
323  // Core test modules (their names have further precedence).
324  $testModules = ( include MW_INSTALL_PATH . '/tests/qunit/QUnitTestResources.php' ) + $testModules;
325  $testModuleNames[] = 'test.MediaWiki';
326 
327  $this->register( $testModules );
328  $this->testModuleNames = $testModuleNames;
329  }
330 
341  public function addSource( $sources, $loadUrl = null ) {
342  if ( !is_array( $sources ) ) {
343  $sources = [ $sources => $loadUrl ];
344  }
345  foreach ( $sources as $id => $source ) {
346  // Disallow duplicates
347  if ( isset( $this->sources[$id] ) ) {
348  throw new RuntimeException( 'Cannot register source ' . $id . ' twice' );
349  }
350 
351  // Support: MediaWiki 1.24 and earlier
352  if ( is_array( $source ) ) {
353  if ( !isset( $source['loadScript'] ) ) {
354  throw new InvalidArgumentException( 'Each source must have a "loadScript" key' );
355  }
356  $source = $source['loadScript'];
357  }
358 
359  $this->sources[$id] = $source;
360  }
361  }
362 
366  public function getModuleNames() {
367  return array_keys( $this->moduleInfos );
368  }
369 
377  public function getTestSuiteModuleNames() {
378  return $this->testModuleNames;
379  }
380 
388  public function isModuleRegistered( $name ) {
389  return isset( $this->moduleInfos[$name] );
390  }
391 
403  public function getModule( $name ) {
404  if ( !isset( $this->modules[$name] ) ) {
405  if ( !isset( $this->moduleInfos[$name] ) ) {
406  // No such module
407  return null;
408  }
409  // Construct the requested module object
410  $info = $this->moduleInfos[$name];
411  if ( isset( $info['factory'] ) ) {
413  $object = call_user_func( $info['factory'], $info );
414  } else {
415  $class = $info['class'] ?? FileModule::class;
417  $object = new $class( $info );
418  }
419  $object->setConfig( $this->getConfig() );
420  $object->setLogger( $this->logger );
421  $object->setHookContainer( $this->hookContainer );
422  $object->setName( $name );
423  $object->setDependencyAccessCallbacks(
424  [ $this, 'loadModuleDependenciesInternal' ],
425  [ $this, 'saveModuleDependenciesInternal' ]
426  );
427  $object->setSkinStylesOverride( $this->moduleSkinStyles );
428  $this->modules[$name] = $object;
429  }
430 
431  return $this->modules[$name];
432  }
433 
440  public function preloadModuleInfo( array $moduleNames, Context $context ) {
441  // Load all tracked indirect file dependencies for the modules
442  $vary = Module::getVary( $context );
443  $entitiesByModule = [];
444  foreach ( $moduleNames as $moduleName ) {
445  $entitiesByModule[$moduleName] = "$moduleName|$vary";
446  }
447  $depsByEntity = $this->depStore->retrieveMulti(
448  self::RL_DEP_STORE_PREFIX,
449  $entitiesByModule
450  );
451  // Inject the indirect file dependencies for all the modules
452  foreach ( $moduleNames as $moduleName ) {
453  $module = $this->getModule( $moduleName );
454  if ( $module ) {
455  $entity = $entitiesByModule[$moduleName];
456  $deps = $depsByEntity[$entity];
457  $paths = Module::expandRelativePaths( $deps['paths'] );
458  $module->setFileDependencies( $context, $paths );
459  }
460  }
461 
462  // Batched version of WikiModule::getTitleInfo
463  $dbr = wfGetDB( DB_REPLICA );
464  WikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
465 
466  // Prime in-object cache for message blobs for modules with messages
467  $modulesWithMessages = [];
468  foreach ( $moduleNames as $moduleName ) {
469  $module = $this->getModule( $moduleName );
470  if ( $module && $module->getMessages() ) {
471  $modulesWithMessages[$moduleName] = $module;
472  }
473  }
474  // Prime in-object cache for message blobs for modules with messages
475  $lang = $context->getLanguage();
476  $store = $this->getMessageBlobStore();
477  $blobs = $store->getBlobs( $modulesWithMessages, $lang );
478  foreach ( $blobs as $moduleName => $blob ) {
479  $modulesWithMessages[$moduleName]->setMessageBlob( $blob, $lang );
480  }
481  }
482 
489  public function loadModuleDependenciesInternal( $moduleName, $variant ) {
490  $deps = $this->depStore->retrieve( self::RL_DEP_STORE_PREFIX, "$moduleName|$variant" );
491 
492  return Module::expandRelativePaths( $deps['paths'] );
493  }
494 
502  public function saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths ) {
503  $hasPendingUpdate = (bool)$this->depStoreUpdateBuffer;
504  $entity = "$moduleName|$variant";
505 
506  if ( array_diff( $paths, $priorPaths ) || array_diff( $priorPaths, $paths ) ) {
507  // Dependency store needs to be updated with the new path list
508  if ( $paths ) {
509  $deps = $this->depStore->newEntityDependencies( $paths, time() );
510  $this->depStoreUpdateBuffer[$entity] = $deps;
511  } else {
512  $this->depStoreUpdateBuffer[$entity] = null;
513  }
514  }
515 
516  // If paths were unchanged, leave the dependency store unchanged also.
517  // The entry will eventually expire, after which we will briefly issue an incomplete
518  // version hash for a 5-min startup window, the module then recomputes and rediscovers
519  // the paths and arrive at the same module version hash once again. It will churn
520  // part of the browser cache once, for clients connecting during that window.
521 
522  if ( !$hasPendingUpdate ) {
524  $updatesByEntity = $this->depStoreUpdateBuffer;
525  $this->depStoreUpdateBuffer = [];
527 
528  $scopeLocks = [];
529  $depsByEntity = [];
530  $entitiesUnreg = [];
531  foreach ( $updatesByEntity as $entity => $update ) {
532  $lockKey = $cache->makeKey( 'rl-deps', $entity );
533  $scopeLocks[$entity] = $cache->getScopedLock( $lockKey, 0 );
534  if ( !$scopeLocks[$entity] ) {
535  // avoid duplicate write request slams (T124649)
536  // the lock must be specific to the current wiki (T247028)
537  continue;
538  }
539  if ( $update === null ) {
540  $entitiesUnreg[] = $entity;
541  } else {
542  $depsByEntity[$entity] = $update;
543  }
544  }
545 
546  $ttl = self::RL_MODULE_DEP_TTL;
547  $this->depStore->storeMulti( self::RL_DEP_STORE_PREFIX, $depsByEntity, $ttl );
548  $this->depStore->remove( self::RL_DEP_STORE_PREFIX, $entitiesUnreg );
549  } );
550  }
551  }
552 
558  public function getSources() {
559  return $this->sources;
560  }
561 
570  public function getLoadScript( $source ) {
571  if ( !isset( $this->sources[$source] ) ) {
572  throw new UnexpectedValueException( "Unknown source '$source'" );
573  }
574  return $this->sources[$source];
575  }
576 
580  public const HASH_LENGTH = 5;
581 
644  public static function makeHash( $value ) {
645  $hash = hash( 'fnv132', $value );
646  // The base_convert will pad it (if too short),
647  // then substr() will trim it (if too long).
648  return substr(
649  \Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
650  0,
651  self::HASH_LENGTH
652  );
653  }
654 
664  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
666  $this->logger->warning(
667  $msg,
668  $context + [ 'exception' => $e ]
669  );
670  $this->errors[] = self::formatExceptionNoComment( $e );
671  }
672 
681  public function getCombinedVersion( Context $context, array $moduleNames ) {
682  if ( !$moduleNames ) {
683  return '';
684  }
685  $hashes = array_map( function ( $module ) use ( $context ) {
686  try {
687  return $this->getModule( $module )->getVersionHash( $context );
688  } catch ( TimeoutException $e ) {
689  throw $e;
690  } catch ( Exception $e ) {
691  // If modules fail to compute a version, don't fail the request (T152266)
692  // and still compute versions of other modules.
693  $this->outputErrorAndLog( $e,
694  'Calculating version for "{module}" failed: {exception}',
695  [
696  'module' => $module,
697  ]
698  );
699  return '';
700  }
701  }, $moduleNames );
702  return self::makeHash( implode( '', $hashes ) );
703  }
704 
719  public function makeVersionQuery( Context $context, array $modules ) {
720  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
721  // version hashes. There is no technical reason for this to be same, and for years the
722  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
723  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
724  // query parameter), then this method must continue to match the JS one.
725  $filtered = [];
726  foreach ( $modules as $name ) {
727  if ( !$this->getModule( $name ) ) {
728  // If a versioned request contains a missing module, the version is a mismatch
729  // as the client considered a module (and version) we don't have.
730  return '';
731  }
732  $filtered[] = $name;
733  }
734  return $this->getCombinedVersion( $context, $filtered );
735  }
736 
742  public function respond( Context $context ) {
743  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
744  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
745  // is used: ob_clean() will clear the GZIP header in that case and it won't come
746  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
747  // the whole thing in our own output buffer to be sure the active buffer
748  // doesn't use ob_gzhandler.
749  // See https://bugs.php.net/bug.php?id=36514
750  ob_start();
751 
752  $responseTime = $this->measureResponseTime();
753 
754  // Find out which modules are missing and instantiate the others
755  $modules = [];
756  $missing = [];
757  foreach ( $context->getModules() as $name ) {
758  $module = $this->getModule( $name );
759  if ( $module ) {
760  // Do not allow private modules to be loaded from the web.
761  // This is a security issue, see T36907.
762  if ( $module->getGroup() === Module::GROUP_PRIVATE ) {
763  // Not a serious error, just means something is trying to access it (T101806)
764  $this->logger->debug( "Request for private module '$name' denied" );
765  $this->errors[] = "Cannot build private module \"$name\"";
766  continue;
767  }
768  $modules[$name] = $module;
769  } else {
770  $missing[] = $name;
771  }
772  }
773 
774  try {
775  // Preload for getCombinedVersion() and for batch makeModuleResponse()
776  $this->preloadModuleInfo( array_keys( $modules ), $context );
777  } catch ( TimeoutException $e ) {
778  throw $e;
779  } catch ( Exception $e ) {
780  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
781  }
782 
783  // Combine versions to propagate cache invalidation
784  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
785 
786  // See RFC 2616 § 3.11 Entity Tags
787  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
788  $etag = 'W/"' . $versionHash . '"';
789 
790  // Try the client-side cache first
791  if ( $this->tryRespondNotModified( $context, $etag ) ) {
792  return; // output handled (buffers cleared)
793  }
794 
795  // Use file cache if enabled and available...
796  if ( $this->useFileCache ) {
797  $fileCache = ResourceFileCache::newFromContext( $context );
798  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
799  return; // output handled
800  }
801  } else {
802  $fileCache = null;
803  }
804 
805  // Generate a response
806  $response = $this->makeModuleResponse( $context, $modules, $missing );
807 
808  // Capture any PHP warnings from the output buffer and append them to the
809  // error list if we're in debug mode.
810  if ( $context->getDebug() ) {
811  $warnings = ob_get_contents();
812  if ( strlen( $warnings ) ) {
813  $this->errors[] = $warnings;
814  }
815  }
816 
817  // Consider saving the response to file cache (unless there are errors).
818  if ( $fileCache && !$this->errors && $missing === [] &&
819  ResourceFileCache::useFileCache( $context ) ) {
820  if ( $fileCache->isCacheWorthy() ) {
821  // There were enough hits, save the response to the cache
822  $fileCache->saveText( $response );
823  } else {
824  $fileCache->incrMissesRecent( $context->getRequest() );
825  }
826  }
827 
828  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
829 
830  // Remove the output buffer and output the response
831  ob_end_clean();
832 
833  if ( $context->getImageObj() && $this->errors ) {
834  // We can't show both the error messages and the response when it's an image.
835  $response = implode( "\n\n", $this->errors );
836  } elseif ( $this->errors ) {
837  $errorText = implode( "\n\n", $this->errors );
838  $errorResponse = self::makeComment( $errorText );
839  if ( $context->shouldIncludeScripts() ) {
840  $errorResponse .= 'if (window.console && console.error) { console.error('
841  . $context->encodeJson( $errorText )
842  . "); }\n";
843  }
844 
845  // Prepend error info to the response
846  $response = $errorResponse . $response;
847  }
848 
849  $this->errors = [];
850  // @phan-suppress-next-line SecurityCheck-XSS
851  echo $response;
852  }
853 
858  protected function measureResponseTime() {
859  $statStart = $_SERVER['REQUEST_TIME_FLOAT'];
860  return new ScopedCallback( static function () use ( $statStart ) {
861  $statTiming = microtime( true ) - $statStart;
862  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
863  $stats->timing( 'resourceloader.responseTime', $statTiming * 1000 );
864  } );
865  }
866 
877  protected function sendResponseHeaders(
878  Context $context, $etag, $errors, array $extra = []
879  ): void {
880  HeaderCallback::warnIfHeadersSent();
881 
882  if ( $errors
883  || (
884  $context->getVersion() !== null
885  && $context->getVersion() !== $this->makeVersionQuery( $context, $context->getModules() )
886  )
887  ) {
888  // If we need to self-correct, set a very short cache expiry
889  // to basically just debounce CDN traffic. This applies to:
890  // - Internal errors, e.g. due to misconfiguration.
891  // - Version mismatch, e.g. due to deployment race (T117587, T47877).
892  $maxage = self::MAXAGE_RECOVER;
893  } elseif ( $context->getVersion() === null ) {
894  // Resources that can't set a version, should have their updates propagate to
895  // clients quickly. This applies to shared resources linked from HTML, such as
896  // the startup module and stylesheets.
897  $maxage = $this->maxageUnversioned;
898  } else {
899  // When a version is set, use a long expiry because changes
900  // will naturally miss the cache by using a differente URL.
901  $maxage = $this->maxageVersioned;
902  }
903  if ( $context->getImageObj() ) {
904  // Output different headers if we're outputting textual errors.
905  if ( $errors ) {
906  header( 'Content-Type: text/plain; charset=utf-8' );
907  } else {
908  $context->getImageObj()->sendResponseHeaders( $context );
909  }
910  } elseif ( $context->getOnly() === 'styles' ) {
911  header( 'Content-Type: text/css; charset=utf-8' );
912  header( 'Access-Control-Allow-Origin: *' );
913  } else {
914  header( 'Content-Type: text/javascript; charset=utf-8' );
915  }
916  // See RFC 2616 § 14.19 ETag
917  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
918  header( 'ETag: ' . $etag );
919  if ( $context->getDebug() ) {
920  // Do not cache debug responses
921  header( 'Cache-Control: private, no-cache, must-revalidate' );
922  header( 'Pragma: no-cache' );
923  } else {
924  // T132418: When a resource expires mid-way a browsing session, prefer to renew it in
925  // the background instead of blocking the next page load (eg. startup module, or CSS).
926  $staleDirective = ( $maxage > self::MAXAGE_RECOVER
927  ? ", stale-while-revalidate=" . min( 60, intval( $maxage / 2 ) )
928  : ''
929  );
930  header( "Cache-Control: public, max-age=$maxage, s-maxage=$maxage" . $staleDirective );
931  header( 'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
932  }
933  foreach ( $extra as $header ) {
934  header( $header );
935  }
936  }
937 
948  protected function tryRespondNotModified( Context $context, $etag ) {
949  // See RFC 2616 § 14.26 If-None-Match
950  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
951  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
952  // Never send 304s in debug mode
953  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
954  // There's another bug in ob_gzhandler (see also the comment at
955  // the top of this function) that causes it to gzip even empty
956  // responses, meaning it's impossible to produce a truly empty
957  // response (because the gzip header is always there). This is
958  // a problem because 304 responses have to be completely empty
959  // per the HTTP spec, and Firefox behaves buggily when they're not.
960  // See also https://bugs.php.net/bug.php?id=51579
961  // To work around this, we tear down all output buffering before
962  // sending the 304.
963  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
964 
965  HttpStatus::header( 304 );
966 
967  $this->sendResponseHeaders( $context, $etag, false );
968  return true;
969  }
970  return false;
971  }
972 
981  protected function tryRespondFromFileCache(
982  ResourceFileCache $fileCache,
983  Context $context,
984  $etag
985  ) {
986  // Buffer output to catch warnings.
987  ob_start();
988  // Get the maximum age the cache can be
989  $maxage = $context->getVersion() === null
990  ? $this->maxageUnversioned
991  : $this->maxageVersioned;
992  // Minimum timestamp the cache file must have
993  $minTime = time() - $maxage;
994  $good = $fileCache->isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
995  if ( !$good ) {
996  try { // RL always hits the DB on file cache miss...
997  wfGetDB( DB_REPLICA );
998  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
999  $good = $fileCache->isCacheGood(); // cache existence check
1000  }
1001  }
1002  if ( $good ) {
1003  $ts = $fileCache->cacheTimestamp();
1004  // Send content type and cache headers
1005  $this->sendResponseHeaders( $context, $etag, false );
1006  $response = $fileCache->fetchText();
1007  // Capture any PHP warnings from the output buffer and append them to the
1008  // response in a comment if we're in debug mode.
1009  if ( $context->getDebug() ) {
1010  $warnings = ob_get_contents();
1011  if ( strlen( $warnings ) ) {
1012  $response = self::makeComment( $warnings ) . $response;
1013  }
1014  }
1015  // Remove the output buffer and output the response
1016  ob_end_clean();
1017  echo $response . "\n/* Cached {$ts} */";
1018  return true; // cache hit
1019  }
1020  // Clear buffer
1021  ob_end_clean();
1022 
1023  return false; // cache miss
1024  }
1025 
1034  public static function makeComment( $text ) {
1035  $encText = str_replace( '*/', '* /', $text );
1036  return "/*\n$encText\n*/\n";
1037  }
1038 
1045  public static function formatException( Throwable $e ) {
1046  return self::makeComment( self::formatExceptionNoComment( $e ) );
1047  }
1048 
1056  protected static function formatExceptionNoComment( Throwable $e ) {
1059  }
1060 
1061  return MWExceptionHandler::getLogMessage( $e ) .
1062  "\nBacktrace:\n" .
1064  }
1065 
1077  public function makeModuleResponse( Context $context,
1078  array $modules, array $missing = []
1079  ) {
1080  if ( $modules === [] && $missing === [] ) {
1081  return <<<MESSAGE
1082 /* This file is the Web entry point for MediaWiki's ResourceLoader:
1083  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1084  no modules were requested. Max made me put this here. */
1085 MESSAGE;
1086  }
1087 
1088  $image = $context->getImageObj();
1089  if ( $image ) {
1090  $data = $image->getImageData( $context );
1091  if ( $data === false ) {
1092  $data = '';
1093  $this->errors[] = 'Image generation failed';
1094  }
1095  return $data;
1096  }
1097 
1098  $states = [];
1099  foreach ( $missing as $name ) {
1100  $states[$name] = 'missing';
1101  }
1102 
1103  $only = $context->getOnly();
1104  $filter = $only === 'styles' ? 'minify-css' : 'minify-js';
1105  $debug = (bool)$context->getDebug();
1106 
1107  $out = '';
1108  foreach ( $modules as $name => $module ) {
1109  try {
1110  $content = $module->getModuleContent( $context );
1111  $implementKey = $name . '@' . $module->getVersionHash( $context );
1112  $strContent = '';
1113 
1114  if ( isset( $content['headers'] ) ) {
1115  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1116  }
1117 
1118  // Append output
1119  switch ( $only ) {
1120  case 'scripts':
1121  $scripts = $content['scripts'];
1122  if ( is_string( $scripts ) ) {
1123  // Load scripts raw...
1124  $strContent = $scripts;
1125  } elseif ( is_array( $scripts ) ) {
1126  // ...except when $scripts is an array of URLs or an associative array
1127  $strContent = self::makeLoaderImplementScript(
1128  $context,
1129  $implementKey,
1130  $scripts,
1131  [],
1132  [],
1133  []
1134  );
1135  }
1136  break;
1137  case 'styles':
1138  $styles = $content['styles'];
1139  // We no longer separate into media, they are all combined now with
1140  // custom media type groups into @media .. {} sections as part of the css string.
1141  // Module returns either an empty array or a numerical array with css strings.
1142  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1143  break;
1144  default:
1145  $scripts = $content['scripts'] ?? '';
1146  if ( is_string( $scripts ) ) {
1147  if ( $name === 'site' || $name === 'user' ) {
1148  // Legacy scripts that run in the global scope without a closure.
1149  // mw.loader.implement will use eval if scripts is a string.
1150  // Minify manually here, because general response minification is
1151  // not effective due it being a string literal, not a function.
1152  if ( !$debug ) {
1153  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1154  }
1155  } else {
1156  $scripts = new XmlJsCode( $scripts );
1157  }
1158  }
1159  $strContent = self::makeLoaderImplementScript(
1160  $context,
1161  $implementKey,
1162  $scripts,
1163  $content['styles'] ?? [],
1164  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1165  $content['templates'] ?? []
1166  );
1167  break;
1168  }
1169 
1170  if ( $debug ) {
1171  // In debug mode, separate each response by a new line.
1172  // For example, between 'mw.loader.implement();' statements.
1173  $strContent = self::ensureNewline( $strContent );
1174  } else {
1175  $strContent = self::filter( $filter, $strContent, [
1176  // Important: Do not cache minifications of embedded modules
1177  // This is especially for the private 'user.options' module,
1178  // which varies on every pageview and would explode the cache (T84960)
1179  'cache' => !$module->shouldEmbedModule( $context )
1180  ] );
1181  }
1182 
1183  if ( $only === 'scripts' ) {
1184  // Use a linebreak between module scripts (T162719)
1185  $out .= self::ensureNewline( $strContent );
1186  } else {
1187  $out .= $strContent;
1188  }
1189  } catch ( TimeoutException $e ) {
1190  throw $e;
1191  } catch ( Exception $e ) {
1192  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1193 
1194  // Respond to client with error-state instead of module implementation
1195  $states[$name] = 'error';
1196  unset( $modules[$name] );
1197  }
1198  }
1199 
1200  // Update module states
1201  if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1202  if ( $modules && $only === 'scripts' ) {
1203  // Set the state of modules loaded as only scripts to ready as
1204  // they don't have an mw.loader.implement wrapper that sets the state
1205  foreach ( $modules as $name => $module ) {
1206  $states[$name] = 'ready';
1207  }
1208  }
1209 
1210  // Set the state of modules we didn't respond to with mw.loader.implement
1211  if ( $states ) {
1212  $stateScript = self::makeLoaderStateScript( $context, $states );
1213  if ( !$debug ) {
1214  $stateScript = self::filter( 'minify-js', $stateScript );
1215  }
1216  // Use a linebreak between module script and state script (T162719)
1217  $out = self::ensureNewline( $out ) . $stateScript;
1218  }
1219  } elseif ( $states ) {
1220  $this->errors[] = 'Problematic modules: '
1221  . $context->encodeJson( $states );
1222  }
1223 
1224  return $out;
1225  }
1226 
1233  public static function ensureNewline( $str ) {
1234  $end = substr( $str, -1 );
1235  if ( $end === false || $end === '' || $end === "\n" ) {
1236  return $str;
1237  }
1238  return $str . "\n";
1239  }
1240 
1247  public function getModulesByMessage( $messageKey ) {
1248  $moduleNames = [];
1249  foreach ( $this->getModuleNames() as $moduleName ) {
1250  $module = $this->getModule( $moduleName );
1251  if ( in_array( $messageKey, $module->getMessages() ) ) {
1252  $moduleNames[] = $moduleName;
1253  }
1254  }
1255  return $moduleNames;
1256  }
1257 
1275  private static function makeLoaderImplementScript(
1276  Context $context, $name, $scripts, $styles, $messages, $templates
1277  ) {
1278  if ( $scripts instanceof XmlJsCode ) {
1279  if ( $scripts->value === '' ) {
1280  $scripts = null;
1281  } else {
1282  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1283  }
1284  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1285  $files = $scripts['files'];
1286  foreach ( $files as &$file ) {
1287  // $file is changed (by reference) from a descriptor array to the content of the file
1288  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1289  if ( $file['type'] === 'script' ) {
1290  // Ensure that the script has a newline at the end to close any comment in the
1291  // last line.
1292  $content = self::ensureNewline( $file['content'] );
1293  // Provide CJS `exports` (in addition to CJS2 `module.exports`) to package modules (T284511).
1294  // $/jQuery are simply used as globals instead.
1295  // TODO: Remove $/jQuery param from traditional module closure too (and bump caching)
1296  $file = new XmlJsCode( "function ( require, module, exports ) {\n$content}" );
1297  } else {
1298  $file = $file['content'];
1299  }
1300  }
1301  $scripts = XmlJsCode::encodeObject( [
1302  'main' => $scripts['main'],
1303  'files' => XmlJsCode::encodeObject( $files, true )
1304  ], true );
1305  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1306  throw new InvalidArgumentException( 'Script must be a string or an array of URLs' );
1307  }
1308 
1309  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1310  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1311  // of "{}". Force them to objects.
1312  $module = [
1313  $name,
1314  $scripts,
1315  (object)$styles,
1316  (object)$messages,
1317  (object)$templates
1318  ];
1319  self::trimArray( $module );
1320 
1321  // We use pretty output unconditionally to make this method simpler.
1322  // Minification is taken care of closer to the output.
1323  return Xml::encodeJsCall( 'mw.loader.implement', $module, true );
1324  }
1325 
1332  public static function makeMessageSetScript( $messages ) {
1333  return 'mw.messages.set('
1334  . self::encodeJsonForScript( (object)$messages )
1335  . ');';
1336  }
1337 
1345  public static function makeCombinedStyles( array $stylePairs ) {
1346  $out = [];
1347  foreach ( $stylePairs as $media => $styles ) {
1348  // FileModule::getStyle can return the styles as a string or an
1349  // array of strings. This is to allow separation in the front-end.
1350  $styles = (array)$styles;
1351  foreach ( $styles as $style ) {
1352  $style = trim( $style );
1353  // Don't output an empty "@media print { }" block (T42498)
1354  if ( $style === '' ) {
1355  continue;
1356  }
1357  // Transform the media type based on request params and config
1358  // The way that this relies on $wgRequest to propagate request params is slightly evil
1359  $media = OutputPage::transformCssMedia( $media );
1360 
1361  if ( $media === '' || $media == 'all' ) {
1362  $out[] = $style;
1363  } elseif ( is_string( $media ) ) {
1364  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1365  }
1366  // else: skip
1367  }
1368  }
1369  return $out;
1370  }
1371 
1381  public static function encodeJsonForScript( $data ) {
1382  // Keep output as small as possible by disabling needless escape modes
1383  // that PHP uses by default.
1384  // However, while most module scripts are only served on HTTP responses
1385  // for JavaScript, some modules can also be embedded in the HTML as inline
1386  // scripts. This, and the fact that we sometimes need to export strings
1387  // containing user-generated content and labels that may genuinely contain
1388  // a sequences like "</script>", we need to encode either '/' or '<'.
1389  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1390  // and allows URLs to mostly remain readable.
1391  $jsonFlags = JSON_UNESCAPED_SLASHES |
1392  JSON_UNESCAPED_UNICODE |
1393  JSON_HEX_TAG |
1394  JSON_HEX_AMP;
1395  if ( self::inDebugMode() ) {
1396  $jsonFlags |= JSON_PRETTY_PRINT;
1397  }
1398  return json_encode( $data, $jsonFlags );
1399  }
1400 
1413  public static function makeLoaderStateScript(
1414  Context $context, array $states
1415  ) {
1416  return 'mw.loader.state('
1417  . $context->encodeJson( $states )
1418  . ');';
1419  }
1420 
1421  private static function isEmptyObject( stdClass $obj ) {
1422  foreach ( $obj as $key => $value ) {
1423  return false;
1424  }
1425  return true;
1426  }
1427 
1441  private static function trimArray( array &$array ): void {
1442  $i = count( $array );
1443  while ( $i-- ) {
1444  if ( $array[$i] === null
1445  || $array[$i] === []
1446  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1447  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1448  ) {
1449  unset( $array[$i] );
1450  } else {
1451  break;
1452  }
1453  }
1454  }
1455 
1481  public static function makeLoaderRegisterScript(
1482  Context $context, array $modules
1483  ) {
1484  // Optimisation: Transform dependency names into indexes when possible
1485  // to produce smaller output. They are expanded by mw.loader.register on
1486  // the other end.
1487  $index = [];
1488  foreach ( $modules as $i => &$module ) {
1489  // Build module name index
1490  $index[$module[0]] = $i;
1491  }
1492  foreach ( $modules as &$module ) {
1493  if ( isset( $module[2] ) ) {
1494  foreach ( $module[2] as &$dependency ) {
1495  if ( isset( $index[$dependency] ) ) {
1496  // Replace module name in dependency list with index
1497  $dependency = $index[$dependency];
1498  }
1499  }
1500  }
1501  }
1502 
1503  array_walk( $modules, [ self::class, 'trimArray' ] );
1504 
1505  return 'mw.loader.register('
1506  . $context->encodeJson( $modules )
1507  . ');';
1508  }
1509 
1523  public static function makeLoaderSourcesScript(
1524  Context $context, array $sources
1525  ) {
1526  return 'mw.loader.addSource('
1527  . $context->encodeJson( $sources )
1528  . ');';
1529  }
1530 
1537  public static function makeLoaderConditionalScript( $script ) {
1538  // Adds a function to lazy-created RLQ
1539  return '(RLQ=window.RLQ||[]).push(function(){' .
1540  trim( $script ) . '});';
1541  }
1542 
1551  public static function makeInlineCodeWithModule( $modules, $script ) {
1552  // Adds an array to lazy-created RLQ
1553  return '(RLQ=window.RLQ||[]).push(['
1554  . self::encodeJsonForScript( $modules ) . ','
1555  . 'function(){' . trim( $script ) . '}'
1556  . ']);';
1557  }
1558 
1570  public static function makeInlineScript( $script, $nonce = null ) {
1571  $js = self::makeLoaderConditionalScript( $script );
1572  $escNonce = '';
1573  if ( $nonce === null ) {
1574  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1575  } elseif ( $nonce !== false ) {
1576  // If it was false, CSP is disabled, so no nonce attribute.
1577  // Nonce should be only base64 characters, so should be safe,
1578  // but better to be safely escaped than sorry.
1579  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1580  }
1581 
1582  return new WrappedString(
1583  Html::inlineScript( $js, $nonce ),
1584  "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1585  '});</script>'
1586  );
1587  }
1588 
1597  public static function makeConfigSetScript( array $configuration ) {
1598  $json = self::encodeJsonForScript( $configuration );
1599  if ( $json === false ) {
1600  $e = new Exception(
1601  'JSON serialization of config data failed. ' .
1602  'This usually means the config data is not valid UTF-8.'
1603  );
1605  return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1606  }
1607  return "mw.config.set($json);";
1608  }
1609 
1623  public static function makePackedModulesString( array $modules ) {
1624  $moduleMap = []; // [ prefix => [ suffixes ] ]
1625  foreach ( $modules as $module ) {
1626  $pos = strrpos( $module, '.' );
1627  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1628  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1629  $moduleMap[$prefix][] = $suffix;
1630  }
1631 
1632  $arr = [];
1633  foreach ( $moduleMap as $prefix => $suffixes ) {
1634  $p = $prefix === '' ? '' : $prefix . '.';
1635  $arr[] = $p . implode( ',', $suffixes );
1636  }
1637  return implode( '|', $arr );
1638  }
1639 
1651  public static function expandModuleNames( $modules ) {
1652  $retval = [];
1653  $exploded = explode( '|', $modules );
1654  foreach ( $exploded as $group ) {
1655  if ( strpos( $group, ',' ) === false ) {
1656  // This is not a set of modules in foo.bar,baz notation
1657  // but a single module
1658  $retval[] = $group;
1659  continue;
1660  }
1661  // This is a set of modules in foo.bar,baz notation
1662  $pos = strrpos( $group, '.' );
1663  if ( $pos === false ) {
1664  // Prefixless modules, i.e. without dots
1665  $retval = array_merge( $retval, explode( ',', $group ) );
1666  continue;
1667  }
1668  // We have a prefix and a bunch of suffixes
1669  $prefix = substr( $group, 0, $pos ); // 'foo'
1670  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1671  foreach ( $suffixes as $suffix ) {
1672  $retval[] = "$prefix.$suffix";
1673  }
1674  }
1675  return $retval;
1676  }
1677 
1688  public static function inDebugMode() {
1689  if ( self::$debugMode === null ) {
1690  global $wgRequest;
1691  $resourceLoaderDebug = MediaWikiServices::getInstance()->getMainConfig()->get(
1692  MainConfigNames::ResourceLoaderDebug );
1693  $str = $wgRequest->getRawVal( 'debug',
1694  $wgRequest->getCookie( 'resourceLoaderDebug', '', $resourceLoaderDebug ? 'true' : '' )
1695  );
1696  self::$debugMode = Context::debugFromString( $str );
1697  }
1698  return self::$debugMode;
1699  }
1700 
1711  public static function clearCache() {
1712  self::$debugMode = null;
1713  }
1714 
1724  public function createLoaderURL( $source, Context $context,
1725  array $extraQuery = []
1726  ) {
1727  $query = self::createLoaderQuery( $context, $extraQuery );
1728  $script = $this->getLoadScript( $source );
1729 
1730  return wfAppendQuery( $script, $query );
1731  }
1732 
1742  protected static function createLoaderQuery(
1743  Context $context, array $extraQuery = []
1744  ) {
1745  return self::makeLoaderQuery(
1746  $context->getModules(),
1747  $context->getLanguage(),
1748  $context->getSkin(),
1749  $context->getUser(),
1750  $context->getVersion(),
1751  $context->getDebug(),
1752  $context->getOnly(),
1753  $context->getRequest()->getBool( 'printable' ),
1754  null,
1755  $extraQuery
1756  );
1757  }
1758 
1775  public static function makeLoaderQuery( array $modules, $lang, $skin, $user = null,
1776  $version = null, $debug = Context::DEBUG_OFF, $only = null,
1777  $printable = false, $handheld = null, array $extraQuery = []
1778  ) {
1779  $query = [
1780  'modules' => self::makePackedModulesString( $modules ),
1781  ];
1782  // Keep urls short by omitting query parameters that
1783  // match the defaults assumed by Context.
1784  // Note: This relies on the defaults either being insignificant or forever constant,
1785  // as otherwise cached urls could change in meaning when the defaults change.
1786  if ( $lang !== Context::DEFAULT_LANG ) {
1787  $query['lang'] = $lang;
1788  }
1789  if ( $skin !== Context::DEFAULT_SKIN ) {
1790  $query['skin'] = $skin;
1791  }
1792  if ( $debug !== Context::DEBUG_OFF ) {
1793  $query['debug'] = strval( $debug );
1794  }
1795  if ( $user !== null ) {
1796  $query['user'] = $user;
1797  }
1798  if ( $version !== null ) {
1799  $query['version'] = $version;
1800  }
1801  if ( $only !== null ) {
1802  $query['only'] = $only;
1803  }
1804  if ( $printable ) {
1805  $query['printable'] = 1;
1806  }
1807  $query += $extraQuery;
1808 
1809  // Make queries uniform in order
1810  ksort( $query );
1811  return $query;
1812  }
1813 
1823  public static function isValidModuleName( $moduleName ) {
1824  $len = strlen( $moduleName );
1825  return $len <= 255 && strcspn( $moduleName, '!,|', 0, $len ) === $len;
1826  }
1827 
1839  public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1840  global $IP;
1841  // When called from the installer, it is possible that a required PHP extension
1842  // is missing (at least for now; see T49564). If this is the case, throw an
1843  // exception (caught by the installer) to prevent a fatal error later on.
1844  if ( !class_exists( Less_Parser::class ) ) {
1845  throw new MWException( 'MediaWiki requires the less.php parser' );
1846  }
1847 
1848  $importDirs[] = "$IP/resources/src/mediawiki.less";
1849 
1850  $parser = new Less_Parser;
1851  $parser->ModifyVars( $vars );
1852  // SetImportDirs expects an array like [ 'path1' => '', 'path2' => '' ]
1853  $parser->SetImportDirs( array_fill_keys( $importDirs, '' ) );
1854  $parser->SetOption( 'relativeUrls', false );
1855 
1856  return $parser;
1857  }
1858 
1872  public function expandUrl( string $base, string $url ): string {
1873  // Net_URL2::resolve() doesn't allow protocol-relative URLs, but we do.
1874  $isProtoRelative = strpos( $base, '//' ) === 0;
1875  if ( $isProtoRelative ) {
1876  $base = "https:$base";
1877  }
1878  // Net_URL2::resolve() takes care of throwing if $base doesn't have a server.
1879  $baseUrl = new Net_URL2( $base );
1880  $ret = $baseUrl->resolve( $url );
1881  if ( $isProtoRelative ) {
1882  $ret->setScheme( false );
1883  }
1884  return $ret->getURL();
1885  }
1886 
1904  public static function filter( $filter, $data, array $options = [] ) {
1905  if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
1906  return $data;
1907  }
1908 
1909  if ( isset( $options['cache'] ) && $options['cache'] === false ) {
1910  return self::applyFilter( $filter, $data ) ?? $data;
1911  }
1912 
1913  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1915 
1916  $key = $cache->makeGlobalKey(
1917  'resourceloader-filter',
1918  $filter,
1919  self::CACHE_VERSION,
1920  md5( $data )
1921  );
1922 
1923  $incKey = "resourceloader_cache.$filter.hit";
1924  $result = $cache->getWithSetCallback(
1925  $key,
1926  BagOStuff::TTL_DAY,
1927  function () use ( $filter, $data, &$incKey ) {
1928  $incKey = "resourceloader_cache.$filter.miss";
1929  return self::applyFilter( $filter, $data );
1930  }
1931  );
1932  $stats->increment( $incKey );
1933 
1934  // Use $data on cache failure
1935  return $result ?? $data;
1936  }
1937 
1943  private static function applyFilter( $filter, $data ) {
1944  $data = trim( $data );
1945  if ( $data ) {
1946  try {
1947  $data = ( $filter === 'minify-css' )
1948  ? CSSMin::minify( $data )
1949  : JavaScriptMinifier::minify( $data );
1950  } catch ( TimeoutException $e ) {
1951  throw $e;
1952  } catch ( Exception $e ) {
1954  return null;
1955  }
1956  }
1957  return $data;
1958  }
1959 
1971  public static function getUserDefaults(
1972  Context $context,
1973  HookContainer $hookContainer,
1974  UserOptionsLookup $userOptionsLookup
1975  ): array {
1976  $defaultOptions = $userOptionsLookup->getDefaultOptions();
1977  $keysToExclude = [];
1978  $hookRunner = new HookRunner( $hookContainer );
1979  $hookRunner->onResourceLoaderExcludeUserOptions( $keysToExclude, $context );
1980  foreach ( $keysToExclude as $excludedKey ) {
1981  unset( $defaultOptions[ $excludedKey ] );
1982  }
1983  return $defaultOptions;
1984  }
1985 
1994  public static function getSiteConfigSettings(
1995  Context $context, Config $conf
1996  ): array {
1997  // Namespace related preparation
1998  // - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
1999  // - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
2000  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2001  $namespaceIds = $contLang->getNamespaceIds();
2002  $caseSensitiveNamespaces = [];
2003  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2004  foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
2005  $namespaceIds[$contLang->lc( $name )] = $index;
2006  if ( !$nsInfo->isCapitalized( $index ) ) {
2007  $caseSensitiveNamespaces[] = $index;
2008  }
2009  }
2010 
2011  $illegalFileChars = $conf->get( MainConfigNames::IllegalFileChars );
2012 
2013  // Build list of variables
2014  $skin = $context->getSkin();
2015 
2016  // Start of supported and stable config vars (for use by extensions/gadgets).
2017  $vars = [
2018  'debug' => $context->getDebug(),
2019  'skin' => $skin,
2020  'stylepath' => $conf->get( MainConfigNames::StylePath ),
2021  'wgArticlePath' => $conf->get( MainConfigNames::ArticlePath ),
2022  'wgScriptPath' => $conf->get( MainConfigNames::ScriptPath ),
2023  'wgScript' => $conf->get( MainConfigNames::Script ),
2024  'wgSearchType' => $conf->get( MainConfigNames::SearchType ),
2025  'wgVariantArticlePath' => $conf->get( MainConfigNames::VariantArticlePath ),
2026  'wgServer' => $conf->get( MainConfigNames::Server ),
2027  'wgServerName' => $conf->get( MainConfigNames::ServerName ),
2028  'wgUserLanguage' => $context->getLanguage(),
2029  'wgContentLanguage' => $contLang->getCode(),
2030  'wgVersion' => MW_VERSION,
2031  'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
2032  'wgNamespaceIds' => $namespaceIds,
2033  'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
2034  'wgSiteName' => $conf->get( MainConfigNames::Sitename ),
2035  'wgDBname' => $conf->get( MainConfigNames::DBname ),
2036  'wgWikiID' => WikiMap::getCurrentWikiId(),
2037  'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
2038  'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
2039  'wgExtensionAssetsPath' => $conf->get( MainConfigNames::ExtensionAssetsPath ),
2040  ];
2041  // End of stable config vars.
2042 
2043  // Internal variables for use by MediaWiki core and/or ResourceLoader.
2044  $vars += [
2045  // @internal For mediawiki.widgets
2046  'wgUrlProtocols' => wfUrlProtocols(),
2047  // @internal For mediawiki.page.watch
2048  // Force object to avoid "empty" associative array from
2049  // becoming [] instead of {} in JS (T36604)
2050  'wgActionPaths' => (object)$conf->get( MainConfigNames::ActionPaths ),
2051  // @internal For mediawiki.language
2052  'wgTranslateNumerals' => $conf->get( MainConfigNames::TranslateNumerals ),
2053  // @internal For mediawiki.Title
2054  'wgExtraSignatureNamespaces' => $conf->get( MainConfigNames::ExtraSignatureNamespaces ),
2055  'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
2056  'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
2057  ];
2058 
2059  Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );
2060 
2061  return $vars;
2062  }
2063 }
2064 
2065 class_alias( ResourceLoader::class, 'ResourceLoader' );
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:91
global $wgRequest
Definition: Setup.php:388
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:85
Class for managing the deferral of updates within the scope of a PHP script invocation.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
The Registry loads JSON files, and uses a Processor to extract information from them.
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.
Hooks class.
Definition: Hooks.php:38
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
This class is a collection of static functions that serve two purposes:
Definition: Html.php:51
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
Handler class for MWExceptions.
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.
Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
MediaWiki exception.
Definition: MWException.php:30
Handle database storage of comments such as edit summaries and log reasons.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:560
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: Context.php:46
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
Definition: Context.php:493
getImageObj()
If this is a request for an image, get the Image object.
Definition: Context.php:374
This class generates message blobs for use by ResourceLoader.
static expandRelativePaths(array $filePaths)
Expand directories relative to $IP.
Definition: Module.php:642
static getVary(Context $context)
Get vary string.
Definition: Module.php:1114
ResourceLoader is a loading system for JavaScript and CSS resources.
getLoadScript( $source)
Get the URL to the load.php endpoint for the given ResourceLoader source.
static makeComment( $text)
Generate a CSS or JS comment block.
respond(Context $context)
Output a response to a load request, including the content-type header.
isModuleRegistered( $name)
Check whether a ResourceLoader module is registered.
loadModuleDependenciesInternal( $moduleName, $variant)
preloadModuleInfo(array $moduleNames, Context $context)
Load information stored in the database and dependency tracking store about modules.
static formatException(Throwable $e)
Handle exception display.
__construct(Config $config, LoggerInterface $logger=null, DependencyStore $tracker=null, array $params=[])
getSources()
Get the list of sources.
setMessageBlobStore(MessageBlobStore $blobStore)
tryRespondNotModified(Context $context, $etag)
Respond with HTTP 304 Not Modified if appropriate.
static formatExceptionNoComment(Throwable $e)
Handle exception display.
sendResponseHeaders(Context $context, $etag, $errors, array $extra=[])
Send main response headers to the client.
measureResponseTime()
Send stats about the time used to build the response.
setDependencyStore(DependencyStore $tracker)
static makeHash( $value)
Create a hash for module versioning purposes.
array $errors
Errors accumulated during a respond() call.
saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths)
getTestSuiteModuleNames()
Get a list of modules with QUnit tests.
makeModuleResponse(Context $context, array $modules, array $missing=[])
Generate code for a response.
getModule( $name)
Get the Module object for a given module name.
setModuleSkinStyles(array $moduleSkinStyles)
outputErrorAndLog(Exception $e, $msg, array $context=[])
Add an error to the 'errors' array and log it.
makeVersionQuery(Context $context, array $modules)
Get the expected value of the 'version' query parameter.
tryRespondFromFileCache(ResourceFileCache $fileCache, Context $context, $etag)
Send out code for a response from file cache if possible.
string[] $extraHeaders
Buffer for extra response headers during a makeModuleResponse() call.
getCombinedVersion(Context $context, array $moduleNames)
Helper method to get and combine versions of multiple modules.
addSource( $sources, $loadUrl=null)
Add a foreign source of modules.
static preloadTitleInfo(Context $context, IDatabase $db, array $moduleNames)
Definition: WikiModule.php:588
Provides access to user options.
Functions to get cache objects.
Definition: ObjectCache.php:65
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from configuration)
static getLocalClusterInstance()
Get the main cluster-local cache object.
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:57
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
ResourceLoader request result caching in the file system.
static useFileCache(RL\Context $context)
Check if an RL request can be cached.
static newFromContext(RL\Context $context)
Construct an ResourceFileCache from a context.
Represents a title within MediaWiki.
Definition: Title.php:52
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:737
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition: Title.php:751
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:47
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
Definition: WebRequest.php:77
Helper tools for dealing with other locally-hosted wikis.
Definition: WikiMap.php:30
static getCurrentWikiId()
Definition: WikiMap.php:300
Track per-module dependency file paths that are expensive to mass compute.
Track per-module file dependencies in object cache via BagOStuff.
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
Module of static functions for generating XML.
Definition: Xml.php:30
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:693
Interface for configuration instances.
Definition: Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$source
const DB_REPLICA
Definition: defines.php:26
$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