MediaWiki  master
ResourceLoader.php
Go to the documentation of this file.
1 <?php
27 use Psr\Log\LoggerAwareInterface;
28 use Psr\Log\LoggerInterface;
29 use Psr\Log\NullLogger;
32 use Wikimedia\Minify\CSSMin;
33 use Wikimedia\Minify\JavaScriptMinifier;
35 use Wikimedia\ScopedCallback;
36 use Wikimedia\Timestamp\ConvertibleTimestamp;
37 use Wikimedia\WrappedString;
38 
59 class ResourceLoader implements LoggerAwareInterface {
61  protected $config;
63  protected $blobStore;
65  protected $depStore;
66 
68  private $logger;
69 
71  private $hookContainer;
72 
74  private $hookRunner;
75 
77  protected $modules = [];
79  protected $moduleInfos = [];
85  protected $testModuleNames = [];
87  protected $testSuiteModuleNames = [];
89  protected $sources = [];
91  protected $errors = [];
93  protected $extraHeaders = [];
94 
96  private $depStoreUpdateBuffer = [];
97 
101  private $moduleSkinStyles = [];
102 
104  protected static $debugMode = null;
105 
107  public const CACHE_VERSION = 8;
108 
110  private const RL_DEP_STORE_PREFIX = 'ResourceLoaderModule';
112  private const RL_MODULE_DEP_TTL = BagOStuff::TTL_WEEK;
113 
115  public const FILTER_NOMIN = '/*@nomin*/';
116 
123  public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
124  // Load all tracked indirect file dependencies for the modules
125  $vary = ResourceLoaderModule::getVary( $context );
126  $entitiesByModule = [];
127  foreach ( $moduleNames as $moduleName ) {
128  $entitiesByModule[$moduleName] = "$moduleName|$vary";
129  }
130  $depsByEntity = $this->depStore->retrieveMulti(
131  self::RL_DEP_STORE_PREFIX,
132  $entitiesByModule
133  );
134  // Inject the indirect file dependencies for all the modules
135  foreach ( $moduleNames as $moduleName ) {
136  $module = $this->getModule( $moduleName );
137  if ( $module ) {
138  $entity = $entitiesByModule[$moduleName];
139  $deps = $depsByEntity[$entity];
140  $paths = ResourceLoaderModule::expandRelativePaths( $deps['paths'] );
141  $module->setFileDependencies( $context, $paths );
142  }
143  }
144 
145  // Batched version of ResourceLoaderWikiModule::getTitleInfo
146  $dbr = wfGetDB( DB_REPLICA );
147  ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
148 
149  // Prime in-object cache for message blobs for modules with messages
150  $modulesWithMessages = [];
151  foreach ( $moduleNames as $moduleName ) {
152  $module = $this->getModule( $moduleName );
153  if ( $module && $module->getMessages() ) {
154  $modulesWithMessages[$moduleName] = $module;
155  }
156  }
157  // Prime in-object cache for message blobs for modules with messages
158  $lang = $context->getLanguage();
159  $store = $this->getMessageBlobStore();
160  $blobs = $store->getBlobs( $modulesWithMessages, $lang );
161  foreach ( $blobs as $moduleName => $blob ) {
162  $modulesWithMessages[$moduleName]->setMessageBlob( $blob, $lang );
163  }
164  }
165 
183  public static function filter( $filter, $data, array $options = [] ) {
184  if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
185  return $data;
186  }
187 
188  if ( isset( $options['cache'] ) && $options['cache'] === false ) {
189  return self::applyFilter( $filter, $data ) ?? $data;
190  }
191 
192  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
194 
195  $key = $cache->makeGlobalKey(
196  'resourceloader-filter',
197  $filter,
198  self::CACHE_VERSION,
199  md5( $data )
200  );
201 
202  $incKey = "resourceloader_cache.$filter.hit";
203  $result = $cache->getWithSetCallback(
204  $key,
205  BagOStuff::TTL_DAY,
206  function () use ( $filter, $data, &$incKey ) {
207  $incKey = "resourceloader_cache.$filter.miss";
208  return self::applyFilter( $filter, $data );
209  }
210  );
211  $stats->increment( $incKey );
212  if ( $result === null ) {
213  // Cached failure
214  $result = $data;
215  }
216 
217  return $result;
218  }
219 
225  private static function applyFilter( $filter, $data ) {
226  $data = trim( $data );
227  if ( $data ) {
228  try {
229  $data = ( $filter === 'minify-css' )
230  ? CSSMin::minify( $data )
231  : JavaScriptMinifier::minify( $data );
232  } catch ( Exception $e ) {
234  return null;
235  }
236  }
237  return $data;
238  }
239 
246  public function __construct(
247  Config $config,
248  LoggerInterface $logger = null,
250  ) {
251  $this->logger = $logger ?: new NullLogger();
252  $services = MediaWikiServices::getInstance();
253 
254  $this->config = $config;
255 
256  $this->hookContainer = $services->getHookContainer();
257  $this->hookRunner = new HookRunner( $this->hookContainer );
258 
259  // Add 'local' source first
260  $this->addSource( 'local', $config->get( 'LoadScript' ) );
261 
262  // Special module that always exists
263  $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
264 
265  $this->setMessageBlobStore(
266  new MessageBlobStore( $this, $this->logger, $services->getMainWANObjectCache() )
267  );
268 
270  $this->setDependencyStore( $tracker );
271  }
272 
276  public function getConfig() {
277  return $this->config;
278  }
279 
284  public function setLogger( LoggerInterface $logger ) {
285  $this->logger = $logger;
286  }
287 
292  public function getLogger() {
293  return $this->logger;
294  }
295 
300  public function getMessageBlobStore() {
301  return $this->blobStore;
302  }
303 
309  $this->blobStore = $blobStore;
310  }
311 
317  $this->depStore = $tracker;
318  }
319 
324  public function setModuleSkinStyles( array $moduleSkinStyles ) {
325  $this->moduleSkinStyles = $moduleSkinStyles;
326  }
327 
339  public function register( $name, array $info = null ) {
340  // Allow multiple modules to be registered in one call
341  $registrations = is_array( $name ) ? $name : [ $name => $info ];
342  foreach ( $registrations as $name => $info ) {
343  // Warn on duplicate registrations
344  if ( isset( $this->moduleInfos[$name] ) ) {
345  // A module has already been registered by this name
346  $this->logger->warning(
347  'ResourceLoader duplicate registration warning. ' .
348  'Another module has already been registered as ' . $name
349  );
350  }
351 
352  // Check validity
353  if ( !self::isValidModuleName( $name ) ) {
354  throw new InvalidArgumentException( "ResourceLoader module name '$name' is invalid, "
355  . "see ResourceLoader::isValidModuleName()" );
356  }
357  if ( !is_array( $info ) ) {
358  throw new InvalidArgumentException(
359  'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
360  );
361  }
362 
363  // Attach module
364  $this->moduleInfos[$name] = $info;
365  }
366  }
367 
372  public function registerTestModules(): void {
373  global $IP;
374 
375  if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
376  throw new MWException( 'Attempt to register JavaScript test modules '
377  . 'but <code>$wgEnableJavaScriptTest</code> is false. '
378  . 'Edit your <code>LocalSettings.php</code> to enable it.' );
379  }
380 
381  // This has a 'qunit' key for compat with the below hook.
382  $testModulesMeta = [ 'qunit' => [] ];
383 
384  $this->hookRunner->onResourceLoaderTestModules( $testModulesMeta, $this );
385  $extRegistry = ExtensionRegistry::getInstance();
386  // In case of conflict, the deprecated hook has precedence.
387  $testModules = $testModulesMeta['qunit']
388  + $extRegistry->getAttribute( 'QUnitTestModules' );
389 
391  foreach ( $testModules as $name => &$module ) {
392  // Turn any single-module dependency into an array
393  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
394  $module['dependencies'] = [ $module['dependencies'] ];
395  }
396 
397  // Ensure the testrunner loads before any test suites
398  $module['dependencies'][] = 'mediawiki.qunit-testrunner';
399 
400  // Keep track of the test suites to load on SpecialJavaScriptTest
401  $testSuiteModuleNames[] = $name;
402  }
403 
404  // Core test suites (their names have further precedence).
405  $testModules = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules;
406  $testSuiteModuleNames[] = 'test.MediaWiki';
407 
408  $this->register( $testModules );
409  $this->testSuiteModuleNames = $testSuiteModuleNames;
410  }
411 
422  public function addSource( $sources, $loadUrl = null ) {
423  if ( !is_array( $sources ) ) {
424  $sources = [ $sources => $loadUrl ];
425  }
426  foreach ( $sources as $id => $source ) {
427  // Disallow duplicates
428  if ( isset( $this->sources[$id] ) ) {
429  throw new RuntimeException( 'Cannot register source ' . $id . ' twice' );
430  }
431 
432  // Support: MediaWiki 1.24 and earlier
433  if ( is_array( $source ) ) {
434  if ( !isset( $source['loadScript'] ) ) {
435  throw new InvalidArgumentException( 'Each source must have a "loadScript" key' );
436  }
437  $source = $source['loadScript'];
438  }
439 
440  $this->sources[$id] = $source;
441  }
442  }
443 
447  public function getModuleNames() {
448  return array_keys( $this->moduleInfos );
449  }
450 
458  public function getTestSuiteModuleNames() {
460  }
461 
469  public function isModuleRegistered( $name ) {
470  return isset( $this->moduleInfos[$name] );
471  }
472 
484  public function getModule( $name ) {
485  if ( !isset( $this->modules[$name] ) ) {
486  if ( !isset( $this->moduleInfos[$name] ) ) {
487  // No such module
488  return null;
489  }
490  // Construct the requested module object
491  $info = $this->moduleInfos[$name];
492  if ( isset( $info['factory'] ) ) {
494  $object = call_user_func( $info['factory'], $info );
495  } else {
496  $class = $info['class'] ?? ResourceLoaderFileModule::class;
498  $object = new $class( $info );
499  }
500  $object->setConfig( $this->getConfig() );
501  $object->setLogger( $this->logger );
502  $object->setHookContainer( $this->hookContainer );
503  $object->setName( $name );
504  $object->setDependencyAccessCallbacks(
505  [ $this, 'loadModuleDependenciesInternal' ],
506  [ $this, 'saveModuleDependenciesInternal' ]
507  );
508  $object->setSkinStylesOverride( $this->moduleSkinStyles );
509  $this->modules[$name] = $object;
510  }
511 
512  return $this->modules[$name];
513  }
514 
521  public function loadModuleDependenciesInternal( $moduleName, $variant ) {
522  $deps = $this->depStore->retrieve( self::RL_DEP_STORE_PREFIX, "$moduleName|$variant" );
523 
524  return ResourceLoaderModule::expandRelativePaths( $deps['paths'] );
525  }
526 
534  public function saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths ) {
535  $hasPendingUpdate = (bool)$this->depStoreUpdateBuffer;
536  $entity = "$moduleName|$variant";
537 
538  if ( array_diff( $paths, $priorPaths ) || array_diff( $priorPaths, $paths ) ) {
539  // Dependency store needs to be updated with the new path list
540  if ( $paths ) {
541  $deps = $this->depStore->newEntityDependencies( $paths, time() );
542  $this->depStoreUpdateBuffer[$entity] = $deps;
543  } else {
544  $this->depStoreUpdateBuffer[$entity] = null;
545  }
546  } elseif ( $priorPaths ) {
547  // Dependency store needs to store the existing path list for longer
548  $this->depStoreUpdateBuffer[$entity] = '*';
549  }
550 
551  // Use a DeferrableUpdate to flush the buffered dependency updates...
552  if ( !$hasPendingUpdate ) {
554  $updatesByEntity = $this->depStoreUpdateBuffer;
555  $this->depStoreUpdateBuffer = []; // consume
557 
558  $scopeLocks = [];
559  $depsByEntity = [];
560  $entitiesUnreg = [];
561  $entitiesRenew = [];
562  foreach ( $updatesByEntity as $entity => $update ) {
563  $lockKey = $cache->makeKey( 'rl-deps', $entity );
564  $scopeLocks[$entity] = $cache->getScopedLock( $lockKey, 0 );
565  if ( !$scopeLocks[$entity] ) {
566  // avoid duplicate write request slams (T124649)
567  // the lock must be specific to the current wiki (T247028)
568  continue;
569  }
570  if ( $update === null ) {
571  $entitiesUnreg[] = $entity;
572  } elseif ( $update === '*' ) {
573  $entitiesRenew[] = $entity;
574  } else {
575  $depsByEntity[$entity] = $update;
576  }
577  }
578 
579  $ttl = self::RL_MODULE_DEP_TTL;
580  $this->depStore->storeMulti( self::RL_DEP_STORE_PREFIX, $depsByEntity, $ttl );
581  $this->depStore->remove( self::RL_DEP_STORE_PREFIX, $entitiesUnreg );
582  $this->depStore->renew( self::RL_DEP_STORE_PREFIX, $entitiesRenew, $ttl );
583  } );
584  }
585  }
586 
592  public function getSources() {
593  return $this->sources;
594  }
595 
604  public function getLoadScript( $source ) {
605  if ( !isset( $this->sources[$source] ) ) {
606  throw new UnexpectedValueException( "Unknown source '$source'" );
607  }
608  return $this->sources[$source];
609  }
610 
614  public const HASH_LENGTH = 5;
615 
678  public static function makeHash( $value ) {
679  $hash = hash( 'fnv132', $value );
680  // The base_convert will pad it (if too short),
681  // then substr() will trim it (if too long).
682  return substr(
683  Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
684  0,
685  self::HASH_LENGTH
686  );
687  }
688 
698  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
700  $this->logger->warning(
701  $msg,
702  $context + [ 'exception' => $e ]
703  );
704  $this->errors[] = self::formatExceptionNoComment( $e );
705  }
706 
715  public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
716  if ( !$moduleNames ) {
717  return '';
718  }
719  $hashes = array_map( function ( $module ) use ( $context ) {
720  try {
721  return $this->getModule( $module )->getVersionHash( $context );
722  } catch ( Exception $e ) {
723  // If modules fail to compute a version, don't fail the request (T152266)
724  // and still compute versions of other modules.
725  $this->outputErrorAndLog( $e,
726  'Calculating version for "{module}" failed: {exception}',
727  [
728  'module' => $module,
729  ]
730  );
731  return '';
732  }
733  }, $moduleNames );
734  return self::makeHash( implode( '', $hashes ) );
735  }
736 
751  public function makeVersionQuery( ResourceLoaderContext $context, array $modules ) {
752  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
753  // version hashes. There is no technical reason for this to be same, and for years the
754  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
755  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
756  // query parameter), then this method must continue to match the JS one.
757  $filtered = [];
758  foreach ( $modules as $name ) {
759  if ( !$this->getModule( $name ) ) {
760  // If a versioned request contains a missing module, the version is a mismatch
761  // as the client considered a module (and version) we don't have.
762  return '';
763  }
764  $filtered[] = $name;
765  }
766  return $this->getCombinedVersion( $context, $filtered );
767  }
768 
774  public function respond( ResourceLoaderContext $context ) {
775  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
776  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
777  // is used: ob_clean() will clear the GZIP header in that case and it won't come
778  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
779  // the whole thing in our own output buffer to be sure the active buffer
780  // doesn't use ob_gzhandler.
781  // See https://bugs.php.net/bug.php?id=36514
782  ob_start();
783 
784  $responseTime = $this->measureResponseTime();
785 
786  // Find out which modules are missing and instantiate the others
787  $modules = [];
788  $missing = [];
789  foreach ( $context->getModules() as $name ) {
790  $module = $this->getModule( $name );
791  if ( $module ) {
792  // Do not allow private modules to be loaded from the web.
793  // This is a security issue, see T36907.
794  if ( $module->getGroup() === 'private' ) {
795  // Not a serious error, just means something is trying to access it (T101806)
796  $this->logger->debug( "Request for private module '$name' denied" );
797  $this->errors[] = "Cannot build private module \"$name\"";
798  continue;
799  }
800  $modules[$name] = $module;
801  } else {
802  $missing[] = $name;
803  }
804  }
805 
806  try {
807  // Preload for getCombinedVersion() and for batch makeModuleResponse()
808  $this->preloadModuleInfo( array_keys( $modules ), $context );
809  } catch ( Exception $e ) {
810  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
811  }
812 
813  // Combine versions to propagate cache invalidation
814  $versionHash = '';
815  try {
816  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
817  } catch ( Exception $e ) {
818  $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
819  }
820 
821  // See RFC 2616 § 3.11 Entity Tags
822  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
823  $etag = 'W/"' . $versionHash . '"';
824 
825  // Try the client-side cache first
826  if ( $this->tryRespondNotModified( $context, $etag ) ) {
827  return; // output handled (buffers cleared)
828  }
829 
830  // Use file cache if enabled and available...
831  if ( $this->config->get( 'UseFileCache' ) ) {
832  $fileCache = ResourceFileCache::newFromContext( $context );
833  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
834  return; // output handled
835  }
836  } else {
837  $fileCache = null;
838  }
839 
840  // Generate a response
841  $response = $this->makeModuleResponse( $context, $modules, $missing );
842 
843  // Capture any PHP warnings from the output buffer and append them to the
844  // error list if we're in debug mode.
845  if ( $context->getDebug() ) {
846  $warnings = ob_get_contents();
847  if ( strlen( $warnings ) ) {
848  $this->errors[] = $warnings;
849  }
850  }
851 
852  // Consider saving the response to file cache (unless there are errors).
853  if ( $fileCache &&
854  !$this->errors &&
855  $missing === [] &&
857  ) {
858  if ( $fileCache->isCacheWorthy() ) {
859  // There were enough hits, save the response to the cache
860  $fileCache->saveText( $response );
861  } else {
862  $fileCache->incrMissesRecent( $context->getRequest() );
863  }
864  }
865 
866  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
867 
868  // Remove the output buffer and output the response
869  ob_end_clean();
870 
871  if ( $context->getImageObj() && $this->errors ) {
872  // We can't show both the error messages and the response when it's an image.
873  $response = implode( "\n\n", $this->errors );
874  } elseif ( $this->errors ) {
875  $errorText = implode( "\n\n", $this->errors );
876  $errorResponse = self::makeComment( $errorText );
877  if ( $context->shouldIncludeScripts() ) {
878  $errorResponse .= 'if (window.console && console.error) { console.error('
879  . $context->encodeJson( $errorText )
880  . "); }\n";
881  }
882 
883  // Prepend error info to the response
884  $response = $errorResponse . $response;
885  }
886 
887  $this->errors = [];
888  // @phan-suppress-next-line SecurityCheck-XSS
889  echo $response;
890  }
891 
896  protected function measureResponseTime() {
897  $statStart = $_SERVER['REQUEST_TIME_FLOAT'];
898  return new ScopedCallback( static function () use ( $statStart ) {
899  $statTiming = microtime( true ) - $statStart;
900  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
901  $stats->timing( 'resourceloader.responseTime', $statTiming * 1000 );
902  } );
903  }
904 
915  protected function sendResponseHeaders(
916  ResourceLoaderContext $context, $etag, $errors, array $extra = []
917  ): void {
918  HeaderCallback::warnIfHeadersSent();
919  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
920  // Use a short cache expiry so that updates propagate to clients quickly, if:
921  // - No version specified (shared resources, e.g. stylesheets)
922  // - There were errors (recover quickly)
923  // - Version mismatch (T117587, T47877)
924  if ( $context->getVersion() === null
925  || $errors
926  || $context->getVersion() !== $this->makeVersionQuery( $context, $context->getModules() )
927  ) {
928  $maxage = $rlMaxage['unversioned'];
929  // If a version was specified we can use a longer expiry time since changing
930  // version numbers causes cache misses
931  } else {
932  $maxage = $rlMaxage['versioned'];
933  }
934  if ( $context->getImageObj() ) {
935  // Output different headers if we're outputting textual errors.
936  if ( $errors ) {
937  header( 'Content-Type: text/plain; charset=utf-8' );
938  } else {
939  $context->getImageObj()->sendResponseHeaders( $context );
940  }
941  } elseif ( $context->getOnly() === 'styles' ) {
942  header( 'Content-Type: text/css; charset=utf-8' );
943  header( 'Access-Control-Allow-Origin: *' );
944  } else {
945  header( 'Content-Type: text/javascript; charset=utf-8' );
946  }
947  // See RFC 2616 § 14.19 ETag
948  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
949  header( 'ETag: ' . $etag );
950  if ( $context->getDebug() ) {
951  // Do not cache debug responses
952  header( 'Cache-Control: private, no-cache, must-revalidate' );
953  header( 'Pragma: no-cache' );
954  } else {
955  header( "Cache-Control: public, max-age=$maxage, s-maxage=$maxage" );
956  header( 'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
957  }
958  foreach ( $extra as $header ) {
959  header( $header );
960  }
961  }
962 
973  protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
974  // See RFC 2616 § 14.26 If-None-Match
975  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
976  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
977  // Never send 304s in debug mode
978  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
979  // There's another bug in ob_gzhandler (see also the comment at
980  // the top of this function) that causes it to gzip even empty
981  // responses, meaning it's impossible to produce a truly empty
982  // response (because the gzip header is always there). This is
983  // a problem because 304 responses have to be completely empty
984  // per the HTTP spec, and Firefox behaves buggily when they're not.
985  // See also https://bugs.php.net/bug.php?id=51579
986  // To work around this, we tear down all output buffering before
987  // sending the 304.
988  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
989 
990  HttpStatus::header( 304 );
991 
992  $this->sendResponseHeaders( $context, $etag, false );
993  return true;
994  }
995  return false;
996  }
997 
1006  protected function tryRespondFromFileCache(
1007  ResourceFileCache $fileCache,
1008  ResourceLoaderContext $context,
1009  $etag
1010  ) {
1011  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
1012  // Buffer output to catch warnings.
1013  ob_start();
1014  // Get the maximum age the cache can be
1015  $maxage = $context->getVersion() === null
1016  ? $rlMaxage['unversioned']
1017  : $rlMaxage['versioned'];
1018  // Minimum timestamp the cache file must have
1019  $minTime = time() - $maxage;
1020  $good = $fileCache->isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1021  if ( !$good ) {
1022  try { // RL always hits the DB on file cache miss...
1023  wfGetDB( DB_REPLICA );
1024  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
1025  $good = $fileCache->isCacheGood(); // cache existence check
1026  }
1027  }
1028  if ( $good ) {
1029  $ts = $fileCache->cacheTimestamp();
1030  // Send content type and cache headers
1031  $this->sendResponseHeaders( $context, $etag, false );
1032  $response = $fileCache->fetchText();
1033  // Capture any PHP warnings from the output buffer and append them to the
1034  // response in a comment if we're in debug mode.
1035  if ( $context->getDebug() ) {
1036  $warnings = ob_get_contents();
1037  if ( strlen( $warnings ) ) {
1038  $response = self::makeComment( $warnings ) . $response;
1039  }
1040  }
1041  // Remove the output buffer and output the response
1042  ob_end_clean();
1043  echo $response . "\n/* Cached {$ts} */";
1044  return true; // cache hit
1045  }
1046  // Clear buffer
1047  ob_end_clean();
1048 
1049  return false; // cache miss
1050  }
1051 
1060  public static function makeComment( $text ) {
1061  $encText = str_replace( '*/', '* /', $text );
1062  return "/*\n$encText\n*/\n";
1063  }
1064 
1071  public static function formatException( Throwable $e ) {
1072  return self::makeComment( self::formatExceptionNoComment( $e ) );
1073  }
1074 
1082  protected static function formatExceptionNoComment( Throwable $e ) {
1083  global $wgShowExceptionDetails;
1084 
1085  if ( !$wgShowExceptionDetails ) {
1087  }
1088 
1089  return MWExceptionHandler::getLogMessage( $e ) .
1090  "\nBacktrace:\n" .
1092  }
1093 
1105  public function makeModuleResponse( ResourceLoaderContext $context,
1106  array $modules, array $missing = []
1107  ) {
1108  $out = '';
1109  $states = [];
1110 
1111  if ( $modules === [] && $missing === [] ) {
1112  return <<<MESSAGE
1113 /* This file is the Web entry point for MediaWiki's ResourceLoader:
1114  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1115  no modules were requested. Max made me put this here. */
1116 MESSAGE;
1117  }
1118 
1119  $image = $context->getImageObj();
1120  if ( $image ) {
1121  $data = $image->getImageData( $context );
1122  if ( $data === false ) {
1123  $data = '';
1124  $this->errors[] = 'Image generation failed';
1125  }
1126  return $data;
1127  }
1128 
1129  foreach ( $missing as $name ) {
1130  $states[$name] = 'missing';
1131  }
1132 
1133  $only = $context->getOnly();
1134  $filter = $only === 'styles' ? 'minify-css' : 'minify-js';
1135  $debug = (bool)$context->getDebug();
1136 
1137  foreach ( $modules as $name => $module ) {
1138  try {
1139  $content = $module->getModuleContent( $context );
1140  $implementKey = $name . '@' . $module->getVersionHash( $context );
1141  $strContent = '';
1142 
1143  if ( isset( $content['headers'] ) ) {
1144  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1145  }
1146 
1147  // Append output
1148  switch ( $only ) {
1149  case 'scripts':
1150  $scripts = $content['scripts'];
1151  if ( is_string( $scripts ) ) {
1152  // Load scripts raw...
1153  $strContent = $scripts;
1154  } elseif ( is_array( $scripts ) ) {
1155  // ...except when $scripts is an array of URLs or an associative array
1156  $strContent = self::makeLoaderImplementScript(
1157  $context,
1158  $implementKey,
1159  $scripts,
1160  [],
1161  [],
1162  []
1163  );
1164  }
1165  break;
1166  case 'styles':
1167  $styles = $content['styles'];
1168  // We no longer separate into media, they are all combined now with
1169  // custom media type groups into @media .. {} sections as part of the css string.
1170  // Module returns either an empty array or a numerical array with css strings.
1171  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1172  break;
1173  default:
1174  $scripts = $content['scripts'] ?? '';
1175  if ( is_string( $scripts ) ) {
1176  if ( $name === 'site' || $name === 'user' ) {
1177  // Legacy scripts that run in the global scope without a closure.
1178  // mw.loader.implement will use eval if scripts is a string.
1179  // Minify manually here, because general response minification is
1180  // not effective due it being a string literal, not a function.
1181  if ( !$debug ) {
1182  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1183  }
1184  } else {
1185  $scripts = new XmlJsCode( $scripts );
1186  }
1187  }
1188  $strContent = self::makeLoaderImplementScript(
1189  $context,
1190  $implementKey,
1191  $scripts,
1192  $content['styles'] ?? [],
1193  // @phan-suppress-next-line SecurityCheck-XSS
1194  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1195  $content['templates'] ?? []
1196  );
1197  break;
1198  }
1199 
1200  if ( !$debug ) {
1201  $strContent = self::filter( $filter, $strContent, [
1202  // Important: Do not cache minifications of embedded modules
1203  // This is especially for the private 'user.options' module,
1204  // which varies on every pageview and would explode the cache (T84960)
1205  'cache' => !$module->shouldEmbedModule( $context )
1206  ] );
1207  } else {
1208  // In debug mode, separate each response by a new line.
1209  // For example, between 'mw.loader.implement();' statements.
1210  $strContent = self::ensureNewline( $strContent );
1211  }
1212 
1213  if ( $only === 'scripts' ) {
1214  // Use a linebreak between module scripts (T162719)
1215  $out .= self::ensureNewline( $strContent );
1216  } else {
1217  $out .= $strContent;
1218  }
1219 
1220  } catch ( Exception $e ) {
1221  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1222 
1223  // Respond to client with error-state instead of module implementation
1224  $states[$name] = 'error';
1225  unset( $modules[$name] );
1226  }
1227  }
1228 
1229  // Update module states
1230  if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1231  if ( $modules && $only === 'scripts' ) {
1232  // Set the state of modules loaded as only scripts to ready as
1233  // they don't have an mw.loader.implement wrapper that sets the state
1234  foreach ( $modules as $name => $module ) {
1235  $states[$name] = 'ready';
1236  }
1237  }
1238 
1239  // Set the state of modules we didn't respond to with mw.loader.implement
1240  if ( $states ) {
1241  $stateScript = self::makeLoaderStateScript( $context, $states );
1242  if ( !$debug ) {
1243  $stateScript = self::filter( 'minify-js', $stateScript );
1244  }
1245  // Use a linebreak between module script and state script (T162719)
1246  $out = self::ensureNewline( $out ) . $stateScript;
1247  }
1248  } elseif ( $states ) {
1249  $this->errors[] = 'Problematic modules: '
1250  . $context->encodeJson( $states );
1251  }
1252 
1253  return $out;
1254  }
1255 
1262  public static function ensureNewline( $str ) {
1263  $end = substr( $str, -1 );
1264  if ( $end === false || $end === '' || $end === "\n" ) {
1265  return $str;
1266  }
1267  return $str . "\n";
1268  }
1269 
1276  public function getModulesByMessage( $messageKey ) {
1277  $moduleNames = [];
1278  foreach ( $this->getModuleNames() as $moduleName ) {
1279  $module = $this->getModule( $moduleName );
1280  if ( in_array( $messageKey, $module->getMessages() ) ) {
1281  $moduleNames[] = $moduleName;
1282  }
1283  }
1284  return $moduleNames;
1285  }
1286 
1304  private static function makeLoaderImplementScript(
1305  ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates
1306  ) {
1307  if ( $scripts instanceof XmlJsCode ) {
1308  if ( $scripts->value === '' ) {
1309  $scripts = null;
1310  } else {
1311  // @phan-suppress-next-line SecurityCheck-XSS
1312  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1313  }
1314  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1315  $files = $scripts['files'];
1316  foreach ( $files as $path => &$file ) {
1317  // $file is changed (by reference) from a descriptor array to the content of the file
1318  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1319  if ( $file['type'] === 'script' ) {
1320  // Ensure that the script has a newline at the end to close any comment in the
1321  // last line.
1322  $content = self::ensureNewline( $file['content'] );
1323  // Multi-file modules only get two parameters ($ and jQuery are being phased out)
1324  $file = new XmlJsCode( "function ( require, module ) {\n$content}" );
1325  } else {
1326  $file = $file['content'];
1327  }
1328  }
1329  $scripts = XmlJsCode::encodeObject( [
1330  'main' => $scripts['main'],
1331  'files' => XmlJsCode::encodeObject( $files, true )
1332  ], true );
1333  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1334  throw new InvalidArgumentException( 'Script must be a string or an array of URLs' );
1335  }
1336 
1337  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1338  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1339  // of "{}". Force them to objects.
1340  $module = [
1341  $name,
1342  $scripts,
1343  (object)$styles,
1344  (object)$messages,
1345  (object)$templates
1346  ];
1347  self::trimArray( $module );
1348 
1349  // We use pretty output unconditionally to make this method simpler.
1350  // Minification is taken care of closer to the output.
1351  return Xml::encodeJsCall( 'mw.loader.implement', $module, true );
1352  }
1353 
1360  public static function makeMessageSetScript( $messages ) {
1361  return 'mw.messages.set('
1362  . self::encodeJsonForScript( (object)$messages )
1363  . ');';
1364  }
1365 
1373  public static function makeCombinedStyles( array $stylePairs ) {
1374  $out = [];
1375  foreach ( $stylePairs as $media => $styles ) {
1376  // ResourceLoaderFileModule::getStyle can return the styles
1377  // as a string or an array of strings. This is to allow separation in
1378  // the front-end.
1379  $styles = (array)$styles;
1380  foreach ( $styles as $style ) {
1381  $style = trim( $style );
1382  // Don't output an empty "@media print { }" block (T42498)
1383  if ( $style !== '' ) {
1384  // Transform the media type based on request params and config
1385  // The way that this relies on $wgRequest to propagate request params is slightly evil
1386  $media = OutputPage::transformCssMedia( $media );
1387 
1388  if ( $media === '' || $media == 'all' ) {
1389  $out[] = $style;
1390  } elseif ( is_string( $media ) ) {
1391  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1392  }
1393  // else: skip
1394  }
1395  }
1396  }
1397  return $out;
1398  }
1399 
1409  public static function encodeJsonForScript( $data ) {
1410  // Keep output as small as possible by disabling needless escape modes
1411  // that PHP uses by default.
1412  // However, while most module scripts are only served on HTTP responses
1413  // for JavaScript, some modules can also be embedded in the HTML as inline
1414  // scripts. This, and the fact that we sometimes need to export strings
1415  // containing user-generated content and labels that may genuinely contain
1416  // a sequences like "</script>", we need to encode either '/' or '<'.
1417  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1418  // and allows URLs to mostly remain readable.
1419  $jsonFlags = JSON_UNESCAPED_SLASHES |
1420  JSON_UNESCAPED_UNICODE |
1421  JSON_HEX_TAG |
1422  JSON_HEX_AMP;
1423  if ( self::inDebugMode() ) {
1424  $jsonFlags |= JSON_PRETTY_PRINT;
1425  }
1426  return json_encode( $data, $jsonFlags );
1427  }
1428 
1441  public static function makeLoaderStateScript(
1442  ResourceLoaderContext $context, array $states
1443  ) {
1444  return 'mw.loader.state('
1445  . $context->encodeJson( $states )
1446  . ');';
1447  }
1448 
1449  private static function isEmptyObject( stdClass $obj ) {
1450  foreach ( $obj as $key => $value ) {
1451  return false;
1452  }
1453  return true;
1454  }
1455 
1469  private static function trimArray( array &$array ): void {
1470  $i = count( $array );
1471  while ( $i-- ) {
1472  if ( $array[$i] === null
1473  || $array[$i] === []
1474  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1475  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1476  ) {
1477  unset( $array[$i] );
1478  } else {
1479  break;
1480  }
1481  }
1482  }
1483 
1509  public static function makeLoaderRegisterScript(
1510  ResourceLoaderContext $context, array $modules
1511  ) {
1512  // Optimisation: Transform dependency names into indexes when possible
1513  // to produce smaller output. They are expanded by mw.loader.register on
1514  // the other end.
1515  $index = [];
1516  foreach ( $modules as $i => &$module ) {
1517  // Build module name index
1518  $index[$module[0]] = $i;
1519  }
1520  foreach ( $modules as &$module ) {
1521  if ( isset( $module[2] ) ) {
1522  foreach ( $module[2] as &$dependency ) {
1523  if ( isset( $index[$dependency] ) ) {
1524  // Replace module name in dependency list with index
1525  $dependency = $index[$dependency];
1526  }
1527  }
1528  }
1529  }
1530 
1531  array_walk( $modules, [ self::class, 'trimArray' ] );
1532 
1533  return 'mw.loader.register('
1534  . $context->encodeJson( $modules )
1535  . ');';
1536  }
1537 
1551  public static function makeLoaderSourcesScript(
1552  ResourceLoaderContext $context, array $sources
1553  ) {
1554  return 'mw.loader.addSource('
1555  . $context->encodeJson( $sources )
1556  . ');';
1557  }
1558 
1565  public static function makeLoaderConditionalScript( $script ) {
1566  // Adds a function to lazy-created RLQ
1567  return '(RLQ=window.RLQ||[]).push(function(){' .
1568  trim( $script ) . '});';
1569  }
1570 
1579  public static function makeInlineCodeWithModule( $modules, $script ) {
1580  // Adds an array to lazy-created RLQ
1581  return '(RLQ=window.RLQ||[]).push(['
1583  . 'function(){' . trim( $script ) . '}'
1584  . ']);';
1585  }
1586 
1598  public static function makeInlineScript( $script, $nonce = null ) {
1599  $js = self::makeLoaderConditionalScript( $script );
1600  $escNonce = '';
1601  if ( $nonce === null ) {
1602  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1603  } elseif ( $nonce !== false ) {
1604  // If it was false, CSP is disabled, so no nonce attribute.
1605  // Nonce should be only base64 characters, so should be safe,
1606  // but better to be safely escaped than sorry.
1607  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1608  }
1609 
1610  return new WrappedString(
1611  Html::inlineScript( $js, $nonce ),
1612  "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1613  '});</script>'
1614  );
1615  }
1616 
1625  public static function makeConfigSetScript( array $configuration ) {
1626  $json = self::encodeJsonForScript( $configuration );
1627  if ( $json === false ) {
1628  $e = new Exception(
1629  'JSON serialization of config data failed. ' .
1630  'This usually means the config data is not valid UTF-8.'
1631  );
1633  return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1634  }
1635  return "mw.config.set($json);";
1636  }
1637 
1651  public static function makePackedModulesString( array $modules ) {
1652  $moduleMap = []; // [ prefix => [ suffixes ] ]
1653  foreach ( $modules as $module ) {
1654  $pos = strrpos( $module, '.' );
1655  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1656  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1657  $moduleMap[$prefix][] = $suffix;
1658  }
1659 
1660  $arr = [];
1661  foreach ( $moduleMap as $prefix => $suffixes ) {
1662  $p = $prefix === '' ? '' : $prefix . '.';
1663  $arr[] = $p . implode( ',', $suffixes );
1664  }
1665  return implode( '|', $arr );
1666  }
1667 
1679  public static function expandModuleNames( $modules ) {
1680  $retval = [];
1681  $exploded = explode( '|', $modules );
1682  foreach ( $exploded as $group ) {
1683  if ( strpos( $group, ',' ) === false ) {
1684  // This is not a set of modules in foo.bar,baz notation
1685  // but a single module
1686  $retval[] = $group;
1687  } else {
1688  // This is a set of modules in foo.bar,baz notation
1689  $pos = strrpos( $group, '.' );
1690  if ( $pos === false ) {
1691  // Prefixless modules, i.e. without dots
1692  $retval = array_merge( $retval, explode( ',', $group ) );
1693  } else {
1694  // We have a prefix and a bunch of suffixes
1695  $prefix = substr( $group, 0, $pos ); // 'foo'
1696  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1697  foreach ( $suffixes as $suffix ) {
1698  $retval[] = "$prefix.$suffix";
1699  }
1700  }
1701  }
1702  }
1703  return $retval;
1704  }
1705 
1716  public static function inDebugMode() {
1717  if ( self::$debugMode === null ) {
1719  $str = $wgRequest->getRawVal( 'debug',
1720  $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ? 'true' : '' )
1721  );
1722  self::$debugMode = ResourceLoaderContext::debugFromString( $str );
1723  }
1724  return self::$debugMode;
1725  }
1726 
1737  public static function clearCache() {
1738  self::$debugMode = null;
1739  }
1740 
1750  public function createLoaderURL( $source, ResourceLoaderContext $context,
1751  array $extraQuery = []
1752  ) {
1753  $query = self::createLoaderQuery( $context, $extraQuery );
1754  $script = $this->getLoadScript( $source );
1755 
1756  return wfAppendQuery( $script, $query );
1757  }
1758 
1768  protected static function createLoaderQuery(
1769  ResourceLoaderContext $context, array $extraQuery = []
1770  ) {
1771  return self::makeLoaderQuery(
1772  $context->getModules(),
1773  $context->getLanguage(),
1774  $context->getSkin(),
1775  $context->getUser(),
1776  $context->getVersion(),
1777  $context->getDebug(),
1778  $context->getOnly(),
1779  $context->getRequest()->getBool( 'printable' ),
1780  null,
1781  $extraQuery
1782  );
1783  }
1784 
1801  public static function makeLoaderQuery( array $modules, $lang, $skin, $user = null,
1802  $version = null, $debug = ResourceLoaderContext::DEBUG_OFF, $only = null,
1803  $printable = false, $handheld = null, array $extraQuery = []
1804  ) {
1805  $query = [
1806  'modules' => self::makePackedModulesString( $modules ),
1807  ];
1808  // Keep urls short by omitting query parameters that
1809  // match the defaults assumed by ResourceLoaderContext.
1810  // Note: This relies on the defaults either being insignificant or forever constant,
1811  // as otherwise cached urls could change in meaning when the defaults change.
1813  $query['lang'] = $lang;
1814  }
1815  if ( $skin !== ResourceLoaderContext::DEFAULT_SKIN ) {
1816  $query['skin'] = $skin;
1817  }
1819  $query['debug'] = strval( $debug );
1820  }
1821  if ( $user !== null ) {
1822  $query['user'] = $user;
1823  }
1824  if ( $version !== null ) {
1825  $query['version'] = $version;
1826  }
1827  if ( $only !== null ) {
1828  $query['only'] = $only;
1829  }
1830  if ( $printable ) {
1831  $query['printable'] = 1;
1832  }
1833  $query += $extraQuery;
1834 
1835  // Make queries uniform in order
1836  ksort( $query );
1837  return $query;
1838  }
1839 
1849  public static function isValidModuleName( $moduleName ) {
1850  $len = strlen( $moduleName );
1851  return $len <= 255 && strcspn( $moduleName, '!,|', 0, $len ) === $len;
1852  }
1853 
1865  public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1866  global $IP;
1867  // When called from the installer, it is possible that a required PHP extension
1868  // is missing (at least for now; see T49564). If this is the case, throw an
1869  // exception (caught by the installer) to prevent a fatal error later on.
1870  if ( !class_exists( Less_Parser::class ) ) {
1871  throw new MWException( 'MediaWiki requires the less.php parser' );
1872  }
1873 
1874  $importDirs[] = "$IP/resources/src/mediawiki.less";
1875 
1876  $parser = new Less_Parser;
1877  $parser->ModifyVars( $vars );
1878  // SetImportDirs expects an array like [ 'path1' => '', 'path2' => '' ]
1879  $parser->SetImportDirs( array_fill_keys( $importDirs, '' ) );
1880  $parser->SetOption( 'relativeUrls', false );
1881 
1882  return $parser;
1883  }
1884 
1898  public function expandUrl( string $base, string $url ): string {
1899  // Net_URL2::resolve() doesn't allow protocol-relative URLs, but we do.
1900  $isProtoRelative = strpos( $base, '//' ) === 0;
1901  if ( $isProtoRelative ) {
1902  $base = "https:$base";
1903  }
1904  // Net_URL2::resolve() takes care of throwing if $base doesn't have a server.
1905  $baseUrl = new Net_URL2( $base );
1906  $ret = $baseUrl->resolve( $url );
1907  if ( $isProtoRelative ) {
1908  $ret->setScheme( false );
1909  }
1910  return $ret->getURL();
1911  }
1912 
1920  public static function getUserDefaults( ResourceLoaderContext $context ): array {
1921  // TODO inject
1922  return MediaWikiServices::getInstance()->getUserOptionsLookup()->getDefaultOptions();
1923  }
1924 
1933  public static function getSiteConfigSettings(
1934  ResourceLoaderContext $context, Config $conf
1935  ): array {
1936  // Namespace related preparation
1937  // - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
1938  // - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
1939  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1940  $namespaceIds = $contLang->getNamespaceIds();
1941  $caseSensitiveNamespaces = [];
1942  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1943  foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
1944  $namespaceIds[$contLang->lc( $name )] = $index;
1945  if ( !$nsInfo->isCapitalized( $index ) ) {
1946  $caseSensitiveNamespaces[] = $index;
1947  }
1948  }
1949 
1950  $illegalFileChars = $conf->get( 'IllegalFileChars' );
1951 
1952  // Build list of variables
1953  $skin = $context->getSkin();
1954 
1955  // Start of supported and stable config vars (for use by extensions/gadgets).
1956  $vars = [
1957  'debug' => $context->getDebug(),
1958  'skin' => $skin,
1959  'stylepath' => $conf->get( 'StylePath' ),
1960  'wgArticlePath' => $conf->get( 'ArticlePath' ),
1961  'wgScriptPath' => $conf->get( 'ScriptPath' ),
1962  'wgScript' => $conf->get( 'Script' ),
1963  'wgSearchType' => $conf->get( 'SearchType' ),
1964  'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
1965  'wgServer' => $conf->get( 'Server' ),
1966  'wgServerName' => $conf->get( 'ServerName' ),
1967  'wgUserLanguage' => $context->getLanguage(),
1968  'wgContentLanguage' => $contLang->getCode(),
1969  'wgVersion' => MW_VERSION,
1970  'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
1971  'wgNamespaceIds' => $namespaceIds,
1972  'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
1973  'wgSiteName' => $conf->get( 'Sitename' ),
1974  'wgDBname' => $conf->get( 'DBname' ),
1975  'wgWikiID' => WikiMap::getCurrentWikiId(),
1976  'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
1977  'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
1978  'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
1979  ];
1980  // End of stable config vars.
1981 
1982  // Internal variables for use by MediaWiki core and/or ResourceLoader.
1983  $vars += [
1984  // @internal For mediawiki.widgets
1985  'wgUrlProtocols' => wfUrlProtocols(),
1986  // @internal For mediawiki.page.watch
1987  // Force object to avoid "empty" associative array from
1988  // becoming [] instead of {} in JS (T36604)
1989  'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
1990  // @internal For mediawiki.language
1991  'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ),
1992  // @internal For mediawiki.Title
1993  'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
1994  'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
1995  'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
1996  ];
1997 
1998  Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );
1999 
2000  return $vars;
2001  }
2002 }
ResourceLoader\filter
static filter( $filter, $data, array $options=[])
Run JavaScript or CSS data through a filter, caching the filtered result for future calls.
Definition: ResourceLoader.php:183
ResourceLoader\makeLoaderConditionalScript
static makeLoaderConditionalScript( $script)
Wrap JavaScript code to run after the startup module.
Definition: ResourceLoader.php:1563
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:37
ResourceLoader\addSource
addSource( $sources, $loadUrl=null)
Add a foreign source of modules.
Definition: ResourceLoader.php:422
ResourceLoader\getMessageBlobStore
getMessageBlobStore()
Definition: ResourceLoader.php:300
wfResetOutputBuffers
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
Definition: GlobalFunctions.php:1604
ResourceLoader\sendResponseHeaders
sendResponseHeaders(ResourceLoaderContext $context, $etag, $errors, array $extra=[])
Send main response headers to the client.
Definition: ResourceLoader.php:915
ResourceLoader\$errors
array $errors
Errors accumulated during current respond() call.
Definition: ResourceLoader.php:91
ResourceLoader\getLessCompiler
getLessCompiler(array $vars=[], array $importDirs=[])
Return a LESS compiler that is set up for use with MediaWiki.
Definition: ResourceLoader.php:1863
CACHE_ANYTHING
const CACHE_ANYTHING
Definition: Defines.php:85
ResourceLoader\makeConfigSetScript
static makeConfigSetScript(array $configuration)
Return JS code which will set the MediaWiki configuration array to the given value.
Definition: ResourceLoader.php:1623
ResourceLoader\ensureNewline
static ensureNewline( $str)
Ensure the string is either empty or ends in a line break.
Definition: ResourceLoader.php:1260
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:273
ResourceLoader\$depStore
DependencyStore $depStore
Definition: ResourceLoader.php:65
ResourceFileCache\newFromContext
static newFromContext(ResourceLoaderContext $context)
Construct an ResourceFileCache from a context.
Definition: ResourceFileCache.php:40
HashBagOStuff
Simple store for keeping values in an associative array for the current process.
Definition: HashBagOStuff.php:32
ResourceLoader\$depStoreUpdateBuffer
array $depStoreUpdateBuffer
Map of (module-variant => buffered DependencyStore updates)
Definition: ResourceLoader.php:96
FileCacheBase\isCacheGood
isCacheGood( $timestamp='')
Check if up to date cache file exists.
Definition: FileCacheBase.php:119
ResourceLoader\formatException
static formatException(Throwable $e)
Handle exception display.
Definition: ResourceLoader.php:1071
ResourceLoader\encodeJsonForScript
static encodeJsonForScript( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
Definition: ResourceLoader.php:1407
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:202
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
$wgRequest
$wgRequest
Definition: Setup.php:722
ResourceLoader\makeInlineScript
static makeInlineScript( $script, $nonce=null)
Make an HTML script that runs given JS code after startup and base modules.
Definition: ResourceLoader.php:1596
MWExceptionHandler\getPublicLogMessage
static getPublicLogMessage(Throwable $e)
Definition: MWExceptionHandler.php:535
ResourceLoaderContext\getModules
getModules()
Definition: ResourceLoaderContext.php:200
ResourceLoader\getSiteConfigSettings
static getSiteConfigSettings(ResourceLoaderContext $context, Config $conf)
Get site configuration settings to expose to JavaScript on all pages via mw.config.
Definition: ResourceLoader.php:1931
ResourceLoader\getLogger
getLogger()
Definition: ResourceLoader.php:292
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:36
ResourceLoader\$hookContainer
HookContainer $hookContainer
Definition: ResourceLoader.php:71
ResourceLoader\getTestSuiteModuleNames
getTestSuiteModuleNames()
Get a list of module names with QUnit test suites.
Definition: ResourceLoader.php:458
ResourceLoader\$testSuiteModuleNames
string[] $testSuiteModuleNames
List of module names that contain QUnit test suites.
Definition: ResourceLoader.php:87
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
ResourceLoader\makeLoaderRegisterScript
static makeLoaderRegisterScript(ResourceLoaderContext $context, array $modules)
Format JS code which calls mw.loader.register() with the given parameters.
Definition: ResourceLoader.php:1507
ResourceLoader\HookRunner
Definition: HookRunner.php:18
ResourceLoader\__construct
__construct(Config $config, LoggerInterface $logger=null, DependencyStore $tracker=null)
Register core modules and runs registration hooks.
Definition: ResourceLoader.php:246
ResourceLoaderModule\getVary
static getVary(ResourceLoaderContext $context)
Get vary string.
Definition: ResourceLoaderModule.php:1074
ResourceLoaderContext\getOnly
getOnly()
Definition: ResourceLoaderContext.php:321
$base
$base
Definition: generateLocalAutoload.php:11
WikiMap\getCurrentWikiId
static getCurrentWikiId()
Definition: WikiMap.php:303
Title\convertByteClassToUnicodeClass
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition: Title.php:766
ResourceLoader\isEmptyObject
static isEmptyObject(stdClass $obj)
Definition: ResourceLoader.php:1447
ResourceLoader\$extraHeaders
string[] $extraHeaders
Extra HTTP response headers from modules loaded in makeModuleResponse()
Definition: ResourceLoader.php:93
ResourceLoader\preloadModuleInfo
preloadModuleInfo(array $moduleNames, ResourceLoaderContext $context)
Load information stored in the database and dependency tracking store about modules.
Definition: ResourceLoader.php:123
OutputPage\transformCssMedia
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
Definition: OutputPage.php:4080
$dbr
$dbr
Definition: testCompression.php:54
$debug
$debug
Definition: mcc.php:31
wfAppendQuery
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
Definition: GlobalFunctions.php:422
XmlJsCode
A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to interpret a given string a...
Definition: XmlJsCode.php:40
ResourceLoader\expandUrl
expandUrl(string $base, string $url)
Resolve a possibly relative URL against a base URL.
Definition: ResourceLoader.php:1896
ExtensionRegistry\getInstance
static getInstance()
Definition: ExtensionRegistry.php:134
MWExceptionHandler\logException
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
Definition: MWExceptionHandler.php:700
Xml\encodeJsCall
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition: Xml.php:691
ResourceLoader\setMessageBlobStore
setMessageBlobStore(MessageBlobStore $blobStore)
Definition: ResourceLoader.php:308
Config
Interface for configuration instances.
Definition: Config.php:30
MWExceptionHandler\getRedactedTraceAsString
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
Definition: MWExceptionHandler.php:375
ResourceLoader\clearCache
static clearCache()
Reset static members used for caching.
Definition: ResourceLoader.php:1735
ResourceLoaderContext\DEBUG_OFF
const DEBUG_OFF
Definition: ResourceLoaderContext.php:42
MWException
MediaWiki exception.
Definition: MWException.php:29
ResourceLoader\$moduleInfos
array[] $moduleInfos
Map of (module name => associative info array)
Definition: ResourceLoader.php:79
ResourceLoader\makeModuleResponse
makeModuleResponse(ResourceLoaderContext $context, array $modules, array $missing=[])
Generate code for a response.
Definition: ResourceLoader.php:1105
ResourceLoader\$debugMode
static int null $debugMode
Definition: ResourceLoader.php:104
ResourceLoaderContext\getRequest
getRequest()
Definition: ResourceLoaderContext.php:186
ResourceLoaderWikiModule\preloadTitleInfo
static preloadTitleInfo(ResourceLoaderContext $context, IDatabase $db, array $moduleNames)
Definition: ResourceLoaderWikiModule.php:477
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$blob
$blob
Definition: testCompression.php:70
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2200
ResourceLoader\makeHash
static makeHash( $value)
Create a hash for module versioning purposes.
Definition: ResourceLoader.php:678
ResourceLoaderContext\getDebug
getDebug()
Definition: ResourceLoaderContext.php:314
ResourceLoader\getSources
getSources()
Get the list of sources.
Definition: ResourceLoader.php:592
ResourceLoader\makeLoaderStateScript
static makeLoaderStateScript(ResourceLoaderContext $context, array $states)
Returns a JS call to mw.loader.state, which sets the state of modules to a given value:
Definition: ResourceLoader.php:1439
ResourceLoader\makeInlineCodeWithModule
static makeInlineCodeWithModule( $modules, $script)
Wrap JavaScript code to run after a required module.
Definition: ResourceLoader.php:1577
ResourceLoader\makeComment
static makeComment( $text)
Generate a CSS or JS comment block.
Definition: ResourceLoader.php:1060
ResourceLoaderModule\expandRelativePaths
static expandRelativePaths(array $filePaths)
Expand directories relative to $IP.
Definition: ResourceLoaderModule.php:601
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:204
ResourceLoader\$logger
LoggerInterface $logger
Definition: ResourceLoader.php:68
ResourceLoaderContext\getVersion
getVersion()
Definition: ResourceLoaderContext.php:330
FileCacheBase\fetchText
fetchText()
Get the uncompressed text from the cache.
Definition: FileCacheBase.php:146
ResourceLoader\makePackedModulesString
static makePackedModulesString(array $modules)
Convert an array of module names to a packed query string.
Definition: ResourceLoader.php:1649
ResourceLoader\setDependencyStore
setDependencyStore(DependencyStore $tracker)
Definition: ResourceLoader.php:316
ResourceLoader\getModulesByMessage
getModulesByMessage( $messageKey)
Get names of modules that use a certain message.
Definition: ResourceLoader.php:1274
ResourceLoader\$modules
ResourceLoaderModule[] $modules
Map of (module name => ResourceLoaderModule)
Definition: ResourceLoader.php:77
ResourceLoader\setModuleSkinStyles
setModuleSkinStyles(array $moduleSkinStyles)
Definition: ResourceLoader.php:324
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:702
ResourceLoaderContext\DEFAULT_SKIN
const DEFAULT_SKIN
Definition: ResourceLoaderContext.php:39
ResourceLoader\getModule
getModule( $name)
Get the ResourceLoaderModule object for a given module name.
Definition: ResourceLoader.php:484
$content
$content
Definition: router.php:76
ResourceFileCache\useFileCache
static useFileCache(ResourceLoaderContext $context)
Check if an RL request can be cached.
Definition: ResourceFileCache.php:66
ResourceLoader\getLoadScript
getLoadScript( $source)
Get the URL to the load.php endpoint for the given ResourceLoader source.
Definition: ResourceLoader.php:604
ResourceLoader\getUserDefaults
static getUserDefaults(ResourceLoaderContext $context)
Get user default options to expose to JavaScript on all pages via mw.user.options.
Definition: ResourceLoader.php:1918
Html\inlineScript
static inlineScript( $contents, $nonce=null)
Output an HTML script tag with the given contents.
Definition: Html.php:589
$header
$header
Definition: updateCredits.php:37
XmlJsCode\encodeObject
static encodeObject( $obj, $pretty=false)
Encode an object containing XmlJsCode objects.
Definition: XmlJsCode.php:59
ResourceLoader\makeCombinedStyles
static makeCombinedStyles(array $stylePairs)
Combines an associative array mapping media type to CSS into a single stylesheet with "@media" blocks...
Definition: ResourceLoader.php:1371
ResourceLoaderContext\DEFAULT_LANG
const DEFAULT_LANG
Definition: ResourceLoaderContext.php:38
MediaWiki\HeaderCallback
Definition: HeaderCallback.php:8
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
ResourceLoader\applyFilter
static applyFilter( $filter, $data)
Definition: ResourceLoader.php:225
ResourceLoader\$hookRunner
HookRunner $hookRunner
Definition: ResourceLoader.php:74
ResourceLoader\HASH_LENGTH
const HASH_LENGTH
Definition: ResourceLoader.php:614
ResourceLoader\$testModuleNames
array $testModuleNames
Associative array mapping framework ids to a list of names of test suite modules like [ 'qunit' => [ ...
Definition: ResourceLoader.php:85
ResourceLoader\isModuleRegistered
isModuleRegistered( $name)
Check whether a ResourceLoader module is registered.
Definition: ResourceLoader.php:469
ResourceLoader\expandModuleNames
static expandModuleNames( $modules)
Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to an array of module names like ‘[ 'jq...
Definition: ResourceLoader.php:1677
Wikimedia\DependencyStore\KeyValueDependencyStore
Lightweight class for tracking path dependencies lists via an object cache instance.
Definition: KeyValueDependencyStore.php:36
ResourceLoader\$blobStore
MessageBlobStore $blobStore
Definition: ResourceLoader.php:63
ResourceLoader\getCombinedVersion
getCombinedVersion(ResourceLoaderContext $context, array $moduleNames)
Helper method to get and combine versions of multiple modules.
Definition: ResourceLoader.php:715
ResourceLoaderContext\getSkin
getSkin()
Definition: ResourceLoaderContext.php:237
ResourceLoaderContext\shouldIncludeScripts
shouldIncludeScripts()
Definition: ResourceLoaderContext.php:410
ResourceLoader\respond
respond(ResourceLoaderContext $context)
Output a response to a load request, including the content-type header.
Definition: ResourceLoader.php:774
ResourceLoader\$config
Config $config
Definition: ResourceLoader.php:61
ResourceLoader\trimArray
static trimArray(array &$array)
Remove empty values from the end of an array.
Definition: ResourceLoader.php:1467
ResourceLoaderContext\getUser
getUser()
Definition: ResourceLoaderContext.php:244
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:48
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:39
ResourceLoader
ResourceLoader is a loading system for JavaScript and CSS resources.
Definition: ResourceLoader.php:59
ResourceLoader\makeLoaderImplementScript
static makeLoaderImplementScript(ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates)
Return JS code that calls mw.loader.implement with given module properties.
Definition: ResourceLoader.php:1302
$cache
$cache
Definition: mcc.php:33
HttpStatus\header
static header( $code)
Output an HTTP status code header.
Definition: HttpStatus.php:96
ResourceLoader\createLoaderURL
createLoaderURL( $source, ResourceLoaderContext $context, array $extraQuery=[])
Build a load.php URL.
Definition: ResourceLoader.php:1748
WebRequest\GETHEADER_LIST
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
Definition: WebRequest.php:73
ResourceLoader\inDebugMode
static inDebugMode()
Determine whether debug mode is on.
Definition: ResourceLoader.php:1714
ResourceLoader\tryRespondFromFileCache
tryRespondFromFileCache(ResourceFileCache $fileCache, ResourceLoaderContext $context, $etag)
Send out code for a response from file cache if possible.
Definition: ResourceLoader.php:1006
ResourceLoader\loadModuleDependenciesInternal
loadModuleDependenciesInternal( $moduleName, $variant)
Definition: ResourceLoader.php:521
$path
$path
Definition: NoLocalSettings.php:25
$wgShowExceptionDetails
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
Definition: DefaultSettings.php:7343
ResourceLoader\$moduleSkinStyles
array $moduleSkinStyles
Styles that are skin-specific and supplement or replace the default skinStyles of a FileModule.
Definition: ResourceLoader.php:101
ResourceLoader\makeMessageSetScript
static makeMessageSetScript( $messages)
Returns JS code which, when called, will register a given list of messages.
Definition: ResourceLoader.php:1358
Wikimedia\Rdbms\DBConnectionError
@newable
Definition: DBConnectionError.php:27
Wikimedia
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
MessageBlobStore
This class generates message blobs for use by ResourceLoader.
Definition: MessageBlobStore.php:38
$source
$source
Definition: mwdoc-filter.php:34
ResourceLoader\measureResponseTime
measureResponseTime()
Send stats about the time used to build the response.
Definition: ResourceLoader.php:896
ResourceLoader\registerTestModules
registerTestModules()
Definition: ResourceLoader.php:372
ResourceLoader\makeLoaderSourcesScript
static makeLoaderSourcesScript(ResourceLoaderContext $context, array $sources)
Format JS code which calls mw.loader.addSource() with the given parameters.
Definition: ResourceLoader.php:1549
ResourceLoader\tryRespondNotModified
tryRespondNotModified(ResourceLoaderContext $context, $etag)
Respond with HTTP 304 Not Modified if appropiate.
Definition: ResourceLoader.php:973
$hashes
$hashes
Definition: testCompression.php:71
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
ResourceLoader\$sources
array $sources
Map of (source => path); E.g.
Definition: ResourceLoader.php:89
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1043
ResourceLoaderContext\debugFromString
static debugFromString(?string $debug)
Definition: ResourceLoaderContext.php:136
Title\legalChars
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:752
ResourceLoaderContext\getImageObj
getImageObj()
If this is a request for an image, get the ResourceLoaderImage object.
Definition: ResourceLoaderContext.php:365
ResourceLoader\formatExceptionNoComment
static formatExceptionNoComment(Throwable $e)
Handle exception display.
Definition: ResourceLoader.php:1082
ResourceLoader\getConfig
getConfig()
Definition: ResourceLoader.php:276
ResourceLoader\createLoaderQuery
static createLoaderQuery(ResourceLoaderContext $context, array $extraQuery=[])
Helper for createLoaderURL()
Definition: ResourceLoader.php:1766
ResourceLoader\saveModuleDependenciesInternal
saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths)
Definition: ResourceLoader.php:534
Wikimedia\DependencyStore\DependencyStore
Class for tracking per-entity dependency path lists that are expensive to mass compute.
Definition: DependencyStore.php:28
$tracker
if(count( $args)< 1) $tracker
Definition: trackBlobs.php:37
$IP
$IP
Definition: WebStart.php:49
ResourceLoader\makeVersionQuery
makeVersionQuery(ResourceLoaderContext $context, array $modules)
Get the expected value of the 'version' query parameter.
Definition: ResourceLoader.php:751
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
Definition: DeferredUpdates.php:145
ResourceFileCache
ResourceLoader request result caching in the file system.
Definition: ResourceFileCache.php:29
$wgResourceLoaderDebug
$wgResourceLoaderDebug
The default debug mode (on/off) for of ResourceLoader requests.
Definition: DefaultSettings.php:4448
ResourceLoaderContext\getRaw
getRaw()
Definition: ResourceLoaderContext.php:334
ResourceLoader\makeLoaderQuery
static makeLoaderQuery(array $modules, $lang, $skin, $user=null, $version=null, $debug=ResourceLoaderContext::DEBUG_OFF, $only=null, $printable=false, $handheld=null, array $extraQuery=[])
Build a query array (array representation of query string) for load.php.
Definition: ResourceLoader.php:1799
ResourceLoaderContext\encodeJson
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
Definition: ResourceLoaderContext.php:484
ResourceLoader\outputErrorAndLog
outputErrorAndLog(Exception $e, $msg, array $context=[])
Add an error to the 'errors' array and log it.
Definition: ResourceLoader.php:698
MWExceptionHandler\getLogMessage
static getLogMessage(Throwable $e)
Get a message formatting the throwable message and its origin.
Definition: MWExceptionHandler.php:486
ResourceLoader\isValidModuleName
static isValidModuleName( $moduleName)
Check a module name for validity.
Definition: ResourceLoader.php:1847
ResourceLoader\getModuleNames
getModuleNames()
Definition: ResourceLoader.php:447
ResourceLoader\setLogger
setLogger(LoggerInterface $logger)
Definition: ResourceLoader.php:284
FileCacheBase\cacheTimestamp
cacheTimestamp()
Get the last-modified timestamp of the cache file.
Definition: FileCacheBase.php:105
ObjectCache\getLocalServerInstance
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
Definition: ObjectCache.php:255