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