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\Timestamp\ConvertibleTimestamp;
36 use Wikimedia\WrappedString;
37 
58 class ResourceLoader implements LoggerAwareInterface {
60  protected $config;
62  protected $blobStore;
64  protected $depStore;
65 
67  private $logger;
68 
70  private $hookContainer;
71 
73  private $hookRunner;
74 
76  protected $modules = [];
78  protected $moduleInfos = [];
84  protected $testModuleNames = [];
86  protected $testSuiteModuleNames = [];
88  protected $sources = [];
90  protected $errors = [];
92  protected $extraHeaders = [];
93 
95  private $depStoreUpdateBuffer = [];
96 
100  private $moduleSkinStyles = [];
101 
103  protected static $debugMode = null;
104 
106  public const CACHE_VERSION = 8;
107 
109  private const RL_DEP_STORE_PREFIX = 'ResourceLoaderModule';
111  private const RL_MODULE_DEP_TTL = BagOStuff::TTL_WEEK;
112 
114  public const FILTER_NOMIN = '/*@nomin*/';
115 
122  public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
123  // Load all tracked indirect file dependencies for the modules
124  $vary = ResourceLoaderModule::getVary( $context );
125  $entitiesByModule = [];
126  foreach ( $moduleNames as $moduleName ) {
127  $entitiesByModule[$moduleName] = "$moduleName|$vary";
128  }
129  $depsByEntity = $this->depStore->retrieveMulti(
130  self::RL_DEP_STORE_PREFIX,
131  $entitiesByModule
132  );
133  // Inject the indirect file dependencies for all the modules
134  foreach ( $moduleNames as $moduleName ) {
135  $module = $this->getModule( $moduleName );
136  if ( $module ) {
137  $entity = $entitiesByModule[$moduleName];
138  $deps = $depsByEntity[$entity];
139  $paths = ResourceLoaderModule::expandRelativePaths( $deps['paths'] );
140  $module->setFileDependencies( $context, $paths );
141  }
142  }
143 
144  // Batched version of ResourceLoaderWikiModule::getTitleInfo
145  $dbr = wfGetDB( DB_REPLICA );
146  ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
147 
148  // Prime in-object cache for message blobs for modules with messages
149  $modulesWithMessages = [];
150  foreach ( $moduleNames as $moduleName ) {
151  $module = $this->getModule( $moduleName );
152  if ( $module && $module->getMessages() ) {
153  $modulesWithMessages[$moduleName] = $module;
154  }
155  }
156  // Prime in-object cache for message blobs for modules with messages
157  $lang = $context->getLanguage();
158  $store = $this->getMessageBlobStore();
159  $blobs = $store->getBlobs( $modulesWithMessages, $lang );
160  foreach ( $blobs as $moduleName => $blob ) {
161  $modulesWithMessages[$moduleName]->setMessageBlob( $blob, $lang );
162  }
163  }
164 
182  public static function filter( $filter, $data, array $options = [] ) {
183  if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
184  return $data;
185  }
186 
187  if ( isset( $options['cache'] ) && $options['cache'] === false ) {
188  return self::applyFilter( $filter, $data ) ?? $data;
189  }
190 
191  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
193 
194  $key = $cache->makeGlobalKey(
195  'resourceloader-filter',
196  $filter,
197  self::CACHE_VERSION,
198  md5( $data )
199  );
200 
201  $result = $cache->get( $key );
202  if ( $result === false ) {
203  $stats->increment( "resourceloader_cache.$filter.miss" );
204  $result = self::applyFilter( $filter, $data );
205  $cache->set( $key, $result, 24 * 3600 );
206  } else {
207  $stats->increment( "resourceloader_cache.$filter.hit" );
208  }
209  if ( $result === null ) {
210  // Cached failure
211  $result = $data;
212  }
213 
214  return $result;
215  }
216 
222  private static function applyFilter( $filter, $data ) {
223  $data = trim( $data );
224  if ( $data ) {
225  try {
226  $data = ( $filter === 'minify-css' )
227  ? CSSMin::minify( $data )
228  : JavaScriptMinifier::minify( $data );
229  } catch ( Exception $e ) {
231  return null;
232  }
233  }
234  return $data;
235  }
236 
243  public function __construct(
244  Config $config,
245  LoggerInterface $logger = null,
247  ) {
248  $this->logger = $logger ?: new NullLogger();
249  $services = MediaWikiServices::getInstance();
250 
251  $this->config = $config;
252 
253  $this->hookContainer = $services->getHookContainer();
254  $this->hookRunner = new HookRunner( $this->hookContainer );
255 
256  // Add 'local' source first
257  $this->addSource( 'local', $config->get( 'LoadScript' ) );
258 
259  // Special module that always exists
260  $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
261 
262  $this->setMessageBlobStore(
263  new MessageBlobStore( $this, $this->logger, $services->getMainWANObjectCache() )
264  );
265 
267  $this->setDependencyStore( $tracker );
268  }
269 
273  public function getConfig() {
274  return $this->config;
275  }
276 
281  public function setLogger( LoggerInterface $logger ) {
282  $this->logger = $logger;
283  }
284 
289  public function getLogger() {
290  return $this->logger;
291  }
292 
297  public function getMessageBlobStore() {
298  return $this->blobStore;
299  }
300 
306  $this->blobStore = $blobStore;
307  }
308 
314  $this->depStore = $tracker;
315  }
316 
321  public function setModuleSkinStyles( array $moduleSkinStyles ) {
322  $this->moduleSkinStyles = $moduleSkinStyles;
323  }
324 
336  public function register( $name, array $info = null ) {
337  // Allow multiple modules to be registered in one call
338  $registrations = is_array( $name ) ? $name : [ $name => $info ];
339  foreach ( $registrations as $name => $info ) {
340  // Warn on duplicate registrations
341  if ( isset( $this->moduleInfos[$name] ) ) {
342  // A module has already been registered by this name
343  $this->logger->warning(
344  'ResourceLoader duplicate registration warning. ' .
345  'Another module has already been registered as ' . $name
346  );
347  }
348 
349  // Check validity
350  if ( !self::isValidModuleName( $name ) ) {
351  throw new InvalidArgumentException( "ResourceLoader module name '$name' is invalid, "
352  . "see ResourceLoader::isValidModuleName()" );
353  }
354  if ( !is_array( $info ) ) {
355  throw new InvalidArgumentException(
356  'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
357  );
358  }
359 
360  // Attach module
361  $this->moduleInfos[$name] = $info;
362  }
363  }
364 
369  public function registerTestModules(): void {
370  global $IP;
371 
372  if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
373  throw new MWException( 'Attempt to register JavaScript test modules '
374  . 'but <code>$wgEnableJavaScriptTest</code> is false. '
375  . 'Edit your <code>LocalSettings.php</code> to enable it.' );
376  }
377 
378  // This has a 'qunit' key for compat with the below hook.
379  $testModulesMeta = [ 'qunit' => [] ];
380 
381  $this->hookRunner->onResourceLoaderTestModules( $testModulesMeta, $this );
382  $extRegistry = ExtensionRegistry::getInstance();
383  // In case of conflict, the deprecated hook has precedence.
384  $testModules = $testModulesMeta['qunit']
385  + $extRegistry->getAttribute( 'QUnitTestModules' );
386 
388  foreach ( $testModules as $name => &$module ) {
389  // Turn any single-module dependency into an array
390  if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
391  $module['dependencies'] = [ $module['dependencies'] ];
392  }
393 
394  // Ensure the testrunner loads before any test suites
395  $module['dependencies'][] = 'mediawiki.qunit-testrunner';
396 
397  // Keep track of the test suites to load on SpecialJavaScriptTest
398  $testSuiteModuleNames[] = $name;
399  }
400 
401  // Core test suites (their names have further precedence).
402  $testModules = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules;
403  $testSuiteModuleNames[] = 'test.MediaWiki';
404 
405  $this->register( $testModules );
406  $this->testSuiteModuleNames = $testSuiteModuleNames;
407  }
408 
419  public function addSource( $sources, $loadUrl = null ) {
420  if ( !is_array( $sources ) ) {
421  $sources = [ $sources => $loadUrl ];
422  }
423  foreach ( $sources as $id => $source ) {
424  // Disallow duplicates
425  if ( isset( $this->sources[$id] ) ) {
426  throw new RuntimeException( 'Cannot register source ' . $id . ' twice' );
427  }
428 
429  // Support: MediaWiki 1.24 and earlier
430  if ( is_array( $source ) ) {
431  if ( !isset( $source['loadScript'] ) ) {
432  throw new InvalidArgumentException( 'Each source must have a "loadScript" key' );
433  }
434  $source = $source['loadScript'];
435  }
436 
437  $this->sources[$id] = $source;
438  }
439  }
440 
444  public function getModuleNames() {
445  return array_keys( $this->moduleInfos );
446  }
447 
455  public function getTestSuiteModuleNames() {
457  }
458 
466  public function isModuleRegistered( $name ) {
467  return isset( $this->moduleInfos[$name] );
468  }
469 
481  public function getModule( $name ) {
482  if ( !isset( $this->modules[$name] ) ) {
483  if ( !isset( $this->moduleInfos[$name] ) ) {
484  // No such module
485  return null;
486  }
487  // Construct the requested module object
488  $info = $this->moduleInfos[$name];
489  if ( isset( $info['factory'] ) ) {
491  $object = call_user_func( $info['factory'], $info );
492  } else {
493  $class = $info['class'] ?? ResourceLoaderFileModule::class;
495  $object = new $class( $info );
496  }
497  $object->setConfig( $this->getConfig() );
498  $object->setLogger( $this->logger );
499  $object->setHookContainer( $this->hookContainer );
500  $object->setName( $name );
501  $object->setDependencyAccessCallbacks(
502  [ $this, 'loadModuleDependenciesInternal' ],
503  [ $this, 'saveModuleDependenciesInternal' ]
504  );
505  $object->setSkinStylesOverride( $this->moduleSkinStyles );
506  $this->modules[$name] = $object;
507  }
508 
509  return $this->modules[$name];
510  }
511 
518  public function loadModuleDependenciesInternal( $moduleName, $variant ) {
519  $deps = $this->depStore->retrieve( self::RL_DEP_STORE_PREFIX, "$moduleName|$variant" );
520 
521  return ResourceLoaderModule::expandRelativePaths( $deps['paths'] );
522  }
523 
531  public function saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths ) {
532  $hasPendingUpdate = (bool)$this->depStoreUpdateBuffer;
533  $entity = "$moduleName|$variant";
534 
535  if ( array_diff( $paths, $priorPaths ) || array_diff( $priorPaths, $paths ) ) {
536  // Dependency store needs to be updated with the new path list
537  if ( $paths ) {
538  $deps = $this->depStore->newEntityDependencies( $paths, time() );
539  $this->depStoreUpdateBuffer[$entity] = $deps;
540  } else {
541  $this->depStoreUpdateBuffer[$entity] = null;
542  }
543  } elseif ( $priorPaths ) {
544  // Dependency store needs to store the existing path list for longer
545  $this->depStoreUpdateBuffer[$entity] = '*';
546  }
547 
548  // Use a DeferrableUpdate to flush the buffered dependency updates...
549  if ( !$hasPendingUpdate ) {
551  $updatesByEntity = $this->depStoreUpdateBuffer;
552  $this->depStoreUpdateBuffer = []; // consume
554 
555  $scopeLocks = [];
556  $depsByEntity = [];
557  $entitiesUnreg = [];
558  $entitiesRenew = [];
559  foreach ( $updatesByEntity as $entity => $update ) {
560  $lockKey = $cache->makeKey( 'rl-deps', $entity );
561  $scopeLocks[$entity] = $cache->getScopedLock( $lockKey, 0 );
562  if ( !$scopeLocks[$entity] ) {
563  // avoid duplicate write request slams (T124649)
564  // the lock must be specific to the current wiki (T247028)
565  continue;
566  }
567  if ( $update === null ) {
568  $entitiesUnreg[] = $entity;
569  } elseif ( $update === '*' ) {
570  $entitiesRenew[] = $entity;
571  } else {
572  $depsByEntity[$entity] = $update;
573  }
574  }
575 
576  $ttl = self::RL_MODULE_DEP_TTL;
577  $this->depStore->storeMulti( self::RL_DEP_STORE_PREFIX, $depsByEntity, $ttl );
578  $this->depStore->remove( self::RL_DEP_STORE_PREFIX, $entitiesUnreg );
579  $this->depStore->renew( self::RL_DEP_STORE_PREFIX, $entitiesRenew, $ttl );
580  } );
581  }
582  }
583 
589  public function getSources() {
590  return $this->sources;
591  }
592 
601  public function getLoadScript( $source ) {
602  if ( !isset( $this->sources[$source] ) ) {
603  throw new UnexpectedValueException( "Unknown source '$source'" );
604  }
605  return $this->sources[$source];
606  }
607 
611  public const HASH_LENGTH = 5;
612 
675  public static function makeHash( $value ) {
676  $hash = hash( 'fnv132', $value );
677  // The base_convert will pad it (if too short),
678  // then substr() will trim it (if too long).
679  return substr(
680  Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
681  0,
682  self::HASH_LENGTH
683  );
684  }
685 
695  public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
697  $this->logger->warning(
698  $msg,
699  $context + [ 'exception' => $e ]
700  );
701  $this->errors[] = self::formatExceptionNoComment( $e );
702  }
703 
712  public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
713  if ( !$moduleNames ) {
714  return '';
715  }
716  $hashes = array_map( function ( $module ) use ( $context ) {
717  try {
718  return $this->getModule( $module )->getVersionHash( $context );
719  } catch ( Exception $e ) {
720  // If modules fail to compute a version, don't fail the request (T152266)
721  // and still compute versions of other modules.
722  $this->outputErrorAndLog( $e,
723  'Calculating version for "{module}" failed: {exception}',
724  [
725  'module' => $module,
726  ]
727  );
728  return '';
729  }
730  }, $moduleNames );
731  return self::makeHash( implode( '', $hashes ) );
732  }
733 
748  public function makeVersionQuery( ResourceLoaderContext $context, array $modules ) {
749  // As of MediaWiki 1.28, the server and client use the same algorithm for combining
750  // version hashes. There is no technical reason for this to be same, and for years the
751  // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
752  // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
753  // query parameter), then this method must continue to match the JS one.
754  $filtered = [];
755  foreach ( $modules as $name ) {
756  if ( !$this->getModule( $name ) ) {
757  // If a versioned request contains a missing module, the version is a mismatch
758  // as the client considered a module (and version) we don't have.
759  return '';
760  }
761  $filtered[] = $name;
762  }
763  return $this->getCombinedVersion( $context, $filtered );
764  }
765 
771  public function respond( ResourceLoaderContext $context ) {
772  // Buffer output to catch warnings. Normally we'd use ob_clean() on the
773  // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
774  // is used: ob_clean() will clear the GZIP header in that case and it won't come
775  // back for subsequent output, resulting in invalid GZIP. So we have to wrap
776  // the whole thing in our own output buffer to be sure the active buffer
777  // doesn't use ob_gzhandler.
778  // See https://bugs.php.net/bug.php?id=36514
779  ob_start();
780 
781  $this->measureResponseTime( RequestContext::getMain()->getTiming() );
782 
783  // Find out which modules are missing and instantiate the others
784  $modules = [];
785  $missing = [];
786  foreach ( $context->getModules() as $name ) {
787  $module = $this->getModule( $name );
788  if ( $module ) {
789  // Do not allow private modules to be loaded from the web.
790  // This is a security issue, see T36907.
791  if ( $module->getGroup() === 'private' ) {
792  // Not a serious error, just means something is trying to access it (T101806)
793  $this->logger->debug( "Request for private module '$name' denied" );
794  $this->errors[] = "Cannot build private module \"$name\"";
795  continue;
796  }
797  $modules[$name] = $module;
798  } else {
799  $missing[] = $name;
800  }
801  }
802 
803  try {
804  // Preload for getCombinedVersion() and for batch makeModuleResponse()
805  $this->preloadModuleInfo( array_keys( $modules ), $context );
806  } catch ( Exception $e ) {
807  $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
808  }
809 
810  // Combine versions to propagate cache invalidation
811  $versionHash = '';
812  try {
813  $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
814  } catch ( Exception $e ) {
815  $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
816  }
817 
818  // See RFC 2616 § 3.11 Entity Tags
819  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
820  $etag = 'W/"' . $versionHash . '"';
821 
822  // Try the client-side cache first
823  if ( $this->tryRespondNotModified( $context, $etag ) ) {
824  return; // output handled (buffers cleared)
825  }
826 
827  // Use file cache if enabled and available...
828  if ( $this->config->get( 'UseFileCache' ) ) {
829  $fileCache = ResourceFileCache::newFromContext( $context );
830  if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
831  return; // output handled
832  }
833  } else {
834  $fileCache = null;
835  }
836 
837  // Generate a response
838  $response = $this->makeModuleResponse( $context, $modules, $missing );
839 
840  // Capture any PHP warnings from the output buffer and append them to the
841  // error list if we're in debug mode.
842  if ( $context->getDebug() ) {
843  $warnings = ob_get_contents();
844  if ( strlen( $warnings ) ) {
845  $this->errors[] = $warnings;
846  }
847  }
848 
849  // Consider saving the response to file cache (unless there are errors).
850  if ( $fileCache &&
851  !$this->errors &&
852  $missing === [] &&
854  ) {
855  if ( $fileCache->isCacheWorthy() ) {
856  // There were enough hits, save the response to the cache
857  $fileCache->saveText( $response );
858  } else {
859  $fileCache->incrMissesRecent( $context->getRequest() );
860  }
861  }
862 
863  $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
864 
865  // Remove the output buffer and output the response
866  ob_end_clean();
867 
868  if ( $context->getImageObj() && $this->errors ) {
869  // We can't show both the error messages and the response when it's an image.
870  $response = implode( "\n\n", $this->errors );
871  } elseif ( $this->errors ) {
872  $errorText = implode( "\n\n", $this->errors );
873  $errorResponse = self::makeComment( $errorText );
874  if ( $context->shouldIncludeScripts() ) {
875  $errorResponse .= 'if (window.console && console.error) { console.error('
876  . $context->encodeJson( $errorText )
877  . "); }\n";
878  }
879 
880  // Prepend error info to the response
881  $response = $errorResponse . $response;
882  }
883 
884  $this->errors = [];
885  // @phan-suppress-next-line SecurityCheck-XSS
886  echo $response;
887  }
888 
889  protected function measureResponseTime( Timing $timing ) {
890  DeferredUpdates::addCallableUpdate( static function () use ( $timing ) {
891  $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
892  if ( $measure !== false ) {
893  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
894  $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
895  }
896  } );
897  }
898 
909  protected function sendResponseHeaders(
910  ResourceLoaderContext $context, $etag, $errors, array $extra = []
911  ): void {
912  HeaderCallback::warnIfHeadersSent();
913  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
914  // Use a short cache expiry so that updates propagate to clients quickly, if:
915  // - No version specified (shared resources, e.g. stylesheets)
916  // - There were errors (recover quickly)
917  // - Version mismatch (T117587, T47877)
918  if ( $context->getVersion() === null
919  || $errors
920  || $context->getVersion() !== $this->makeVersionQuery( $context, $context->getModules() )
921  ) {
922  $maxage = $rlMaxage['unversioned'];
923  // If a version was specified we can use a longer expiry time since changing
924  // version numbers causes cache misses
925  } else {
926  $maxage = $rlMaxage['versioned'];
927  }
928  if ( $context->getImageObj() ) {
929  // Output different headers if we're outputting textual errors.
930  if ( $errors ) {
931  header( 'Content-Type: text/plain; charset=utf-8' );
932  } else {
933  $context->getImageObj()->sendResponseHeaders( $context );
934  }
935  } elseif ( $context->getOnly() === 'styles' ) {
936  header( 'Content-Type: text/css; charset=utf-8' );
937  header( 'Access-Control-Allow-Origin: *' );
938  } else {
939  header( 'Content-Type: text/javascript; charset=utf-8' );
940  }
941  // See RFC 2616 § 14.19 ETag
942  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
943  header( 'ETag: ' . $etag );
944  if ( $context->getDebug() ) {
945  // Do not cache debug responses
946  header( 'Cache-Control: private, no-cache, must-revalidate' );
947  header( 'Pragma: no-cache' );
948  } else {
949  header( "Cache-Control: public, max-age=$maxage, s-maxage=$maxage" );
950  header( 'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
951  }
952  foreach ( $extra as $header ) {
953  header( $header );
954  }
955  }
956 
967  protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
968  // See RFC 2616 § 14.26 If-None-Match
969  // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
970  $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
971  // Never send 304s in debug mode
972  if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
973  // There's another bug in ob_gzhandler (see also the comment at
974  // the top of this function) that causes it to gzip even empty
975  // responses, meaning it's impossible to produce a truly empty
976  // response (because the gzip header is always there). This is
977  // a problem because 304 responses have to be completely empty
978  // per the HTTP spec, and Firefox behaves buggily when they're not.
979  // See also https://bugs.php.net/bug.php?id=51579
980  // To work around this, we tear down all output buffering before
981  // sending the 304.
982  wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
983 
984  HttpStatus::header( 304 );
985 
986  $this->sendResponseHeaders( $context, $etag, false );
987  return true;
988  }
989  return false;
990  }
991 
1000  protected function tryRespondFromFileCache(
1001  ResourceFileCache $fileCache,
1002  ResourceLoaderContext $context,
1003  $etag
1004  ) {
1005  $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
1006  // Buffer output to catch warnings.
1007  ob_start();
1008  // Get the maximum age the cache can be
1009  $maxage = $context->getVersion() === null
1010  ? $rlMaxage['unversioned']
1011  : $rlMaxage['versioned'];
1012  // Minimum timestamp the cache file must have
1013  $minTime = time() - $maxage;
1014  $good = $fileCache->isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1015  if ( !$good ) {
1016  try { // RL always hits the DB on file cache miss...
1017  wfGetDB( DB_REPLICA );
1018  } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
1019  $good = $fileCache->isCacheGood(); // cache existence check
1020  }
1021  }
1022  if ( $good ) {
1023  $ts = $fileCache->cacheTimestamp();
1024  // Send content type and cache headers
1025  $this->sendResponseHeaders( $context, $etag, false );
1026  $response = $fileCache->fetchText();
1027  // Capture any PHP warnings from the output buffer and append them to the
1028  // response in a comment if we're in debug mode.
1029  if ( $context->getDebug() ) {
1030  $warnings = ob_get_contents();
1031  if ( strlen( $warnings ) ) {
1032  $response = self::makeComment( $warnings ) . $response;
1033  }
1034  }
1035  // Remove the output buffer and output the response
1036  ob_end_clean();
1037  echo $response . "\n/* Cached {$ts} */";
1038  return true; // cache hit
1039  }
1040  // Clear buffer
1041  ob_end_clean();
1042 
1043  return false; // cache miss
1044  }
1045 
1054  public static function makeComment( $text ) {
1055  $encText = str_replace( '*/', '* /', $text );
1056  return "/*\n$encText\n*/\n";
1057  }
1058 
1065  public static function formatException( Throwable $e ) {
1066  return self::makeComment( self::formatExceptionNoComment( $e ) );
1067  }
1068 
1076  protected static function formatExceptionNoComment( Throwable $e ) {
1077  global $wgShowExceptionDetails;
1078 
1079  if ( !$wgShowExceptionDetails ) {
1081  }
1082 
1083  return MWExceptionHandler::getLogMessage( $e ) .
1084  "\nBacktrace:\n" .
1086  }
1087 
1099  public function makeModuleResponse( ResourceLoaderContext $context,
1100  array $modules, array $missing = []
1101  ) {
1102  $out = '';
1103  $states = [];
1104 
1105  if ( $modules === [] && $missing === [] ) {
1106  return <<<MESSAGE
1107 /* This file is the Web entry point for MediaWiki's ResourceLoader:
1108  <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1109  no modules were requested. Max made me put this here. */
1110 MESSAGE;
1111  }
1112 
1113  $image = $context->getImageObj();
1114  if ( $image ) {
1115  $data = $image->getImageData( $context );
1116  if ( $data === false ) {
1117  $data = '';
1118  $this->errors[] = 'Image generation failed';
1119  }
1120  return $data;
1121  }
1122 
1123  foreach ( $missing as $name ) {
1124  $states[$name] = 'missing';
1125  }
1126 
1127  $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js';
1128 
1129  foreach ( $modules as $name => $module ) {
1130  try {
1131  $content = $module->getModuleContent( $context );
1132  $implementKey = $name . '@' . $module->getVersionHash( $context );
1133  $strContent = '';
1134 
1135  if ( isset( $content['headers'] ) ) {
1136  $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1137  }
1138 
1139  // Append output
1140  switch ( $context->getOnly() ) {
1141  case 'scripts':
1142  $scripts = $content['scripts'];
1143  if ( is_string( $scripts ) ) {
1144  // Load scripts raw...
1145  $strContent = $scripts;
1146  } elseif ( is_array( $scripts ) ) {
1147  // ...except when $scripts is an array of URLs or an associative array
1148  $strContent = self::makeLoaderImplementScript(
1149  $context,
1150  $implementKey,
1151  $scripts,
1152  [],
1153  [],
1154  []
1155  );
1156  }
1157  break;
1158  case 'styles':
1159  $styles = $content['styles'];
1160  // We no longer separate into media, they are all combined now with
1161  // custom media type groups into @media .. {} sections as part of the css string.
1162  // Module returns either an empty array or a numerical array with css strings.
1163  $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1164  break;
1165  default:
1166  $scripts = $content['scripts'] ?? '';
1167  if ( is_string( $scripts ) ) {
1168  if ( $name === 'site' || $name === 'user' ) {
1169  // Legacy scripts that run in the global scope without a closure.
1170  // mw.loader.implement will use globalEval if scripts is a string.
1171  // Minify manually here, because general response minification is
1172  // not effective due it being a string literal, not a function.
1173  if ( !$context->getDebug() ) {
1174  $scripts = self::filter( 'minify-js', $scripts ); // T107377
1175  }
1176  } else {
1177  $scripts = new XmlJsCode( $scripts );
1178  }
1179  }
1180  $strContent = self::makeLoaderImplementScript(
1181  $context,
1182  $implementKey,
1183  $scripts,
1184  $content['styles'] ?? [],
1185  // @phan-suppress-next-line SecurityCheck-XSS
1186  isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1187  $content['templates'] ?? []
1188  );
1189  break;
1190  }
1191 
1192  if ( !$context->getDebug() ) {
1193  $strContent = self::filter( $filter, $strContent );
1194  } else {
1195  // In debug mode, separate each response by a new line.
1196  // For example, between 'mw.loader.implement();' statements.
1197  $strContent = self::ensureNewline( $strContent );
1198  }
1199 
1200  if ( $context->getOnly() === 'scripts' ) {
1201  // Use a linebreak between module scripts (T162719)
1202  $out .= self::ensureNewline( $strContent );
1203  } else {
1204  $out .= $strContent;
1205  }
1206 
1207  } catch ( Exception $e ) {
1208  $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1209 
1210  // Respond to client with error-state instead of module implementation
1211  $states[$name] = 'error';
1212  unset( $modules[$name] );
1213  }
1214  }
1215 
1216  // Update module states
1217  if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1218  if ( $modules && $context->getOnly() === 'scripts' ) {
1219  // Set the state of modules loaded as only scripts to ready as
1220  // they don't have an mw.loader.implement wrapper that sets the state
1221  foreach ( $modules as $name => $module ) {
1222  $states[$name] = 'ready';
1223  }
1224  }
1225 
1226  // Set the state of modules we didn't respond to with mw.loader.implement
1227  if ( $states ) {
1228  $stateScript = self::makeLoaderStateScript( $context, $states );
1229  if ( !$context->getDebug() ) {
1230  $stateScript = self::filter( 'minify-js', $stateScript );
1231  }
1232  // Use a linebreak between module script and state script (T162719)
1233  $out = self::ensureNewline( $out ) . $stateScript;
1234  }
1235  } elseif ( $states ) {
1236  $this->errors[] = 'Problematic modules: '
1237  . $context->encodeJson( $states );
1238  }
1239 
1240  return $out;
1241  }
1242 
1249  public static function ensureNewline( $str ) {
1250  $end = substr( $str, -1 );
1251  if ( $end === false || $end === '' || $end === "\n" ) {
1252  return $str;
1253  }
1254  return $str . "\n";
1255  }
1256 
1263  public function getModulesByMessage( $messageKey ) {
1264  $moduleNames = [];
1265  foreach ( $this->getModuleNames() as $moduleName ) {
1266  $module = $this->getModule( $moduleName );
1267  if ( in_array( $messageKey, $module->getMessages() ) ) {
1268  $moduleNames[] = $moduleName;
1269  }
1270  }
1271  return $moduleNames;
1272  }
1273 
1291  private static function makeLoaderImplementScript(
1292  ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates
1293  ) {
1294  if ( $scripts instanceof XmlJsCode ) {
1295  if ( $scripts->value === '' ) {
1296  $scripts = null;
1297  } elseif ( $context->getDebug() ) {
1298  // @phan-suppress-next-line SecurityCheck-XSS
1299  $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1300  } else {
1301  // @phan-suppress-next-line SecurityCheck-XSS
1302  $scripts = new XmlJsCode(
1303  'function($,jQuery,require,module){' . self::ensureNewline( $scripts->value ) . '}'
1304  );
1305  }
1306  } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1307  $files = $scripts['files'];
1308  foreach ( $files as $path => &$file ) {
1309  // $file is changed (by reference) from a descriptor array to the content of the file
1310  // All of these essentially do $file = $file['content'];, some just have wrapping around it
1311  if ( $file['type'] === 'script' ) {
1312  // Ensure that the script has a newline at the end to close any comment in the
1313  // last line.
1314  $content = self::ensureNewline( $file['content'] );
1315  // Multi-file modules only get two parameters ($ and jQuery are being phased out)
1316  if ( $context->getDebug() ) {
1317  $file = new XmlJsCode( "function ( require, module ) {\n$content}" );
1318  } else {
1319  $file = new XmlJsCode( 'function(require,module){' . $content . '}' );
1320  }
1321  } else {
1322  $file = $file['content'];
1323  }
1324  }
1325  $scripts = XmlJsCode::encodeObject( [
1326  'main' => $scripts['main'],
1327  'files' => XmlJsCode::encodeObject( $files, $context->getDebug() )
1328  ], $context->getDebug() );
1329  } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1330  throw new InvalidArgumentException( 'Script must be a string or an array of URLs' );
1331  }
1332 
1333  // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1334  // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1335  // of "{}". Force them to objects.
1336  $module = [
1337  $name,
1338  $scripts,
1339  (object)$styles,
1340  (object)$messages,
1341  (object)$templates
1342  ];
1343  self::trimArray( $module );
1344 
1345  return Xml::encodeJsCall( 'mw.loader.implement', $module, $context->getDebug() );
1346  }
1347 
1354  public static function makeMessageSetScript( $messages ) {
1355  return 'mw.messages.set('
1356  . self::encodeJsonForScript( (object)$messages )
1357  . ');';
1358  }
1359 
1367  public static function makeCombinedStyles( array $stylePairs ) {
1368  $out = [];
1369  foreach ( $stylePairs as $media => $styles ) {
1370  // ResourceLoaderFileModule::getStyle can return the styles
1371  // as a string or an array of strings. This is to allow separation in
1372  // the front-end.
1373  $styles = (array)$styles;
1374  foreach ( $styles as $style ) {
1375  $style = trim( $style );
1376  // Don't output an empty "@media print { }" block (T42498)
1377  if ( $style !== '' ) {
1378  // Transform the media type based on request params and config
1379  // The way that this relies on $wgRequest to propagate request params is slightly evil
1380  $media = OutputPage::transformCssMedia( $media );
1381 
1382  if ( $media === '' || $media == 'all' ) {
1383  $out[] = $style;
1384  } elseif ( is_string( $media ) ) {
1385  $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1386  }
1387  // else: skip
1388  }
1389  }
1390  }
1391  return $out;
1392  }
1393 
1403  public static function encodeJsonForScript( $data ) {
1404  // Keep output as small as possible by disabling needless escape modes
1405  // that PHP uses by default.
1406  // However, while most module scripts are only served on HTTP responses
1407  // for JavaScript, some modules can also be embedded in the HTML as inline
1408  // scripts. This, and the fact that we sometimes need to export strings
1409  // containing user-generated content and labels that may genuinely contain
1410  // a sequences like "</script>", we need to encode either '/' or '<'.
1411  // By default PHP escapes '/'. Let's escape '<' instead which is less common
1412  // and allows URLs to mostly remain readable.
1413  $jsonFlags = JSON_UNESCAPED_SLASHES |
1414  JSON_UNESCAPED_UNICODE |
1415  JSON_HEX_TAG |
1416  JSON_HEX_AMP;
1417  if ( self::inDebugMode() ) {
1418  $jsonFlags |= JSON_PRETTY_PRINT;
1419  }
1420  return json_encode( $data, $jsonFlags );
1421  }
1422 
1435  public static function makeLoaderStateScript(
1436  ResourceLoaderContext $context, array $states
1437  ) {
1438  return 'mw.loader.state('
1439  . $context->encodeJson( $states )
1440  . ');';
1441  }
1442 
1443  private static function isEmptyObject( stdClass $obj ) {
1444  foreach ( $obj as $key => $value ) {
1445  return false;
1446  }
1447  return true;
1448  }
1449 
1463  private static function trimArray( array &$array ): void {
1464  $i = count( $array );
1465  while ( $i-- ) {
1466  if ( $array[$i] === null
1467  || $array[$i] === []
1468  || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1469  || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1470  ) {
1471  unset( $array[$i] );
1472  } else {
1473  break;
1474  }
1475  }
1476  }
1477 
1503  public static function makeLoaderRegisterScript(
1504  ResourceLoaderContext $context, array $modules
1505  ) {
1506  // Optimisation: Transform dependency names into indexes when possible
1507  // to produce smaller output. They are expanded by mw.loader.register on
1508  // the other end using resolveIndexedDependencies().
1509  $index = [];
1510  foreach ( $modules as $i => &$module ) {
1511  // Build module name index
1512  $index[$module[0]] = $i;
1513  }
1514  foreach ( $modules as &$module ) {
1515  if ( isset( $module[2] ) ) {
1516  foreach ( $module[2] as &$dependency ) {
1517  if ( isset( $index[$dependency] ) ) {
1518  // Replace module name in dependency list with index
1519  $dependency = $index[$dependency];
1520  }
1521  }
1522  }
1523  }
1524 
1525  array_walk( $modules, [ self::class, 'trimArray' ] );
1526 
1527  return 'mw.loader.register('
1528  . $context->encodeJson( $modules )
1529  . ');';
1530  }
1531 
1545  public static function makeLoaderSourcesScript(
1546  ResourceLoaderContext $context, array $sources
1547  ) {
1548  return 'mw.loader.addSource('
1549  . $context->encodeJson( $sources )
1550  . ');';
1551  }
1552 
1559  public static function makeLoaderConditionalScript( $script ) {
1560  // Adds a function to lazy-created RLQ
1561  return '(RLQ=window.RLQ||[]).push(function(){' .
1562  trim( $script ) . '});';
1563  }
1564 
1573  public static function makeInlineCodeWithModule( $modules, $script ) {
1574  // Adds an array to lazy-created RLQ
1575  return '(RLQ=window.RLQ||[]).push(['
1577  . 'function(){' . trim( $script ) . '}'
1578  . ']);';
1579  }
1580 
1592  public static function makeInlineScript( $script, $nonce = null ) {
1593  $js = self::makeLoaderConditionalScript( $script );
1594  $escNonce = '';
1595  if ( $nonce === null ) {
1596  wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1597  } elseif ( $nonce !== false ) {
1598  // If it was false, CSP is disabled, so no nonce attribute.
1599  // Nonce should be only base64 characters, so should be safe,
1600  // but better to be safely escaped than sorry.
1601  $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1602  }
1603 
1604  return new WrappedString(
1605  Html::inlineScript( $js, $nonce ),
1606  "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1607  '});</script>'
1608  );
1609  }
1610 
1619  public static function makeConfigSetScript( array $configuration ) {
1620  $json = self::encodeJsonForScript( $configuration );
1621  if ( $json === false ) {
1622  $e = new Exception(
1623  'JSON serialization of config data failed. ' .
1624  'This usually means the config data is not valid UTF-8.'
1625  );
1627  return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1628  }
1629  return "mw.config.set($json);";
1630  }
1631 
1645  public static function makePackedModulesString( array $modules ) {
1646  $moduleMap = []; // [ prefix => [ suffixes ] ]
1647  foreach ( $modules as $module ) {
1648  $pos = strrpos( $module, '.' );
1649  $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1650  $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1651  $moduleMap[$prefix][] = $suffix;
1652  }
1653 
1654  $arr = [];
1655  foreach ( $moduleMap as $prefix => $suffixes ) {
1656  $p = $prefix === '' ? '' : $prefix . '.';
1657  $arr[] = $p . implode( ',', $suffixes );
1658  }
1659  return implode( '|', $arr );
1660  }
1661 
1673  public static function expandModuleNames( $modules ) {
1674  $retval = [];
1675  $exploded = explode( '|', $modules );
1676  foreach ( $exploded as $group ) {
1677  if ( strpos( $group, ',' ) === false ) {
1678  // This is not a set of modules in foo.bar,baz notation
1679  // but a single module
1680  $retval[] = $group;
1681  } else {
1682  // This is a set of modules in foo.bar,baz notation
1683  $pos = strrpos( $group, '.' );
1684  if ( $pos === false ) {
1685  // Prefixless modules, i.e. without dots
1686  $retval = array_merge( $retval, explode( ',', $group ) );
1687  } else {
1688  // We have a prefix and a bunch of suffixes
1689  $prefix = substr( $group, 0, $pos ); // 'foo'
1690  $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1691  foreach ( $suffixes as $suffix ) {
1692  $retval[] = "$prefix.$suffix";
1693  }
1694  }
1695  }
1696  }
1697  return $retval;
1698  }
1699 
1710  public static function inDebugMode() {
1711  if ( self::$debugMode === null ) {
1713  $str = $wgRequest->getRawVal( 'debug',
1714  $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ? 'true' : '' )
1715  );
1716  self::$debugMode = ResourceLoaderContext::debugFromString( $str );
1717  }
1718  return self::$debugMode;
1719  }
1720 
1731  public static function clearCache() {
1732  self::$debugMode = null;
1733  }
1734 
1744  public function createLoaderURL( $source, ResourceLoaderContext $context,
1745  array $extraQuery = []
1746  ) {
1747  $query = self::createLoaderQuery( $context, $extraQuery );
1748  $script = $this->getLoadScript( $source );
1749 
1750  return wfAppendQuery( $script, $query );
1751  }
1752 
1762  protected static function createLoaderQuery(
1763  ResourceLoaderContext $context, array $extraQuery = []
1764  ) {
1765  return self::makeLoaderQuery(
1766  $context->getModules(),
1767  $context->getLanguage(),
1768  $context->getSkin(),
1769  $context->getUser(),
1770  $context->getVersion(),
1771  $context->getDebug(),
1772  $context->getOnly(),
1773  $context->getRequest()->getBool( 'printable' ),
1774  $context->getRequest()->getBool( 'handheld' ),
1775  $extraQuery
1776  );
1777  }
1778 
1795  public static function makeLoaderQuery( array $modules, $lang, $skin, $user = null,
1796  $version = null, $debug = ResourceLoaderContext::DEBUG_OFF, $only = null,
1797  $printable = false, $handheld = false, array $extraQuery = []
1798  ) {
1799  $query = [
1800  'modules' => self::makePackedModulesString( $modules ),
1801  ];
1802  // Keep urls short by omitting query parameters that
1803  // match the defaults assumed by ResourceLoaderContext.
1804  // Note: This relies on the defaults either being insignificant or forever constant,
1805  // as otherwise cached urls could change in meaning when the defaults change.
1807  $query['lang'] = $lang;
1808  }
1809  if ( $skin !== ResourceLoaderContext::DEFAULT_SKIN ) {
1810  $query['skin'] = $skin;
1811  }
1813  $query['debug'] = strval( $debug );
1814  }
1815  if ( $user !== null ) {
1816  $query['user'] = $user;
1817  }
1818  if ( $version !== null ) {
1819  $query['version'] = $version;
1820  }
1821  if ( $only !== null ) {
1822  $query['only'] = $only;
1823  }
1824  if ( $printable ) {
1825  $query['printable'] = 1;
1826  }
1827  if ( $handheld ) {
1828  $query['handheld'] = 1;
1829  }
1830  $query += $extraQuery;
1831 
1832  // Make queries uniform in order
1833  ksort( $query );
1834  return $query;
1835  }
1836 
1846  public static function isValidModuleName( $moduleName ) {
1847  $len = strlen( $moduleName );
1848  return $len <= 255 && strcspn( $moduleName, '!,|', 0, $len ) === $len;
1849  }
1850 
1862  public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1863  global $IP;
1864  // When called from the installer, it is possible that a required PHP extension
1865  // is missing (at least for now; see T49564). If this is the case, throw an
1866  // exception (caught by the installer) to prevent a fatal error later on.
1867  if ( !class_exists( Less_Parser::class ) ) {
1868  throw new MWException( 'MediaWiki requires the less.php parser' );
1869  }
1870 
1871  $importDirs[] = "$IP/resources/src/mediawiki.less";
1872 
1873  $parser = new Less_Parser;
1874  $parser->ModifyVars( $vars );
1875  // SetImportDirs expects an array like [ 'path1' => '', 'path2' => '' ]
1876  $parser->SetImportDirs( array_fill_keys( $importDirs, '' ) );
1877  $parser->SetOption( 'relativeUrls', false );
1878 
1879  return $parser;
1880  }
1881 
1895  public function expandUrl( string $base, string $url ): string {
1896  // Net_URL2::resolve() doesn't allow protocol-relative URLs, but we do.
1897  $isProtoRelative = strpos( $base, '//' ) === 0;
1898  if ( $isProtoRelative ) {
1899  $base = "https:$base";
1900  }
1901  // Net_URL2::resolve() takes care of throwing if $base doesn't have a server.
1902  $baseUrl = new Net_URL2( $base );
1903  $ret = $baseUrl->resolve( $url );
1904  if ( $isProtoRelative ) {
1905  $ret->setScheme( false );
1906  }
1907  return $ret->getURL();
1908  }
1909 
1918  public static function getSiteConfigSettings(
1919  ResourceLoaderContext $context, Config $conf
1920  ): array {
1921  // Namespace related preparation
1922  // - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
1923  // - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
1924  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1925  $namespaceIds = $contLang->getNamespaceIds();
1926  $caseSensitiveNamespaces = [];
1927  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1928  foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
1929  $namespaceIds[$contLang->lc( $name )] = $index;
1930  if ( !$nsInfo->isCapitalized( $index ) ) {
1931  $caseSensitiveNamespaces[] = $index;
1932  }
1933  }
1934 
1935  $illegalFileChars = $conf->get( 'IllegalFileChars' );
1936 
1937  // Build list of variables
1938  $skin = $context->getSkin();
1939 
1940  // Start of supported and stable config vars (for use by extensions/gadgets).
1941  $vars = [
1942  'debug' => $context->getDebug(),
1943  'skin' => $skin,
1944  'stylepath' => $conf->get( 'StylePath' ),
1945  'wgArticlePath' => $conf->get( 'ArticlePath' ),
1946  'wgScriptPath' => $conf->get( 'ScriptPath' ),
1947  'wgScript' => $conf->get( 'Script' ),
1948  'wgSearchType' => $conf->get( 'SearchType' ),
1949  'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
1950  'wgServer' => $conf->get( 'Server' ),
1951  'wgServerName' => $conf->get( 'ServerName' ),
1952  'wgUserLanguage' => $context->getLanguage(),
1953  'wgContentLanguage' => $contLang->getCode(),
1954  'wgVersion' => MW_VERSION,
1955  'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
1956  'wgNamespaceIds' => $namespaceIds,
1957  'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
1958  'wgSiteName' => $conf->get( 'Sitename' ),
1959  'wgDBname' => $conf->get( 'DBname' ),
1960  'wgWikiID' => WikiMap::getCurrentWikiId(),
1961  'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
1962  'wgCommentByteLimit' => null,
1963  'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
1964  'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
1965  ];
1966  // End of stable config vars.
1967 
1968  // Internal variables for use by MediaWiki core and/or ResourceLoader.
1969  $vars += [
1970  // @internal For mediawiki.widgets
1971  'wgUrlProtocols' => wfUrlProtocols(),
1972  // @internal For mediawiki.page.watch
1973  // Force object to avoid "empty" associative array from
1974  // becoming [] instead of {} in JS (T36604)
1975  'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
1976  // @internal For mediawiki.language
1977  'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ),
1978  // @internal For mediawiki.Title
1979  'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
1980  // @internal For mediawiki.cookie
1981  'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
1982  'wgCookieDomain' => $conf->get( 'CookieDomain' ),
1983  'wgCookiePath' => $conf->get( 'CookiePath' ),
1984  'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
1985  // @internal For mediawiki.Title
1986  'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
1987  'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
1988  // @internal For mediawiki.ForeignUpload
1989  'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
1990  'wgEnableUploads' => $conf->get( 'EnableUploads' ),
1991  ];
1992 
1993  Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );
1994 
1995  return $vars;
1996  }
1997 }
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:182
ResourceLoader\makeLoaderConditionalScript
static makeLoaderConditionalScript( $script)
Wrap JavaScript code to run after the startup module.
Definition: ResourceLoader.php:1557
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:34
ResourceLoader\addSource
addSource( $sources, $loadUrl=null)
Add a foreign source of modules.
Definition: ResourceLoader.php:419
ResourceLoader\getMessageBlobStore
getMessageBlobStore()
Definition: ResourceLoader.php:297
wfResetOutputBuffers
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
Definition: GlobalFunctions.php:1603
ResourceLoader\sendResponseHeaders
sendResponseHeaders(ResourceLoaderContext $context, $etag, $errors, array $extra=[])
Send main response headers to the client.
Definition: ResourceLoader.php:909
ResourceLoader\$errors
array $errors
Errors accumulated during current respond() call.
Definition: ResourceLoader.php:90
ResourceLoader\getLessCompiler
getLessCompiler(array $vars=[], array $importDirs=[])
Return a LESS compiler that is set up for use with MediaWiki.
Definition: ResourceLoader.php:1860
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:1617
ResourceLoader\ensureNewline
static ensureNewline( $str)
Ensure the string is either empty or ends in a line break.
Definition: ResourceLoader.php:1247
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:273
ResourceLoader\$depStore
DependencyStore $depStore
Definition: ResourceLoader.php:64
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:95
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:1065
ResourceLoader\encodeJsonForScript
static encodeJsonForScript( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
Definition: ResourceLoader.php:1401
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
$wgRequest
$wgRequest
Definition: Setup.php:702
ResourceLoader\makeInlineScript
static makeInlineScript( $script, $nonce=null)
Make an HTML script that runs given JS code after startup and base modules.
Definition: ResourceLoader.php:1590
MWExceptionHandler\getPublicLogMessage
static getPublicLogMessage(Throwable $e)
Definition: MWExceptionHandler.php:535
ResourceLoaderContext\getModules
getModules()
Definition: ResourceLoaderContext.php:179
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:1916
ResourceLoader\getLogger
getLogger()
Definition: ResourceLoader.php:289
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:36
ResourceLoader\$hookContainer
HookContainer $hookContainer
Definition: ResourceLoader.php:70
ResourceLoader\getTestSuiteModuleNames
getTestSuiteModuleNames()
Get a list of module names with QUnit test suites.
Definition: ResourceLoader.php:455
ResourceLoader\$testSuiteModuleNames
string[] $testSuiteModuleNames
List of module names that contain QUnit test suites.
Definition: ResourceLoader.php:86
$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:1501
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:243
ResourceLoaderModule\getVary
static getVary(ResourceLoaderContext $context)
Get vary string.
Definition: ResourceLoaderModule.php:1068
ResourceLoaderContext\getOnly
getOnly()
Definition: ResourceLoaderContext.php:272
$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:751
ResourceLoader\isEmptyObject
static isEmptyObject(stdClass $obj)
Definition: ResourceLoader.php:1441
ResourceLoader\$extraHeaders
string[] $extraHeaders
Extra HTTP response headers from modules loaded in makeModuleResponse()
Definition: ResourceLoader.php:92
ResourceLoader\preloadModuleInfo
preloadModuleInfo(array $moduleNames, ResourceLoaderContext $context)
Load information stored in the database and dependency tracking store about modules.
Definition: ResourceLoader.php:122
OutputPage\transformCssMedia
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
Definition: OutputPage.php:4037
$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:1893
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:305
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:1729
ResourceLoaderContext\DEBUG_OFF
const DEBUG_OFF
Definition: ResourceLoaderContext.php:39
MWException
MediaWiki exception.
Definition: MWException.php:29
ResourceLoader\$moduleInfos
array[] $moduleInfos
Map of (module name => associative info array)
Definition: ResourceLoader.php:78
ResourceLoader\makeModuleResponse
makeModuleResponse(ResourceLoaderContext $context, array $modules, array $missing=[])
Generate code for a response.
Definition: ResourceLoader.php:1099
ResourceLoader\$debugMode
static int null $debugMode
Definition: ResourceLoader.php:103
ResourceLoaderContext\getRequest
getRequest()
Definition: ResourceLoaderContext.php:165
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
Timing
An interface to help developers measure the performance of their applications.
Definition: Timing.php:45
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2198
ResourceLoader\makeHash
static makeHash( $value)
Create a hash for module versioning purposes.
Definition: ResourceLoader.php:675
ResourceLoaderContext\getDebug
getDebug()
Definition: ResourceLoaderContext.php:265
Timing\measure
measure( $measureName, $startMark='requestStart', $endMark=null)
This method stores the duration between two marks along with the associated name (a "measure").
Definition: Timing.php:123
ResourceLoader\getSources
getSources()
Get the list of sources.
Definition: ResourceLoader.php:589
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:1433
ResourceLoader\makeInlineCodeWithModule
static makeInlineCodeWithModule( $modules, $script)
Wrap JavaScript code to run after a required module.
Definition: ResourceLoader.php:1571
ResourceLoader\makeComment
static makeComment( $text)
Generate a CSS or JS comment block.
Definition: ResourceLoader.php:1054
ResourceLoaderModule\expandRelativePaths
static expandRelativePaths(array $filePaths)
Expand directories relative to $IP.
Definition: ResourceLoaderModule.php:595
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:183
ResourceLoader\makeLoaderQuery
static makeLoaderQuery(array $modules, $lang, $skin, $user=null, $version=null, $debug=ResourceLoaderContext::DEBUG_OFF, $only=null, $printable=false, $handheld=false, array $extraQuery=[])
Build a query array (array representation of query string) for load.php.
Definition: ResourceLoader.php:1793
ResourceLoader\$logger
LoggerInterface $logger
Definition: ResourceLoader.php:67
ResourceLoaderContext\getVersion
getVersion()
Definition: ResourceLoaderContext.php:281
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:1643
ResourceLoader\setDependencyStore
setDependencyStore(DependencyStore $tracker)
Definition: ResourceLoader.php:313
ResourceLoader\getModulesByMessage
getModulesByMessage( $messageKey)
Get names of modules that use a certain message.
Definition: ResourceLoader.php:1261
ResourceLoader\$modules
ResourceLoaderModule[] $modules
Map of (module name => ResourceLoaderModule)
Definition: ResourceLoader.php:76
ResourceLoader\setModuleSkinStyles
setModuleSkinStyles(array $moduleSkinStyles)
Definition: ResourceLoader.php:321
wfUrlProtocols
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
Definition: GlobalFunctions.php:702
ResourceLoaderContext\DEFAULT_SKIN
const DEFAULT_SKIN
Definition: ResourceLoaderContext.php:36
ResourceLoader\getModule
getModule( $name)
Get the ResourceLoaderModule object for a given module name.
Definition: ResourceLoader.php:481
$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:601
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:1365
ResourceLoaderContext\DEFAULT_LANG
const DEFAULT_LANG
Definition: ResourceLoaderContext.php:35
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:222
ResourceLoader\$hookRunner
HookRunner $hookRunner
Definition: ResourceLoader.php:73
ResourceLoader\HASH_LENGTH
const HASH_LENGTH
Definition: ResourceLoader.php:611
ResourceLoader\$testModuleNames
array $testModuleNames
Associative array mapping framework ids to a list of names of test suite modules like [ 'qunit' => [ ...
Definition: ResourceLoader.php:84
ResourceLoader\isModuleRegistered
isModuleRegistered( $name)
Check whether a ResourceLoader module is registered.
Definition: ResourceLoader.php:466
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:1671
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:62
ResourceLoader\getCombinedVersion
getCombinedVersion(ResourceLoaderContext $context, array $moduleNames)
Helper method to get and combine versions of multiple modules.
Definition: ResourceLoader.php:712
ResourceLoaderContext\getSkin
getSkin()
Definition: ResourceLoaderContext.php:215
ResourceLoaderContext\shouldIncludeScripts
shouldIncludeScripts()
Definition: ResourceLoaderContext.php:361
ResourceLoader\respond
respond(ResourceLoaderContext $context)
Output a response to a load request, including the content-type header.
Definition: ResourceLoader.php:771
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:484
ResourceLoader\$config
Config $config
Definition: ResourceLoader.php:60
ResourceLoader\trimArray
static trimArray(array &$array)
Remove empty values from the end of an array.
Definition: ResourceLoader.php:1461
ResourceLoaderContext\getUser
getUser()
Definition: ResourceLoaderContext.php:222
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:58
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:1289
$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:1742
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:1708
ResourceLoader\tryRespondFromFileCache
tryRespondFromFileCache(ResourceFileCache $fileCache, ResourceLoaderContext $context, $etag)
Send out code for a response from file cache if possible.
Definition: ResourceLoader.php:1000
ResourceLoader\loadModuleDependenciesInternal
loadModuleDependenciesInternal( $moduleName, $variant)
Definition: ResourceLoader.php:518
$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:7415
ResourceLoader\$moduleSkinStyles
array $moduleSkinStyles
Styles that are skin-specific and supplement or replace the default skinStyles of a FileModule.
Definition: ResourceLoader.php:100
ResourceLoader\makeMessageSetScript
static makeMessageSetScript( $messages)
Returns JS code which, when called, will register a given list of messages.
Definition: ResourceLoader.php:1352
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\registerTestModules
registerTestModules()
Definition: ResourceLoader.php:369
ResourceLoader\makeLoaderSourcesScript
static makeLoaderSourcesScript(ResourceLoaderContext $context, array $sources)
Format JS code which calls mw.loader.addSource() with the given parameters.
Definition: ResourceLoader.php:1543
ResourceLoader\tryRespondNotModified
tryRespondNotModified(ResourceLoaderContext $context, $etag)
Respond with HTTP 304 Not Modified if appropiate.
Definition: ResourceLoader.php:967
$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:88
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:115
Title\legalChars
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:737
ResourceLoaderContext\getImageObj
getImageObj()
If this is a request for an image, get the ResourceLoaderImage object.
Definition: ResourceLoaderContext.php:316
ResourceLoader\formatExceptionNoComment
static formatExceptionNoComment(Throwable $e)
Handle exception display.
Definition: ResourceLoader.php:1076
ResourceLoader\getConfig
getConfig()
Definition: ResourceLoader.php:273
ResourceLoader\createLoaderQuery
static createLoaderQuery(ResourceLoaderContext $context, array $extraQuery=[])
Helper for createLoaderURL()
Definition: ResourceLoader.php:1760
ResourceLoader\saveModuleDependenciesInternal
saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths)
Definition: ResourceLoader.php:531
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:748
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:4447
ResourceLoaderContext\getRaw
getRaw()
Definition: ResourceLoaderContext.php:285
ResourceLoaderContext\encodeJson
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
Definition: ResourceLoaderContext.php:432
ResourceLoader\outputErrorAndLog
outputErrorAndLog(Exception $e, $msg, array $context=[])
Add an error to the 'errors' array and log it.
Definition: ResourceLoader.php:695
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:1844
ResourceLoader\getModuleNames
getModuleNames()
Definition: ResourceLoader.php:444
ResourceLoader\setLogger
setLogger(LoggerInterface $logger)
Definition: ResourceLoader.php:281
FileCacheBase\cacheTimestamp
cacheTimestamp()
Get the last-modified timestamp of the cache file.
Definition: FileCacheBase.php:105
ResourceLoader\measureResponseTime
measureResponseTime(Timing $timing)
Definition: ResourceLoader.php:889
ObjectCache\getLocalServerInstance
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
Definition: ObjectCache.php:255