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