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 // T132418: When a resource expires mid-way a browsing session, prefer to renew it in
939 // the background instead of blocking the next page load (eg. startup module, or CSS).
940 $staleDirective = ( $maxage > self::MAXAGE_RECOVER
941 ? ", stale-while-revalidate=" . min( 60, intval( $maxage / 2 ) )
942 : ''
943 );
944 header( "Cache-Control: public, max-age=$maxage, s-maxage=$maxage" . $staleDirective );
945 header( 'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
946 }
947 foreach ( $extra as $header ) {
948 header( $header );
949 }
950 }
951
962 protected function tryRespondNotModified( Context $context, $etag ) {
963 // See RFC 2616 § 14.26 If-None-Match
964 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
965 $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
966 // Never send 304s in debug mode
967 if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
968 // There's another bug in ob_gzhandler (see also the comment at
969 // the top of this function) that causes it to gzip even empty
970 // responses, meaning it's impossible to produce a truly empty
971 // response (because the gzip header is always there). This is
972 // a problem because 304 responses have to be completely empty
973 // per the HTTP spec, and Firefox behaves buggily when they're not.
974 // See also https://bugs.php.net/bug.php?id=51579
975 // To work around this, we tear down all output buffering before
976 // sending the 304.
977 wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
978
979 HttpStatus::header( 304 );
980
981 $this->sendResponseHeaders( $context, $etag, false );
982 return true;
983 }
984 return false;
985 }
986
995 protected function tryRespondFromFileCache(
996 ResourceFileCache $fileCache,
997 Context $context,
998 $etag
999 ) {
1000 // Buffer output to catch warnings.
1001 ob_start();
1002 // Get the maximum age the cache can be
1003 $maxage = $context->getVersion() === null
1004 ? $this->maxageUnversioned
1005 : $this->maxageVersioned;
1006 // Minimum timestamp the cache file must have
1007 $minTime = time() - $maxage;
1008 $good = $fileCache->isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1009 if ( !$good ) {
1010 try { // RL always hits the DB on file cache miss...
1012 } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
1013 $good = $fileCache->isCacheGood(); // cache existence check
1014 }
1015 }
1016 if ( $good ) {
1017 $ts = $fileCache->cacheTimestamp();
1018 // Send content type and cache headers
1019 $this->sendResponseHeaders( $context, $etag, false );
1020 $response = $fileCache->fetchText();
1021 // Capture any PHP warnings from the output buffer and append them to the
1022 // response in a comment if we're in debug mode.
1023 if ( $context->getDebug() ) {
1024 $warnings = ob_get_contents();
1025 if ( strlen( $warnings ) ) {
1026 $response = self::makeComment( $warnings ) . $response;
1027 }
1028 }
1029 // Remove the output buffer and output the response
1030 ob_end_clean();
1031 echo $response . "\n/* Cached {$ts} */";
1032 return true; // cache hit
1033 }
1034 // Clear buffer
1035 ob_end_clean();
1036
1037 return false; // cache miss
1038 }
1039
1048 public static function makeComment( $text ) {
1049 $encText = str_replace( '*/', '* /', $text );
1050 return "/*\n$encText\n*/\n";
1051 }
1052
1059 public static function formatException( Throwable $e ) {
1060 return self::makeComment( self::formatExceptionNoComment( $e ) );
1061 }
1062
1070 protected static function formatExceptionNoComment( Throwable $e ) {
1071 if ( !MWExceptionRenderer::shouldShowExceptionDetails() ) {
1072 return MWExceptionHandler::getPublicLogMessage( $e );
1073 }
1074
1075 return MWExceptionHandler::getLogMessage( $e ) .
1076 "\nBacktrace:\n" .
1077 MWExceptionHandler::getRedactedTraceAsString( $e );
1078 }
1079
1091 public function makeModuleResponse( Context $context,
1092 array $modules, array $missing = []
1093 ) {
1094 if ( $modules === [] && $missing === [] ) {
1095 return <<<MESSAGE
1096/* This file is the Web entry point for MediaWiki's ResourceLoader:
1097 <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1098 no modules were requested. Max made me put this here. */
1099MESSAGE;
1100 }
1101
1102 $image = $context->getImageObj();
1103 if ( $image ) {
1104 $data = $image->getImageData( $context );
1105 if ( $data === false ) {
1106 $data = '';
1107 $this->errors[] = 'Image generation failed';
1108 }
1109 return $data;
1110 }
1111
1112 $states = [];
1113 foreach ( $missing as $name ) {
1114 $states[$name] = 'missing';
1115 }
1116
1117 $only = $context->getOnly();
1118 $filter = $only === 'styles' ? 'minify-css' : 'minify-js';
1119 $debug = (bool)$context->getDebug();
1120
1121 $out = '';
1122 foreach ( $modules as $name => $module ) {
1123 try {
1124 $content = $module->getModuleContent( $context );
1125 $implementKey = $name . '@' . $module->getVersionHash( $context );
1126 $strContent = '';
1127
1128 if ( isset( $content['headers'] ) ) {
1129 $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1130 }
1131
1132 // Append output
1133 switch ( $only ) {
1134 case 'scripts':
1135 $scripts = $content['scripts'];
1136 if ( is_string( $scripts ) ) {
1137 // Load scripts raw...
1138 $strContent = $scripts;
1139 } elseif ( is_array( $scripts ) ) {
1140 // ...except when $scripts is an array of URLs or an associative array
1141 $strContent = self::makeLoaderImplementScript(
1142 $context,
1143 $implementKey,
1144 $scripts,
1145 [],
1146 [],
1147 []
1148 );
1149 }
1150 break;
1151 case 'styles':
1152 $styles = $content['styles'];
1153 // We no longer separate into media, they are all combined now with
1154 // custom media type groups into @media .. {} sections as part of the css string.
1155 // Module returns either an empty array or a numerical array with css strings.
1156 $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1157 break;
1158 default:
1159 $scripts = $content['scripts'] ?? '';
1160 if ( is_string( $scripts ) ) {
1161 if ( $name === 'site' || $name === 'user' ) {
1162 // Legacy scripts that run in the global scope without a closure.
1163 // mw.loader.implement will use eval if scripts is a string.
1164 // Minify manually here, because general response minification is
1165 // not effective due it being a string literal, not a function.
1166 if ( !$debug ) {
1167 $scripts = self::filter( 'minify-js', $scripts ); // T107377
1168 }
1169 } else {
1170 $scripts = new XmlJsCode( $scripts );
1171 }
1172 }
1173 $strContent = self::makeLoaderImplementScript(
1174 $context,
1175 $implementKey,
1176 $scripts,
1177 $content['styles'] ?? [],
1178 isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1179 $content['templates'] ?? []
1180 );
1181 break;
1182 }
1183
1184 if ( $debug ) {
1185 // In debug mode, separate each response by a new line.
1186 // For example, between 'mw.loader.implement();' statements.
1187 $strContent = self::ensureNewline( $strContent );
1188 } else {
1189 $strContent = self::filter( $filter, $strContent, [
1190 // Important: Do not cache minifications of embedded modules
1191 // This is especially for the private 'user.options' module,
1192 // which varies on every pageview and would explode the cache (T84960)
1193 'cache' => !$module->shouldEmbedModule( $context )
1194 ] );
1195 }
1196
1197 if ( $only === 'scripts' ) {
1198 // Use a linebreak between module scripts (T162719)
1199 $out .= self::ensureNewline( $strContent );
1200 } else {
1201 $out .= $strContent;
1202 }
1203 } catch ( TimeoutException $e ) {
1204 throw $e;
1205 } catch ( Exception $e ) {
1206 $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1207
1208 // Respond to client with error-state instead of module implementation
1209 $states[$name] = 'error';
1210 unset( $modules[$name] );
1211 }
1212 }
1213
1214 // Update module states
1215 if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1216 if ( $modules && $only === 'scripts' ) {
1217 // Set the state of modules loaded as only scripts to ready as
1218 // they don't have an mw.loader.implement wrapper that sets the state
1219 foreach ( $modules as $name => $module ) {
1220 $states[$name] = 'ready';
1221 }
1222 }
1223
1224 // Set the state of modules we didn't respond to with mw.loader.implement
1225 if ( $states ) {
1226 $stateScript = self::makeLoaderStateScript( $context, $states );
1227 if ( !$debug ) {
1228 $stateScript = self::filter( 'minify-js', $stateScript );
1229 }
1230 // Use a linebreak between module script and state script (T162719)
1231 $out = self::ensureNewline( $out ) . $stateScript;
1232 }
1233 } elseif ( $states ) {
1234 $this->errors[] = 'Problematic modules: '
1235 . $context->encodeJson( $states );
1236 }
1237
1238 return $out;
1239 }
1240
1247 public static function ensureNewline( $str ) {
1248 $end = substr( $str, -1 );
1249 if ( $end === false || $end === '' || $end === "\n" ) {
1250 return $str;
1251 }
1252 return $str . "\n";
1253 }
1254
1261 public function getModulesByMessage( $messageKey ) {
1262 $moduleNames = [];
1263 foreach ( $this->getModuleNames() as $moduleName ) {
1264 $module = $this->getModule( $moduleName );
1265 if ( in_array( $messageKey, $module->getMessages() ) ) {
1266 $moduleNames[] = $moduleName;
1267 }
1268 }
1269 return $moduleNames;
1270 }
1271
1289 private static function makeLoaderImplementScript(
1290 Context $context, $name, $scripts, $styles, $messages, $templates
1291 ) {
1292 if ( $scripts instanceof XmlJsCode ) {
1293 if ( $scripts->value === '' ) {
1294 $scripts = null;
1295 } else {
1296 $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1297 }
1298 } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1299 $files = $scripts['files'];
1300 foreach ( $files as $path => &$file ) {
1301 // $file is changed (by reference) from a descriptor array to the content of the file
1302 // All of these essentially do $file = $file['content'];, some just have wrapping around it
1303 if ( $file['type'] === 'script' ) {
1304 // Ensure that the script has a newline at the end to close any comment in the
1305 // last line.
1306 $content = self::ensureNewline( $file['content'] );
1307 // Provide CJS `exports` (in addition to CJS2 `module.exports`) to package modules (T284511).
1308 // $/jQuery are simply used as globals instead.
1309 // TODO: Remove $/jQuery param from traditional module closure too (and bump caching)
1310 $file = new XmlJsCode( "function ( require, module, exports ) {\n$content}" );
1311 } else {
1312 $file = $file['content'];
1313 }
1314 }
1315 $scripts = XmlJsCode::encodeObject( [
1316 'main' => $scripts['main'],
1317 'files' => XmlJsCode::encodeObject( $files, true )
1318 ], true );
1319 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1320 throw new InvalidArgumentException( 'Script must be a string or an array of URLs' );
1321 }
1322
1323 // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1324 // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1325 // of "{}". Force them to objects.
1326 $module = [
1327 $name,
1328 $scripts,
1329 (object)$styles,
1330 (object)$messages,
1331 (object)$templates
1332 ];
1333 self::trimArray( $module );
1334
1335 // We use pretty output unconditionally to make this method simpler.
1336 // Minification is taken care of closer to the output.
1337 return Xml::encodeJsCall( 'mw.loader.implement', $module, true );
1338 }
1339
1346 public static function makeMessageSetScript( $messages ) {
1347 return 'mw.messages.set('
1348 . self::encodeJsonForScript( (object)$messages )
1349 . ');';
1350 }
1351
1359 public static function makeCombinedStyles( array $stylePairs ) {
1360 $out = [];
1361 foreach ( $stylePairs as $media => $styles ) {
1362 // FileModule::getStyle can return the styles as a string or an
1363 // array of strings. This is to allow separation in the front-end.
1364 $styles = (array)$styles;
1365 foreach ( $styles as $style ) {
1366 $style = trim( $style );
1367 // Don't output an empty "@media print { }" block (T42498)
1368 if ( $style === '' ) {
1369 continue;
1370 }
1371 // Transform the media type based on request params and config
1372 // The way that this relies on $wgRequest to propagate request params is slightly evil
1373 $media = OutputPage::transformCssMedia( $media );
1374
1375 if ( $media === '' || $media == 'all' ) {
1376 $out[] = $style;
1377 } elseif ( is_string( $media ) ) {
1378 $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1379 }
1380 // else: skip
1381 }
1382 }
1383 return $out;
1384 }
1385
1395 public static function encodeJsonForScript( $data ) {
1396 // Keep output as small as possible by disabling needless escape modes
1397 // that PHP uses by default.
1398 // However, while most module scripts are only served on HTTP responses
1399 // for JavaScript, some modules can also be embedded in the HTML as inline
1400 // scripts. This, and the fact that we sometimes need to export strings
1401 // containing user-generated content and labels that may genuinely contain
1402 // a sequences like "</script>", we need to encode either '/' or '<'.
1403 // By default PHP escapes '/'. Let's escape '<' instead which is less common
1404 // and allows URLs to mostly remain readable.
1405 $jsonFlags = JSON_UNESCAPED_SLASHES |
1406 JSON_UNESCAPED_UNICODE |
1407 JSON_HEX_TAG |
1408 JSON_HEX_AMP;
1409 if ( self::inDebugMode() ) {
1410 $jsonFlags |= JSON_PRETTY_PRINT;
1411 }
1412 return json_encode( $data, $jsonFlags );
1413 }
1414
1427 public static function makeLoaderStateScript(
1428 Context $context, array $states
1429 ) {
1430 return 'mw.loader.state('
1431 . $context->encodeJson( $states )
1432 . ');';
1433 }
1434
1435 private static function isEmptyObject( stdClass $obj ) {
1436 foreach ( $obj as $key => $value ) {
1437 return false;
1438 }
1439 return true;
1440 }
1441
1455 private static function trimArray( array &$array ): void {
1456 $i = count( $array );
1457 while ( $i-- ) {
1458 if ( $array[$i] === null
1459 || $array[$i] === []
1460 || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1461 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1462 ) {
1463 unset( $array[$i] );
1464 } else {
1465 break;
1466 }
1467 }
1468 }
1469
1495 public static function makeLoaderRegisterScript(
1496 Context $context, array $modules
1497 ) {
1498 // Optimisation: Transform dependency names into indexes when possible
1499 // to produce smaller output. They are expanded by mw.loader.register on
1500 // the other end.
1501 $index = [];
1502 foreach ( $modules as $i => &$module ) {
1503 // Build module name index
1504 $index[$module[0]] = $i;
1505 }
1506 foreach ( $modules as &$module ) {
1507 if ( isset( $module[2] ) ) {
1508 foreach ( $module[2] as &$dependency ) {
1509 if ( isset( $index[$dependency] ) ) {
1510 // Replace module name in dependency list with index
1511 $dependency = $index[$dependency];
1512 }
1513 }
1514 }
1515 }
1516
1517 array_walk( $modules, [ self::class, 'trimArray' ] );
1518
1519 return 'mw.loader.register('
1520 . $context->encodeJson( $modules )
1521 . ');';
1522 }
1523
1537 public static function makeLoaderSourcesScript(
1538 Context $context, array $sources
1539 ) {
1540 return 'mw.loader.addSource('
1541 . $context->encodeJson( $sources )
1542 . ');';
1543 }
1544
1551 public static function makeLoaderConditionalScript( $script ) {
1552 // Adds a function to lazy-created RLQ
1553 return '(RLQ=window.RLQ||[]).push(function(){' .
1554 trim( $script ) . '});';
1555 }
1556
1565 public static function makeInlineCodeWithModule( $modules, $script ) {
1566 // Adds an array to lazy-created RLQ
1567 return '(RLQ=window.RLQ||[]).push(['
1568 . self::encodeJsonForScript( $modules ) . ','
1569 . 'function(){' . trim( $script ) . '}'
1570 . ']);';
1571 }
1572
1584 public static function makeInlineScript( $script, $nonce = null ) {
1585 $js = self::makeLoaderConditionalScript( $script );
1586 $escNonce = '';
1587 if ( $nonce === null ) {
1588 wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1589 } elseif ( $nonce !== false ) {
1590 // If it was false, CSP is disabled, so no nonce attribute.
1591 // Nonce should be only base64 characters, so should be safe,
1592 // but better to be safely escaped than sorry.
1593 $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1594 }
1595
1596 return new WrappedString(
1597 Html::inlineScript( $js, $nonce ),
1598 "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1599 '});</script>'
1600 );
1601 }
1602
1611 public static function makeConfigSetScript( array $configuration ) {
1612 $json = self::encodeJsonForScript( $configuration );
1613 if ( $json === false ) {
1614 $e = new Exception(
1615 'JSON serialization of config data failed. ' .
1616 'This usually means the config data is not valid UTF-8.'
1617 );
1619 return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1620 }
1621 return "mw.config.set($json);";
1622 }
1623
1637 public static function makePackedModulesString( array $modules ) {
1638 $moduleMap = []; // [ prefix => [ suffixes ] ]
1639 foreach ( $modules as $module ) {
1640 $pos = strrpos( $module, '.' );
1641 $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1642 $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1643 $moduleMap[$prefix][] = $suffix;
1644 }
1645
1646 $arr = [];
1647 foreach ( $moduleMap as $prefix => $suffixes ) {
1648 $p = $prefix === '' ? '' : $prefix . '.';
1649 $arr[] = $p . implode( ',', $suffixes );
1650 }
1651 return implode( '|', $arr );
1652 }
1653
1665 public static function expandModuleNames( $modules ) {
1666 $retval = [];
1667 $exploded = explode( '|', $modules );
1668 foreach ( $exploded as $group ) {
1669 if ( strpos( $group, ',' ) === false ) {
1670 // This is not a set of modules in foo.bar,baz notation
1671 // but a single module
1672 $retval[] = $group;
1673 continue;
1674 }
1675 // This is a set of modules in foo.bar,baz notation
1676 $pos = strrpos( $group, '.' );
1677 if ( $pos === false ) {
1678 // Prefixless modules, i.e. without dots
1679 $retval = array_merge( $retval, explode( ',', $group ) );
1680 continue;
1681 }
1682 // We have a prefix and a bunch of suffixes
1683 $prefix = substr( $group, 0, $pos ); // 'foo'
1684 $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1685 foreach ( $suffixes as $suffix ) {
1686 $retval[] = "$prefix.$suffix";
1687 }
1688 }
1689 return $retval;
1690 }
1691
1702 public static function inDebugMode() {
1703 if ( self::$debugMode === null ) {
1704 global $wgRequest;
1705 $resourceLoaderDebug = MediaWikiServices::getInstance()->getMainConfig()->get(
1706 MainConfigNames::ResourceLoaderDebug );
1707 $str = $wgRequest->getRawVal( 'debug',
1708 $wgRequest->getCookie( 'resourceLoaderDebug', '', $resourceLoaderDebug ? 'true' : '' )
1709 );
1710 self::$debugMode = Context::debugFromString( $str );
1711 }
1712 return self::$debugMode;
1713 }
1714
1725 public static function clearCache() {
1726 self::$debugMode = null;
1727 }
1728
1738 public function createLoaderURL( $source, Context $context,
1739 array $extraQuery = []
1740 ) {
1741 $query = self::createLoaderQuery( $context, $extraQuery );
1742 $script = $this->getLoadScript( $source );
1743
1744 return wfAppendQuery( $script, $query );
1745 }
1746
1756 protected static function createLoaderQuery(
1757 Context $context, array $extraQuery = []
1758 ) {
1759 return self::makeLoaderQuery(
1760 $context->getModules(),
1761 $context->getLanguage(),
1762 $context->getSkin(),
1763 $context->getUser(),
1764 $context->getVersion(),
1765 $context->getDebug(),
1766 $context->getOnly(),
1767 $context->getRequest()->getBool( 'printable' ),
1768 null,
1769 $extraQuery
1770 );
1771 }
1772
1789 public static function makeLoaderQuery( array $modules, $lang, $skin, $user = null,
1790 $version = null, $debug = Context::DEBUG_OFF, $only = null,
1791 $printable = false, $handheld = null, array $extraQuery = []
1792 ) {
1793 $query = [
1794 'modules' => self::makePackedModulesString( $modules ),
1795 ];
1796 // Keep urls short by omitting query parameters that
1797 // match the defaults assumed by Context.
1798 // Note: This relies on the defaults either being insignificant or forever constant,
1799 // as otherwise cached urls could change in meaning when the defaults change.
1800 if ( $lang !== Context::DEFAULT_LANG ) {
1801 $query['lang'] = $lang;
1802 }
1803 if ( $skin !== Context::DEFAULT_SKIN ) {
1804 $query['skin'] = $skin;
1805 }
1806 if ( $debug !== Context::DEBUG_OFF ) {
1807 $query['debug'] = strval( $debug );
1808 }
1809 if ( $user !== null ) {
1810 $query['user'] = $user;
1811 }
1812 if ( $version !== null ) {
1813 $query['version'] = $version;
1814 }
1815 if ( $only !== null ) {
1816 $query['only'] = $only;
1817 }
1818 if ( $printable ) {
1819 $query['printable'] = 1;
1820 }
1821 $query += $extraQuery;
1822
1823 // Make queries uniform in order
1824 ksort( $query );
1825 return $query;
1826 }
1827
1837 public static function isValidModuleName( $moduleName ) {
1838 $len = strlen( $moduleName );
1839 return $len <= 255 && strcspn( $moduleName, '!,|', 0, $len ) === $len;
1840 }
1841
1853 public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1854 global $IP;
1855 // When called from the installer, it is possible that a required PHP extension
1856 // is missing (at least for now; see T49564). If this is the case, throw an
1857 // exception (caught by the installer) to prevent a fatal error later on.
1858 if ( !class_exists( Less_Parser::class ) ) {
1859 throw new MWException( 'MediaWiki requires the less.php parser' );
1860 }
1861
1862 $importDirs[] = "$IP/resources/src/mediawiki.less";
1863
1864 $parser = new Less_Parser;
1865 $parser->ModifyVars( $vars );
1866 // SetImportDirs expects an array like [ 'path1' => '', 'path2' => '' ]
1867 $parser->SetImportDirs( array_fill_keys( $importDirs, '' ) );
1868 $parser->SetOption( 'relativeUrls', false );
1869
1870 return $parser;
1871 }
1872
1886 public function expandUrl( string $base, string $url ): string {
1887 // Net_URL2::resolve() doesn't allow protocol-relative URLs, but we do.
1888 $isProtoRelative = strpos( $base, '//' ) === 0;
1889 if ( $isProtoRelative ) {
1890 $base = "https:$base";
1891 }
1892 // Net_URL2::resolve() takes care of throwing if $base doesn't have a server.
1893 $baseUrl = new Net_URL2( $base );
1894 $ret = $baseUrl->resolve( $url );
1895 if ( $isProtoRelative ) {
1896 $ret->setScheme( false );
1897 }
1898 return $ret->getURL();
1899 }
1900
1918 public static function filter( $filter, $data, array $options = [] ) {
1919 if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
1920 return $data;
1921 }
1922
1923 if ( isset( $options['cache'] ) && $options['cache'] === false ) {
1924 return self::applyFilter( $filter, $data ) ?? $data;
1925 }
1926
1927 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1929
1930 $key = $cache->makeGlobalKey(
1931 'resourceloader-filter',
1932 $filter,
1933 self::CACHE_VERSION,
1934 md5( $data )
1935 );
1936
1937 $incKey = "resourceloader_cache.$filter.hit";
1938 $result = $cache->getWithSetCallback(
1939 $key,
1940 BagOStuff::TTL_DAY,
1941 function () use ( $filter, $data, &$incKey ) {
1942 $incKey = "resourceloader_cache.$filter.miss";
1943 return self::applyFilter( $filter, $data );
1944 }
1945 );
1946 $stats->increment( $incKey );
1947 if ( $result === null ) {
1948 // Cached failure
1949 $result = $data;
1950 }
1951
1952 return $result;
1953 }
1954
1960 private static function applyFilter( $filter, $data ) {
1961 $data = trim( $data );
1962 if ( $data ) {
1963 try {
1964 $data = ( $filter === 'minify-css' )
1965 ? CSSMin::minify( $data )
1966 : JavaScriptMinifier::minify( $data );
1967 } catch ( TimeoutException $e ) {
1968 throw $e;
1969 } catch ( Exception $e ) {
1971 return null;
1972 }
1973 }
1974 return $data;
1975 }
1976
1988 public static function getUserDefaults(
1989 Context $context,
1990 HookContainer $hookContainer,
1991 UserOptionsLookup $userOptionsLookup
1992 ): array {
1993 $defaultOptions = $userOptionsLookup->getDefaultOptions();
1994 $keysToExclude = [];
1995 $hookRunner = new HookRunner( $hookContainer );
1996 $hookRunner->onResourceLoaderExcludeUserOptions( $keysToExclude, $context );
1997 foreach ( $keysToExclude as $excludedKey ) {
1998 unset( $defaultOptions[ $excludedKey ] );
1999 }
2000 return $defaultOptions;
2001 }
2002
2011 public static function getSiteConfigSettings(
2012 Context $context, Config $conf
2013 ): array {
2014 // Namespace related preparation
2015 // - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
2016 // - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
2017 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2018 $namespaceIds = $contLang->getNamespaceIds();
2019 $caseSensitiveNamespaces = [];
2020 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2021 foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
2022 $namespaceIds[$contLang->lc( $name )] = $index;
2023 if ( !$nsInfo->isCapitalized( $index ) ) {
2024 $caseSensitiveNamespaces[] = $index;
2025 }
2026 }
2027
2028 $illegalFileChars = $conf->get( MainConfigNames::IllegalFileChars );
2029
2030 // Build list of variables
2031 $skin = $context->getSkin();
2032
2033 // Start of supported and stable config vars (for use by extensions/gadgets).
2034 $vars = [
2035 'debug' => $context->getDebug(),
2036 'skin' => $skin,
2037 'stylepath' => $conf->get( MainConfigNames::StylePath ),
2038 'wgArticlePath' => $conf->get( MainConfigNames::ArticlePath ),
2039 'wgScriptPath' => $conf->get( MainConfigNames::ScriptPath ),
2040 'wgScript' => $conf->get( MainConfigNames::Script ),
2041 'wgSearchType' => $conf->get( MainConfigNames::SearchType ),
2042 'wgVariantArticlePath' => $conf->get( MainConfigNames::VariantArticlePath ),
2043 'wgServer' => $conf->get( MainConfigNames::Server ),
2044 'wgServerName' => $conf->get( MainConfigNames::ServerName ),
2045 'wgUserLanguage' => $context->getLanguage(),
2046 'wgContentLanguage' => $contLang->getCode(),
2047 'wgVersion' => MW_VERSION,
2048 'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
2049 'wgNamespaceIds' => $namespaceIds,
2050 'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
2051 'wgSiteName' => $conf->get( MainConfigNames::Sitename ),
2052 'wgDBname' => $conf->get( MainConfigNames::DBname ),
2053 'wgWikiID' => WikiMap::getCurrentWikiId(),
2054 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
2055 'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
2056 'wgExtensionAssetsPath' => $conf->get( MainConfigNames::ExtensionAssetsPath ),
2057 ];
2058 // End of stable config vars.
2059
2060 // Internal variables for use by MediaWiki core and/or ResourceLoader.
2061 $vars += [
2062 // @internal For mediawiki.widgets
2063 'wgUrlProtocols' => wfUrlProtocols(),
2064 // @internal For mediawiki.page.watch
2065 // Force object to avoid "empty" associative array from
2066 // becoming [] instead of {} in JS (T36604)
2067 'wgActionPaths' => (object)$conf->get( MainConfigNames::ActionPaths ),
2068 // @internal For mediawiki.language
2069 'wgTranslateNumerals' => $conf->get( MainConfigNames::TranslateNumerals ),
2070 // @internal For mediawiki.Title
2071 'wgExtraSignatureNamespaces' => $conf->get( MainConfigNames::ExtraSignatureNamespaces ),
2072 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
2073 'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
2074 ];
2075
2076 Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );
2077
2078 return $vars;
2079 }
2080}
2081
2082class_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