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