MediaWiki  master
ResourceLoader.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\ResourceLoader;
24 
25 use BagOStuff;
26 use CommentStore;
27 use Config;
28 use DeferredUpdates;
29 use Exception;
31 use HashBagOStuff;
32 use Hooks;
33 use Html;
34 use HttpStatus;
35 use InvalidArgumentException;
36 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 
89 class Context72Hack extends Context {
90 }
91 
100 class ResourceLoader implements LoggerAwareInterface {
102  public const CACHE_VERSION = 9;
104  public const FILTER_NOMIN = '/*@nomin*/';
105 
107  private const RL_DEP_STORE_PREFIX = 'ResourceLoaderModule';
109  private const RL_MODULE_DEP_TTL = BagOStuff::TTL_YEAR;
111  private const MAXAGE_RECOVER = 60;
112 
114  protected static $debugMode = null;
115 
117  private $config;
119  private $blobStore;
121  private $depStore;
123  private $logger;
125  private $hookContainer;
127  private $hookRunner;
129  private $loadScript;
131  private $maxageVersioned;
133  private $maxageUnversioned;
135  private $useFileCache;
136 
138  private $modules = [];
140  private $moduleInfos = [];
142  private $testModuleNames = [];
144  private $sources = [];
146  protected $errors = [];
151  protected $extraHeaders = [];
153  private $depStoreUpdateBuffer = [];
158  private $moduleSkinStyles = [];
159 
183  public function __construct(
184  Config $config,
185  LoggerInterface $logger = null,
186  DependencyStore $tracker = null,
187  array $params = []
188  ) {
189  $this->loadScript = $params['loadScript'] ?? '/load.php';
190  $this->maxageVersioned = $params['maxageVersioned'] ?? 30 * 24 * 60 * 60;
191  $this->maxageUnversioned = $params['maxageUnversioned'] ?? 5 * 60;
192  $this->useFileCache = $params['useFileCache'] ?? false;
193 
194  $this->config = $config;
195  $this->logger = $logger ?: new NullLogger();
196 
197  $services = MediaWikiServices::getInstance();
198  $this->hookContainer = $services->getHookContainer();
199  $this->hookRunner = new HookRunner( $this->hookContainer );
200 
201  // Add 'local' source first
202  $this->addSource( 'local', $this->loadScript );
203 
204  // Special module that always exists
205  $this->register( 'startup', [ 'class' => StartUpModule::class ] );
206 
207  $this->setMessageBlobStore(
208  new MessageBlobStore( $this, $this->logger, $services->getMainWANObjectCache() )
209  );
210 
212  $this->setDependencyStore( $tracker );
213  }
214 
218  public function getConfig() {
219  return $this->config;
220  }
221 
226  public function setLogger( LoggerInterface $logger ) {
227  $this->logger = $logger;
228  }
229 
234  public function getLogger() {
235  return $this->logger;
236  }
237 
242  public function getMessageBlobStore() {
243  return $this->blobStore;
244  }
245 
250  public function setMessageBlobStore( MessageBlobStore $blobStore ) {
251  $this->blobStore = $blobStore;
252  }
253 
259  $this->depStore = $tracker;
260  }
261 
266  public function setModuleSkinStyles( array $moduleSkinStyles ) {
267  $this->moduleSkinStyles = $moduleSkinStyles;
268  }
269 
281  public function register( $name, array $info = null ) {
282  // Allow multiple modules to be registered in one call
283  $registrations = is_array( $name ) ? $name : [ $name => $info ];
284  foreach ( $registrations as $name => $info ) {
285  // Warn on duplicate registrations
286  if ( isset( $this->moduleInfos[$name] ) ) {
287  // A module has already been registered by this name
288  $this->logger->warning(
289  'ResourceLoader duplicate registration warning. ' .
290  'Another module has already been registered as ' . $name
291  );
292  }
293 
294  // Check validity
295  if ( !self::isValidModuleName( $name ) ) {
296  throw new InvalidArgumentException( "ResourceLoader module name '$name' is invalid, "
297  . "see ResourceLoader::isValidModuleName()" );
298  }
299  if ( !is_array( $info ) ) {
300  throw new InvalidArgumentException(
301  'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
302  );
303  }
304 
305  // Attach module
306  $this->moduleInfos[$name] = $info;
307  }
308  }
309 
314  public function registerTestModules(): void {
315  $testModulesMeta = [ 'qunit' => [] ];
316  $this->hookRunner->onResourceLoaderTestModules( $testModulesMeta, $this );
317 
318  $extRegistry = ExtensionRegistry::getInstance();
319  // In case of conflict, the deprecated hook has precedence.
320  $testModules = $testModulesMeta['qunit']
321  + $extRegistry->getAttribute( 'QUnitTestModules' );
322 
323  $testModuleNames = [];
324  foreach ( $testModules as $name => &$module ) {
325  // Turn any single-module dependency into an array
326  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
327  $module['dependencies'] = [ $module['dependencies'] ];
328  }
329 
330  // Ensure the testrunner loads before any tests
331  $module['dependencies'][] = 'mediawiki.qunit-testrunner';
332 
333  // Keep track of the modules to load on SpecialJavaScriptTest
334  $testModuleNames[] = $name;
335  }
336 
337  // Core test modules (their names have further precedence).
338  $testModules = ( include MW_INSTALL_PATH . '/tests/qunit/QUnitTestResources.php' ) + $testModules;
339  $testModuleNames[] = 'test.MediaWiki';
340 
341  $this->register( $testModules );
342  $this->testModuleNames = $testModuleNames;
343  }
344 
355  public function addSource( $sources, $loadUrl = null ) {
356  if ( !is_array( $sources ) ) {
357  $sources = [ $sources => $loadUrl ];
358  }
359  foreach ( $sources as $id => $source ) {
360  // Disallow duplicates
361  if ( isset( $this->sources[$id] ) ) {
362  throw new RuntimeException( 'Cannot register source ' . $id . ' twice' );
363  }
364 
365  // Support: MediaWiki 1.24 and earlier
366  if ( is_array( $source ) ) {
367  if ( !isset( $source['loadScript'] ) ) {
368  throw new InvalidArgumentException( 'Each source must have a "loadScript" key' );
369  }
370  $source = $source['loadScript'];
371  }
372 
373  $this->sources[$id] = $source;
374  }
375  }
376 
380  public function getModuleNames() {
381  return array_keys( $this->moduleInfos );
382  }
383 
391  public function getTestSuiteModuleNames() {
392  return $this->testModuleNames;
393  }
394 
402  public function isModuleRegistered( $name ) {
403  return isset( $this->moduleInfos[$name] );
404  }
405 
417  public function getModule( $name ) {
418  if ( !isset( $this->modules[$name] ) ) {
419  if ( !isset( $this->moduleInfos[$name] ) ) {
420  // No such module
421  return null;
422  }
423  // Construct the requested module object
424  $info = $this->moduleInfos[$name];
425  if ( isset( $info['factory'] ) ) {
427  $object = call_user_func( $info['factory'], $info );
428  } else {
429  $class = $info['class'] ?? FileModule::class;
431  $object = new $class( $info );
432  }
433  $object->setConfig( $this->getConfig() );
434  $object->setLogger( $this->logger );
435  $object->setHookContainer( $this->hookContainer );
436  $object->setName( $name );
437  $object->setDependencyAccessCallbacks(
438  [ $this, 'loadModuleDependenciesInternal' ],
439  [ $this, 'saveModuleDependenciesInternal' ]
440  );
441  $object->setSkinStylesOverride( $this->moduleSkinStyles );
442  $this->modules[$name] = $object;
443  }
444 
445  return $this->modules[$name];
446  }
447 
454  public function preloadModuleInfo( array $moduleNames, Context $context ) {
455  // Load all tracked indirect file dependencies for the modules
456  $vary = Module::getVary( $context );
457  $entitiesByModule = [];
458  foreach ( $moduleNames as $moduleName ) {
459  $entitiesByModule[$moduleName] = "$moduleName|$vary";
460  }
461  $depsByEntity = $this->depStore->retrieveMulti(
462  self::RL_DEP_STORE_PREFIX,
463  $entitiesByModule
464  );
465  // Inject the indirect file dependencies for all the modules
466  foreach ( $moduleNames as $moduleName ) {
467  $module = $this->getModule( $moduleName );
468  if ( $module ) {
469  $entity = $entitiesByModule[$moduleName];
470  $deps = $depsByEntity[$entity];
471  $paths = Module::expandRelativePaths( $deps['paths'] );
472  $module->setFileDependencies( $context, $paths );
473  }
474  }
475 
476  // Batched version of WikiModule::getTitleInfo
477  $dbr = wfGetDB( DB_REPLICA );
478  WikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
479 
480  // Prime in-object cache for message blobs for modules with messages
481  $modulesWithMessages = [];
482  foreach ( $moduleNames as $moduleName ) {
483  $module = $this->getModule( $moduleName );
484  if ( $module && $module->getMessages() ) {
485  $modulesWithMessages[$moduleName] = $module;
486  }
487  }
488  // Prime in-object cache for message blobs for modules with messages
489  $lang = $context->getLanguage();
490  $store = $this->getMessageBlobStore();
491  $blobs = $store->getBlobs( $modulesWithMessages, $lang );
492  foreach ( $blobs as $moduleName => $blob ) {
493  $modulesWithMessages[$moduleName]->setMessageBlob( $blob, $lang );
494  }
495  }
496 
503  public function loadModuleDependenciesInternal( $moduleName, $variant ) {
504  $deps = $this->depStore->retrieve( self::RL_DEP_STORE_PREFIX, "$moduleName|$variant" );
505 
506  return Module::expandRelativePaths( $deps['paths'] );
507  }
508 
516  public function saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths ) {
517  $hasPendingUpdate = (bool)$this->depStoreUpdateBuffer;
518  $entity = "$moduleName|$variant";
519 
520  if ( array_diff( $paths, $priorPaths ) || array_diff( $priorPaths, $paths ) ) {
521  // Dependency store needs to be updated with the new path list
522  if ( $paths ) {
523  $deps = $this->depStore->newEntityDependencies( $paths, time() );
524  $this->depStoreUpdateBuffer[$entity] = $deps;
525  } else {
526  $this->depStoreUpdateBuffer[$entity] = null;
527  }
528  }
529 
530  // If paths were unchanged, leave the dependency store unchanged also.
531  // The entry will eventually expire, after which we will briefly issue an incomplete
532  // version hash for a 5-min startup window, the module then recomputes and rediscovers
533  // the paths and arrive at the same module version hash once again. It will churn
534  // part of the browser cache once, for clients connecting during that window.
535 
536  if ( !$hasPendingUpdate ) {
538  $updatesByEntity = $this->depStoreUpdateBuffer;
539  $this->depStoreUpdateBuffer = [];
541 
542  $scopeLocks = [];
543  $depsByEntity = [];
544  $entitiesUnreg = [];
545  foreach ( $updatesByEntity as $entity => $update ) {
546  $lockKey = $cache->makeKey( 'rl-deps', $entity );
547  $scopeLocks[$entity] = $cache->getScopedLock( $lockKey, 0 );
548  if ( !$scopeLocks[$entity] ) {
549  // avoid duplicate write request slams (T124649)
550  // the lock must be specific to the current wiki (T247028)
551  continue;
552  }
553  if ( $update === null ) {
554  $entitiesUnreg[] = $entity;
555  } else {
556  $depsByEntity[$entity] = $update;
557  }
558  }
559 
560  $ttl = self::RL_MODULE_DEP_TTL;
561  $this->depStore->storeMulti( self::RL_DEP_STORE_PREFIX, $depsByEntity, $ttl );
562  $this->depStore->remove( self::RL_DEP_STORE_PREFIX, $entitiesUnreg );
563  } );
564  }
565  }
566 
572  public function getSources() {
573  return $this->sources;
574  }
575 
584  public function getLoadScript( $source ) {
585  if ( !isset( $this->sources[$source] ) ) {
586  throw new UnexpectedValueException( "Unknown source '$source'" );
587  }
588  return $this->sources[$source];
589  }
590 
594  public const HASH_LENGTH = 5;
595 
658  public static function makeHash( $value ) {
659  $hash = hash( 'fnv132', $value );
660  // The base_convert will pad it (if too short),
661  // then substr() will trim it (if too long).
662  return substr(
663  \Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
664  0,
665  self::HASH_LENGTH
666  );
667  }
668 
678  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
680  $this->logger->warning(
681  $msg,
682  $context + [ 'exception' => $e ]
683  );
684  $this->errors[] = self::formatExceptionNoComment( $e );
685  }
686 
695  public function getCombinedVersion( Context $context, array $moduleNames ) {
696  if ( !$moduleNames ) {
697  return '';
698  }
699  $hashes = array_map( function ( $module ) use ( $context ) {
700  try {
701  return $this->getModule( $module )->getVersionHash( $context );
702  } catch ( TimeoutException $e ) {
703  throw $e;
704  } catch ( Exception $e ) {
705  // If modules fail to compute a version, don't fail the request (T152266)
706  // and still compute versions of other modules.
707  $this->outputErrorAndLog( $e,
708  'Calculating version for "{module}" failed: {exception}',
709  [
710  'module' => $module,
711  ]
712  );
713  return '';
714  }
715  }, $moduleNames );
716  return self::makeHash( implode( '', $hashes ) );
717  }
718 
733  public function makeVersionQuery( Context $context, array $modules ) {
734  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
735  // version hashes. There is no technical reason for this to be same, and for years the
736  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
737  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
738  // query parameter), then this method must continue to match the JS one.
739  $filtered = [];
740  foreach ( $modules as $name ) {
741  if ( !$this->getModule( $name ) ) {
742  // If a versioned request contains a missing module, the version is a mismatch
743  // as the client considered a module (and version) we don't have.
744  return '';
745  }
746  $filtered[] = $name;
747  }
748  return $this->getCombinedVersion( $context, $filtered );
749  }
750 
756  public function respond( Context $context ) {
757  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
758  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
759  // is used: ob_clean() will clear the GZIP header in that case and it won't come
760  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
761  // the whole thing in our own output buffer to be sure the active buffer
762  // doesn't use ob_gzhandler.
763  // See https://bugs.php.net/bug.php?id=36514
764  ob_start();
765 
766  $responseTime = $this->measureResponseTime();
767 
768  // Find out which modules are missing and instantiate the others
769  $modules = [];
770  $missing = [];
771  foreach ( $context->getModules() as $name ) {
772  $module = $this->getModule( $name );
773  if ( $module ) {
774  // Do not allow private modules to be loaded from the web.
775  // This is a security issue, see T36907.
776  if ( $module->getGroup() === Module::GROUP_PRIVATE ) {
777  // Not a serious error, just means something is trying to access it (T101806)
778  $this->logger->debug( "Request for private module '$name' denied" );
779  $this->errors[] = "Cannot build private module \"$name\"";
780  continue;
781  }
782  $modules[$name] = $module;
783  } else {
784  $missing[] = $name;
785  }
786  }
787 
788  try {
789  // Preload for getCombinedVersion() and for batch makeModuleResponse()
790  $this->preloadModuleInfo( array_keys( $modules ), $context );
791  } catch ( TimeoutException $e ) {
792  throw $e;
793  } catch ( Exception $e ) {
794  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
795  }
796 
797  // Combine versions to propagate cache invalidation
798  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
799 
800  // See RFC 2616 § 3.11 Entity Tags
801  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
802  $etag = 'W/"' . $versionHash . '"';
803 
804  // Try the client-side cache first
805  if ( $this->tryRespondNotModified( $context, $etag ) ) {
806  return; // output handled (buffers cleared)
807  }
808 
809  // Use file cache if enabled and available...
810  if ( $this->useFileCache ) {
811  $fileCache = ResourceFileCache::newFromContext( $context );
812  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
813  return; // output handled
814  }
815  } else {
816  $fileCache = null;
817  }
818 
819  // Generate a response
820  $response = $this->makeModuleResponse( $context, $modules, $missing );
821 
822  // Capture any PHP warnings from the output buffer and append them to the
823  // error list if we're in debug mode.
824  if ( $context->getDebug() ) {
825  $warnings = ob_get_contents();
826  if ( strlen( $warnings ) ) {
827  $this->errors[] = $warnings;
828  }
829  }
830 
831  // Consider saving the response to file cache (unless there are errors).
832  if ( $fileCache && !$this->errors && $missing === [] &&
833  ResourceFileCache::useFileCache( $context ) ) {
834  if ( $fileCache->isCacheWorthy() ) {
835  // There were enough hits, save the response to the cache
836  $fileCache->saveText( $response );
837  } else {
838  $fileCache->incrMissesRecent( $context->getRequest() );
839  }
840  }
841 
842  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
843 
844  // Remove the output buffer and output the response
845  ob_end_clean();
846 
847  if ( $context->getImageObj() && $this->errors ) {
848  // We can't show both the error messages and the response when it's an image.
849  $response = implode( "\n\n", $this->errors );
850  } elseif ( $this->errors ) {
851  $errorText = implode( "\n\n", $this->errors );
852  $errorResponse = self::makeComment( $errorText );
853  if ( $context->shouldIncludeScripts() ) {
854  $errorResponse .= 'if (window.console && console.error) { console.error('
855  . $context->encodeJson( $errorText )
856  . "); }\n";
857  }
858 
859  // Prepend error info to the response
860  $response = $errorResponse . $response;
861  }
862 
863  $this->errors = [];
864  // @phan-suppress-next-line SecurityCheck-XSS
865  echo $response;
866  }
867 
872  protected function measureResponseTime() {
873  $statStart = $_SERVER['REQUEST_TIME_FLOAT'];
874  return new ScopedCallback( static function () use ( $statStart ) {
875  $statTiming = microtime( true ) - $statStart;
876  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
877  $stats->timing( 'resourceloader.responseTime', $statTiming * 1000 );
878  } );
879  }
880 
891  protected function sendResponseHeaders(
892  Context $context, $etag, $errors, array $extra = []
893  ): void {
894  HeaderCallback::warnIfHeadersSent();
895 
896  if ( $errors
897  || (
898  $context->getVersion() !== null
899  && $context->getVersion() !== $this->makeVersionQuery( $context, $context->getModules() )
900  )
901  ) {
902  // If we need to self-correct, set a very short cache expiry
903  // to basically just debounce CDN traffic. This applies to:
904  // - Internal errors, e.g. due to misconfiguration.
905  // - Version mismatch, e.g. due to deployment race (T117587, T47877).
906  $maxage = self::MAXAGE_RECOVER;
907  } elseif ( $context->getVersion() === null ) {
908  // Resources that can't set a version, should have their updates propagate to
909  // clients quickly. This applies to shared resources linked from HTML, such as
910  // the startup module and stylesheets.
911  $maxage = $this->maxageUnversioned;
912  } else {
913  // When a version is set, use a long expiry because changes
914  // will naturally miss the cache by using a differente URL.
915  $maxage = $this->maxageVersioned;
916  }
917  if ( $context->getImageObj() ) {
918  // Output different headers if we're outputting textual errors.
919  if ( $errors ) {
920  header( 'Content-Type: text/plain; charset=utf-8' );
921  } else {
922  $context->getImageObj()->sendResponseHeaders( $context );
923  }
924  } elseif ( $context->getOnly() === 'styles' ) {
925  header( 'Content-Type: text/css; charset=utf-8' );
926  header( 'Access-Control-Allow-Origin: *' );
927  } else {
928  header( 'Content-Type: text/javascript; charset=utf-8' );
929  }
930  // See RFC 2616 § 14.19 ETag
931  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
932  header( 'ETag: ' . $etag );
933  if ( $context->getDebug() ) {
934  // Do not cache debug responses
935  header( 'Cache-Control: private, no-cache, must-revalidate' );
936  header( 'Pragma: no-cache' );
937  } else {
938  header( "Cache-Control: public, max-age=$maxage, s-maxage=$maxage" );
939  header( 'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
940  }
941  foreach ( $extra as $header ) {
942  header( $header );
943  }
944  }
945 
956  protected function tryRespondNotModified( Context $context, $etag ) {
957  // See RFC 2616 § 14.26 If-None-Match
958  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
959  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
960  // Never send 304s in debug mode
961  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
962  // There's another bug in ob_gzhandler (see also the comment at
963  // the top of this function) that causes it to gzip even empty
964  // responses, meaning it's impossible to produce a truly empty
965  // response (because the gzip header is always there). This is
966  // a problem because 304 responses have to be completely empty
967  // per the HTTP spec, and Firefox behaves buggily when they're not.
968  // See also https://bugs.php.net/bug.php?id=51579
969  // To work around this, we tear down all output buffering before
970  // sending the 304.
971  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
972 
973  HttpStatus::header( 304 );
974 
975  $this->sendResponseHeaders( $context, $etag, false );
976  return true;
977  }
978  return false;
979  }
980 
989  protected function tryRespondFromFileCache(
990  ResourceFileCache $fileCache,
991  Context $context,
992  $etag
993  ) {
994  // Buffer output to catch warnings.
995  ob_start();
996  // Get the maximum age the cache can be
997  $maxage = $context->getVersion() === null
998  ? $this->maxageUnversioned
999  : $this->maxageVersioned;
1000  // Minimum timestamp the cache file must have
1001  $minTime = time() - $maxage;
1002  $good = $fileCache->isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1003  if ( !$good ) {
1004  try { // RL always hits the DB on file cache miss...
1005  wfGetDB( DB_REPLICA );
1006  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
1007  $good = $fileCache->isCacheGood(); // cache existence check
1008  }
1009  }
1010  if ( $good ) {
1011  $ts = $fileCache->cacheTimestamp();
1012  // Send content type and cache headers
1013  $this->sendResponseHeaders( $context, $etag, false );
1014  $response = $fileCache->fetchText();
1015  // Capture any PHP warnings from the output buffer and append them to the
1016  // response in a comment if we're in debug mode.
1017  if ( $context->getDebug() ) {
1018  $warnings = ob_get_contents();
1019  if ( strlen( $warnings ) ) {
1020  $response = self::makeComment( $warnings ) . $response;
1021  }
1022  }
1023  // Remove the output buffer and output the response
1024  ob_end_clean();
1025  echo $response . "\n/* Cached {$ts} */";
1026  return true; // cache hit
1027  }
1028  // Clear buffer
1029  ob_end_clean();
1030 
1031  return false; // cache miss
1032  }
1033 
1042  public static function makeComment( $text ) {
1043  $encText = str_replace( '*/', '* /', $text );
1044  return "/*\n$encText\n*/\n";
1045  }
1046 
1053  public static function formatException( Throwable $e ) {
1054  return self::makeComment( self::formatExceptionNoComment( $e ) );
1055  }
1056 
1064  protected static function formatExceptionNoComment( Throwable $e ) {
1067  }
1068 
1069  return MWExceptionHandler::getLogMessage( $e ) .
1070  "\nBacktrace:\n" .
1072  }
1073 
1085  public function makeModuleResponse( Context $context,
1086  array $modules, array $missing = []
1087  ) {
1088  if ( $modules === [] && $missing === [] ) {
1089  return <<<MESSAGE
1090 /* This file is the Web entry point for MediaWiki's ResourceLoader:
1091  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1092  no modules were requested. Max made me put this here. */
1093 MESSAGE;
1094  }
1095 
1096  $image = $context->getImageObj();
1097  if ( $image ) {
1098  $data = $image->getImageData( $context );
1099  if ( $data === false ) {
1100  $data = '';
1101  $this->errors[] = 'Image generation failed';
1102  }
1103  return $data;
1104  }
1105 
1106  $states = [];
1107  foreach ( $missing as $name ) {
1108  $states[$name] = 'missing';
1109  }
1110 
1111  $only = $context->getOnly();
1112  $filter = $only === 'styles' ? 'minify-css' : 'minify-js';
1113  $debug = (bool)$context->getDebug();
1114 
1115  $out = '';
1116  foreach ( $modules as $name => $module ) {
1117  try {
1118  $content = $module->getModuleContent( $context );
1119  $implementKey = $name . '@' . $module->getVersionHash( $context );
1120  $strContent = '';
1121 
1122  if ( isset( $content['headers'] ) ) {
1123  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1124  }
1125 
1126  // Append output
1127  switch ( $only ) {
1128  case 'scripts':
1129  $scripts = $content['scripts'];
1130  if ( is_string( $scripts ) ) {
1131  // Load scripts raw...
1132  $strContent = $scripts;
1133  } elseif ( is_array( $scripts ) ) {
1134  // ...except when $scripts is an array of URLs or an associative array
1135  $strContent = self::makeLoaderImplementScript(
1136  $context,
1137  $implementKey,
1138  $scripts,
1139  [],
1140  [],
1141  []
1142  );
1143  }
1144  break;
1145  case 'styles':
1146  $styles = $content['styles'];
1147  // We no longer separate into media, they are all combined now with
1148  // custom media type groups into @media .. {} sections as part of the css string.
1149  // Module returns either an empty array or a numerical array with css strings.
1150  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1151  break;
1152  default:
1153  $scripts = $content['scripts'] ?? '';
1154  if ( is_string( $scripts ) ) {
1155  if ( $name === 'site' || $name === 'user' ) {
1156  // Legacy scripts that run in the global scope without a closure.
1157  // mw.loader.implement will use eval if scripts is a string.
1158  // Minify manually here, because general response minification is
1159  // not effective due it being a string literal, not a function.
1160  if ( !$debug ) {
1161  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1162  }
1163  } else {
1164  $scripts = new XmlJsCode( $scripts );
1165  }
1166  }
1167  $strContent = self::makeLoaderImplementScript(
1168  $context,
1169  $implementKey,
1170  $scripts,
1171  $content['styles'] ?? [],
1172  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1173  $content['templates'] ?? []
1174  );
1175  break;
1176  }
1177 
1178  if ( $debug ) {
1179  // In debug mode, separate each response by a new line.
1180  // For example, between 'mw.loader.implement();' statements.
1181  $strContent = self::ensureNewline( $strContent );
1182  } else {
1183  $strContent = self::filter( $filter, $strContent, [
1184  // Important: Do not cache minifications of embedded modules
1185  // This is especially for the private 'user.options' module,
1186  // which varies on every pageview and would explode the cache (T84960)
1187  'cache' => !$module->shouldEmbedModule( $context )
1188  ] );
1189  }
1190 
1191  if ( $only === 'scripts' ) {
1192  // Use a linebreak between module scripts (T162719)
1193  $out .= self::ensureNewline( $strContent );
1194  } else {
1195  $out .= $strContent;
1196  }
1197  } catch ( TimeoutException $e ) {
1198  throw $e;
1199  } catch ( Exception $e ) {
1200  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1201 
1202  // Respond to client with error-state instead of module implementation
1203  $states[$name] = 'error';
1204  unset( $modules[$name] );
1205  }
1206  }
1207 
1208  // Update module states
1209  if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1210  if ( $modules && $only === 'scripts' ) {
1211  // Set the state of modules loaded as only scripts to ready as
1212  // they don't have an mw.loader.implement wrapper that sets the state
1213  foreach ( $modules as $name => $module ) {
1214  $states[$name] = 'ready';
1215  }
1216  }
1217 
1218  // Set the state of modules we didn't respond to with mw.loader.implement
1219  if ( $states ) {
1220  $stateScript = self::makeLoaderStateScript( $context, $states );
1221  if ( !$debug ) {
1222  $stateScript = self::filter( 'minify-js', $stateScript );
1223  }
1224  // Use a linebreak between module script and state script (T162719)
1225  $out = self::ensureNewline( $out ) . $stateScript;
1226  }
1227  } elseif ( $states ) {
1228  $this->errors[] = 'Problematic modules: '
1229  . $context->encodeJson( $states );
1230  }
1231 
1232  return $out;
1233  }
1234 
1241  public static function ensureNewline( $str ) {
1242  $end = substr( $str, -1 );
1243  if ( $end === false || $end === '' || $end === "\n" ) {
1244  return $str;
1245  }
1246  return $str . "\n";
1247  }
1248 
1255  public function getModulesByMessage( $messageKey ) {
1256  $moduleNames = [];
1257  foreach ( $this->getModuleNames() as $moduleName ) {
1258  $module = $this->getModule( $moduleName );
1259  if ( in_array( $messageKey, $module->getMessages() ) ) {
1260  $moduleNames[] = $moduleName;
1261  }
1262  }
1263  return $moduleNames;
1264  }
1265 
1283  private static function makeLoaderImplementScript(
1284  Context $context, $name, $scripts, $styles, $messages, $templates
1285  ) {
1286  if ( $scripts instanceof XmlJsCode ) {
1287  if ( $scripts->value === '' ) {
1288  $scripts = null;
1289  } else {
1290  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1291  }
1292  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1293  $files = $scripts['files'];
1294  foreach ( $files as &$file ) {
1295  // $file is changed (by reference) from a descriptor array to the content of the file
1296  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1297  if ( $file['type'] === 'script' ) {
1298  // Ensure that the script has a newline at the end to close any comment in the
1299  // last line.
1300  $content = self::ensureNewline( $file['content'] );
1301  // Provide CJS `exports` (in addition to CJS2 `module.exports`) to package modules (T284511).
1302  // $/jQuery are simply used as globals instead.
1303  // TODO: Remove $/jQuery param from traditional module closure too (and bump caching)
1304  $file = new XmlJsCode( "function ( require, module, exports ) {\n$content}" );
1305  } else {
1306  $file = $file['content'];
1307  }
1308  }
1309  $scripts = XmlJsCode::encodeObject( [
1310  'main' => $scripts['main'],
1311  'files' => XmlJsCode::encodeObject( $files, true )
1312  ], true );
1313  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1314  throw new InvalidArgumentException( 'Script must be a string or an array of URLs' );
1315  }
1316 
1317  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1318  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1319  // of "{}". Force them to objects.
1320  $module = [
1321  $name,
1322  $scripts,
1323  (object)$styles,
1324  (object)$messages,
1325  (object)$templates
1326  ];
1327  self::trimArray( $module );
1328 
1329  // We use pretty output unconditionally to make this method simpler.
1330  // Minification is taken care of closer to the output.
1331  return Xml::encodeJsCall( 'mw.loader.implement', $module, true );
1332  }
1333 
1340  public static function makeMessageSetScript( $messages ) {
1341  return 'mw.messages.set('
1342  . self::encodeJsonForScript( (object)$messages )
1343  . ');';
1344  }
1345 
1353  public static function makeCombinedStyles( array $stylePairs ) {
1354  $out = [];
1355  foreach ( $stylePairs as $media => $styles ) {
1356  // FileModule::getStyle can return the styles as a string or an
1357  // array of strings. This is to allow separation in the front-end.
1358  $styles = (array)$styles;
1359  foreach ( $styles as $style ) {
1360  $style = trim( $style );
1361  // Don't output an empty "@media print { }" block (T42498)
1362  if ( $style === '' ) {
1363  continue;
1364  }
1365  // Transform the media type based on request params and config
1366  // The way that this relies on $wgRequest to propagate request params is slightly evil
1367  $media = OutputPage::transformCssMedia( $media );
1368 
1369  if ( $media === '' || $media == 'all' ) {
1370  $out[] = $style;
1371  } elseif ( is_string( $media ) ) {
1372  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1373  }
1374  // else: skip
1375  }
1376  }
1377  return $out;
1378  }
1379 
1389  public static function encodeJsonForScript( $data ) {
1390  // Keep output as small as possible by disabling needless escape modes
1391  // that PHP uses by default.
1392  // However, while most module scripts are only served on HTTP responses
1393  // for JavaScript, some modules can also be embedded in the HTML as inline
1394  // scripts. This, and the fact that we sometimes need to export strings
1395  // containing user-generated content and labels that may genuinely contain
1396  // a sequences like "</script>", we need to encode either '/' or '<'.
1397  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1398  // and allows URLs to mostly remain readable.
1399  $jsonFlags = JSON_UNESCAPED_SLASHES |
1400  JSON_UNESCAPED_UNICODE |
1401  JSON_HEX_TAG |
1402  JSON_HEX_AMP;
1403  if ( self::inDebugMode() ) {
1404  $jsonFlags |= JSON_PRETTY_PRINT;
1405  }
1406  return json_encode( $data, $jsonFlags );
1407  }
1408 
1421  public static function makeLoaderStateScript(
1422  Context $context, array $states
1423  ) {
1424  return 'mw.loader.state('
1425  . $context->encodeJson( $states )
1426  . ');';
1427  }
1428 
1429  private static function isEmptyObject( stdClass $obj ) {
1430  foreach ( $obj as $key => $value ) {
1431  return false;
1432  }
1433  return true;
1434  }
1435 
1449  private static function trimArray( array &$array ): void {
1450  $i = count( $array );
1451  while ( $i-- ) {
1452  if ( $array[$i] === null
1453  || $array[$i] === []
1454  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1455  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1456  ) {
1457  unset( $array[$i] );
1458  } else {
1459  break;
1460  }
1461  }
1462  }
1463 
1489  public static function makeLoaderRegisterScript(
1490  Context $context, array $modules
1491  ) {
1492  // Optimisation: Transform dependency names into indexes when possible
1493  // to produce smaller output. They are expanded by mw.loader.register on
1494  // the other end.
1495  $index = [];
1496  foreach ( $modules as $i => &$module ) {
1497  // Build module name index
1498  $index[$module[0]] = $i;
1499  }
1500  foreach ( $modules as &$module ) {
1501  if ( isset( $module[2] ) ) {
1502  foreach ( $module[2] as &$dependency ) {
1503  if ( isset( $index[$dependency] ) ) {
1504  // Replace module name in dependency list with index
1505  $dependency = $index[$dependency];
1506  }
1507  }
1508  }
1509  }
1510 
1511  array_walk( $modules, [ self::class, 'trimArray' ] );
1512 
1513  return 'mw.loader.register('
1514  . $context->encodeJson( $modules )
1515  . ');';
1516  }
1517 
1531  public static function makeLoaderSourcesScript(
1532  Context $context, array $sources
1533  ) {
1534  return 'mw.loader.addSource('
1535  . $context->encodeJson( $sources )
1536  . ');';
1537  }
1538 
1545  public static function makeLoaderConditionalScript( $script ) {
1546  // Adds a function to lazy-created RLQ
1547  return '(RLQ=window.RLQ||[]).push(function(){' .
1548  trim( $script ) . '});';
1549  }
1550 
1559  public static function makeInlineCodeWithModule( $modules, $script ) {
1560  // Adds an array to lazy-created RLQ
1561  return '(RLQ=window.RLQ||[]).push(['
1562  . self::encodeJsonForScript( $modules ) . ','
1563  . 'function(){' . trim( $script ) . '}'
1564  . ']);';
1565  }
1566 
1578  public static function makeInlineScript( $script, $nonce = null ) {
1579  $js = self::makeLoaderConditionalScript( $script );
1580  $escNonce = '';
1581  if ( $nonce === null ) {
1582  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1583  } elseif ( $nonce !== false ) {
1584  // If it was false, CSP is disabled, so no nonce attribute.
1585  // Nonce should be only base64 characters, so should be safe,
1586  // but better to be safely escaped than sorry.
1587  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1588  }
1589 
1590  return new WrappedString(
1591  Html::inlineScript( $js, $nonce ),
1592  "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1593  '});</script>'
1594  );
1595  }
1596 
1605  public static function makeConfigSetScript( array $configuration ) {
1606  $json = self::encodeJsonForScript( $configuration );
1607  if ( $json === false ) {
1608  $e = new Exception(
1609  'JSON serialization of config data failed. ' .
1610  'This usually means the config data is not valid UTF-8.'
1611  );
1613  return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1614  }
1615  return "mw.config.set($json);";
1616  }
1617 
1631  public static function makePackedModulesString( array $modules ) {
1632  $moduleMap = []; // [ prefix => [ suffixes ] ]
1633  foreach ( $modules as $module ) {
1634  $pos = strrpos( $module, '.' );
1635  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1636  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1637  $moduleMap[$prefix][] = $suffix;
1638  }
1639 
1640  $arr = [];
1641  foreach ( $moduleMap as $prefix => $suffixes ) {
1642  $p = $prefix === '' ? '' : $prefix . '.';
1643  $arr[] = $p . implode( ',', $suffixes );
1644  }
1645  return implode( '|', $arr );
1646  }
1647 
1659  public static function expandModuleNames( $modules ) {
1660  $retval = [];
1661  $exploded = explode( '|', $modules );
1662  foreach ( $exploded as $group ) {
1663  if ( strpos( $group, ',' ) === false ) {
1664  // This is not a set of modules in foo.bar,baz notation
1665  // but a single module
1666  $retval[] = $group;
1667  continue;
1668  }
1669  // This is a set of modules in foo.bar,baz notation
1670  $pos = strrpos( $group, '.' );
1671  if ( $pos === false ) {
1672  // Prefixless modules, i.e. without dots
1673  $retval = array_merge( $retval, explode( ',', $group ) );
1674  continue;
1675  }
1676  // We have a prefix and a bunch of suffixes
1677  $prefix = substr( $group, 0, $pos ); // 'foo'
1678  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1679  foreach ( $suffixes as $suffix ) {
1680  $retval[] = "$prefix.$suffix";
1681  }
1682  }
1683  return $retval;
1684  }
1685 
1696  public static function inDebugMode() {
1697  if ( self::$debugMode === null ) {
1698  global $wgRequest;
1699  $resourceLoaderDebug = MediaWikiServices::getInstance()->getMainConfig()->get(
1700  MainConfigNames::ResourceLoaderDebug );
1701  $str = $wgRequest->getRawVal( 'debug',
1702  $wgRequest->getCookie( 'resourceLoaderDebug', '', $resourceLoaderDebug ? 'true' : '' )
1703  );
1704  self::$debugMode = Context::debugFromString( $str );
1705  }
1706  return self::$debugMode;
1707  }
1708 
1719  public static function clearCache() {
1720  self::$debugMode = null;
1721  }
1722 
1732  public function createLoaderURL( $source, Context $context,
1733  array $extraQuery = []
1734  ) {
1735  $query = self::createLoaderQuery( $context, $extraQuery );
1736  $script = $this->getLoadScript( $source );
1737 
1738  return wfAppendQuery( $script, $query );
1739  }
1740 
1750  protected static function createLoaderQuery(
1751  Context $context, array $extraQuery = []
1752  ) {
1753  return self::makeLoaderQuery(
1754  $context->getModules(),
1755  $context->getLanguage(),
1756  $context->getSkin(),
1757  $context->getUser(),
1758  $context->getVersion(),
1759  $context->getDebug(),
1760  $context->getOnly(),
1761  $context->getRequest()->getBool( 'printable' ),
1762  null,
1763  $extraQuery
1764  );
1765  }
1766 
1783  public static function makeLoaderQuery( array $modules, $lang, $skin, $user = null,
1784  $version = null, $debug = Context::DEBUG_OFF, $only = null,
1785  $printable = false, $handheld = null, array $extraQuery = []
1786  ) {
1787  $query = [
1788  'modules' => self::makePackedModulesString( $modules ),
1789  ];
1790  // Keep urls short by omitting query parameters that
1791  // match the defaults assumed by Context.
1792  // Note: This relies on the defaults either being insignificant or forever constant,
1793  // as otherwise cached urls could change in meaning when the defaults change.
1794  if ( $lang !== Context::DEFAULT_LANG ) {
1795  $query['lang'] = $lang;
1796  }
1797  if ( $skin !== Context::DEFAULT_SKIN ) {
1798  $query['skin'] = $skin;
1799  }
1800  if ( $debug !== Context::DEBUG_OFF ) {
1801  $query['debug'] = strval( $debug );
1802  }
1803  if ( $user !== null ) {
1804  $query['user'] = $user;
1805  }
1806  if ( $version !== null ) {
1807  $query['version'] = $version;
1808  }
1809  if ( $only !== null ) {
1810  $query['only'] = $only;
1811  }
1812  if ( $printable ) {
1813  $query['printable'] = 1;
1814  }
1815  $query += $extraQuery;
1816 
1817  // Make queries uniform in order
1818  ksort( $query );
1819  return $query;
1820  }
1821 
1831  public static function isValidModuleName( $moduleName ) {
1832  $len = strlen( $moduleName );
1833  return $len <= 255 && strcspn( $moduleName, '!,|', 0, $len ) === $len;
1834  }
1835 
1847  public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1848  global $IP;
1849  // When called from the installer, it is possible that a required PHP extension
1850  // is missing (at least for now; see T49564). If this is the case, throw an
1851  // exception (caught by the installer) to prevent a fatal error later on.
1852  if ( !class_exists( Less_Parser::class ) ) {
1853  throw new MWException( 'MediaWiki requires the less.php parser' );
1854  }
1855 
1856  $importDirs[] = "$IP/resources/src/mediawiki.less";
1857 
1858  $parser = new Less_Parser;
1859  $parser->ModifyVars( $vars );
1860  // SetImportDirs expects an array like [ 'path1' => '', 'path2' => '' ]
1861  $parser->SetImportDirs( array_fill_keys( $importDirs, '' ) );
1862  $parser->SetOption( 'relativeUrls', false );
1863 
1864  return $parser;
1865  }
1866 
1880  public function expandUrl( string $base, string $url ): string {
1881  // Net_URL2::resolve() doesn't allow protocol-relative URLs, but we do.
1882  $isProtoRelative = strpos( $base, '//' ) === 0;
1883  if ( $isProtoRelative ) {
1884  $base = "https:$base";
1885  }
1886  // Net_URL2::resolve() takes care of throwing if $base doesn't have a server.
1887  $baseUrl = new Net_URL2( $base );
1888  $ret = $baseUrl->resolve( $url );
1889  if ( $isProtoRelative ) {
1890  $ret->setScheme( false );
1891  }
1892  return $ret->getURL();
1893  }
1894 
1912  public static function filter( $filter, $data, array $options = [] ) {
1913  if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
1914  return $data;
1915  }
1916 
1917  if ( isset( $options['cache'] ) && $options['cache'] === false ) {
1918  return self::applyFilter( $filter, $data ) ?? $data;
1919  }
1920 
1921  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1923 
1924  $key = $cache->makeGlobalKey(
1925  'resourceloader-filter',
1926  $filter,
1927  self::CACHE_VERSION,
1928  md5( $data )
1929  );
1930 
1931  $incKey = "resourceloader_cache.$filter.hit";
1932  $result = $cache->getWithSetCallback(
1933  $key,
1934  BagOStuff::TTL_DAY,
1935  function () use ( $filter, $data, &$incKey ) {
1936  $incKey = "resourceloader_cache.$filter.miss";
1937  return self::applyFilter( $filter, $data );
1938  }
1939  );
1940  $stats->increment( $incKey );
1941  if ( $result === null ) {
1942  // Cached failure
1943  $result = $data;
1944  }
1945 
1946  return $result;
1947  }
1948 
1954  private static function applyFilter( $filter, $data ) {
1955  $data = trim( $data );
1956  if ( $data ) {
1957  try {
1958  $data = ( $filter === 'minify-css' )
1959  ? CSSMin::minify( $data )
1960  : JavaScriptMinifier::minify( $data );
1961  } catch ( TimeoutException $e ) {
1962  throw $e;
1963  } catch ( Exception $e ) {
1965  return null;
1966  }
1967  }
1968  return $data;
1969  }
1970 
1982  public static function getUserDefaults(
1983  Context $context,
1984  HookContainer $hookContainer,
1985  UserOptionsLookup $userOptionsLookup
1986  ): array {
1987  $defaultOptions = $userOptionsLookup->getDefaultOptions();
1988  $keysToExclude = [];
1989  $hookRunner = new HookRunner( $hookContainer );
1990  $hookRunner->onResourceLoaderExcludeUserOptions( $keysToExclude, $context );
1991  foreach ( $keysToExclude as $excludedKey ) {
1992  unset( $defaultOptions[ $excludedKey ] );
1993  }
1994  return $defaultOptions;
1995  }
1996 
2005  public static function getSiteConfigSettings(
2006  Context $context, Config $conf
2007  ): array {
2008  // Namespace related preparation
2009  // - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
2010  // - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
2011  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2012  $namespaceIds = $contLang->getNamespaceIds();
2013  $caseSensitiveNamespaces = [];
2014  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2015  foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
2016  $namespaceIds[$contLang->lc( $name )] = $index;
2017  if ( !$nsInfo->isCapitalized( $index ) ) {
2018  $caseSensitiveNamespaces[] = $index;
2019  }
2020  }
2021 
2022  $illegalFileChars = $conf->get( MainConfigNames::IllegalFileChars );
2023 
2024  // Build list of variables
2025  $skin = $context->getSkin();
2026 
2027  // Start of supported and stable config vars (for use by extensions/gadgets).
2028  $vars = [
2029  'debug' => $context->getDebug(),
2030  'skin' => $skin,
2031  'stylepath' => $conf->get( MainConfigNames::StylePath ),
2032  'wgArticlePath' => $conf->get( MainConfigNames::ArticlePath ),
2033  'wgScriptPath' => $conf->get( MainConfigNames::ScriptPath ),
2034  'wgScript' => $conf->get( MainConfigNames::Script ),
2035  'wgSearchType' => $conf->get( MainConfigNames::SearchType ),
2036  'wgVariantArticlePath' => $conf->get( MainConfigNames::VariantArticlePath ),
2037  'wgServer' => $conf->get( MainConfigNames::Server ),
2038  'wgServerName' => $conf->get( MainConfigNames::ServerName ),
2039  'wgUserLanguage' => $context->getLanguage(),
2040  'wgContentLanguage' => $contLang->getCode(),
2041  'wgVersion' => MW_VERSION,
2042  'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
2043  'wgNamespaceIds' => $namespaceIds,
2044  'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
2045  'wgSiteName' => $conf->get( MainConfigNames::Sitename ),
2046  'wgDBname' => $conf->get( MainConfigNames::DBname ),
2047  'wgWikiID' => WikiMap::getCurrentWikiId(),
2048  'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
2049  'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
2050  'wgExtensionAssetsPath' => $conf->get( MainConfigNames::ExtensionAssetsPath ),
2051  ];
2052  // End of stable config vars.
2053 
2054  // Internal variables for use by MediaWiki core and/or ResourceLoader.
2055  $vars += [
2056  // @internal For mediawiki.widgets
2057  'wgUrlProtocols' => wfUrlProtocols(),
2058  // @internal For mediawiki.page.watch
2059  // Force object to avoid "empty" associative array from
2060  // becoming [] instead of {} in JS (T36604)
2061  'wgActionPaths' => (object)$conf->get( MainConfigNames::ActionPaths ),
2062  // @internal For mediawiki.language
2063  'wgTranslateNumerals' => $conf->get( MainConfigNames::TranslateNumerals ),
2064  // @internal For mediawiki.Title
2065  'wgExtraSignatureNamespaces' => $conf->get( MainConfigNames::ExtraSignatureNamespaces ),
2066  'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
2067  'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
2068  ];
2069 
2070  Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );
2071 
2072  return $vars;
2073  }
2074 }
2075 
2076 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:377
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:85
Handle database storage of comments such as edit summaries and log reasons.
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
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:29
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:566
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.
PHP 7.2 hack to work around the issue described at https://phabricator.wikimedia.org/T166010#5962098 ...
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:1111
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:591
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:55
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:49
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:734
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition: Title.php:748
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:44
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
Definition: WebRequest.php:74
Helper tools for dealing with other locally-hosted wikis.
Definition: WikiMap.php:29
static getCurrentWikiId()
Definition: WikiMap.php:303
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: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: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