MediaWiki REL1_35
ResourceLoader.php
Go to the documentation of this file.
1<?php
27use Psr\Log\LoggerAwareInterface;
28use Psr\Log\LoggerInterface;
29use Psr\Log\NullLogger;
33use Wikimedia\Timestamp\ConvertibleTimestamp;
34use Wikimedia\WrappedString;
35
56class ResourceLoader implements LoggerAwareInterface {
58 protected $config;
60 protected $blobStore;
62 protected $depStore;
63
65 private $logger;
66
69
71 private $hookRunner;
72
74 protected $modules = [];
76 protected $moduleInfos = [];
82 protected $testModuleNames = [];
84 protected $testSuiteModuleNames = [];
86 protected $sources = [];
88 protected $errors = [];
90 protected $extraHeaders = [];
91
94
98 private $moduleSkinStyles = [];
99
101 protected static $debugMode = null;
102
104 public const CACHE_VERSION = 8;
105
107 private const RL_DEP_STORE_PREFIX = 'ResourceLoaderModule';
109 private const RL_MODULE_DEP_TTL = BagOStuff::TTL_WEEK;
110
112 public const FILTER_NOMIN = '/*@nomin*/';
113
120 public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
121 // Load all tracked indirect file dependencies for the modules
122 $vary = ResourceLoaderModule::getVary( $context );
123 $entitiesByModule = [];
124 foreach ( $moduleNames as $moduleName ) {
125 $entitiesByModule[$moduleName] = "$moduleName|$vary";
126 }
127 $depsByEntity = $this->depStore->retrieveMulti(
128 self::RL_DEP_STORE_PREFIX,
129 $entitiesByModule
130 );
131 // Inject the indirect file dependencies for all the modules
132 foreach ( $moduleNames as $moduleName ) {
133 $module = $this->getModule( $moduleName );
134 if ( $module ) {
135 $entity = $entitiesByModule[$moduleName];
136 $deps = $depsByEntity[$entity];
137 $paths = ResourceLoaderModule::expandRelativePaths( $deps['paths'] );
138 $module->setFileDependencies( $context, $paths );
139 }
140 }
141
142 // Batched version of ResourceLoaderWikiModule::getTitleInfo
144 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
145
146 // Prime in-object cache for message blobs for modules with messages
147 $modulesWithMessages = [];
148 foreach ( $moduleNames as $moduleName ) {
149 $module = $this->getModule( $moduleName );
150 if ( $module && $module->getMessages() ) {
151 $modulesWithMessages[$moduleName] = $module;
152 }
153 }
154 // Prime in-object cache for message blobs for modules with messages
155 $lang = $context->getLanguage();
156 $store = $this->getMessageBlobStore();
157 $blobs = $store->getBlobs( $modulesWithMessages, $lang );
158 foreach ( $blobs as $moduleName => $blob ) {
159 $modulesWithMessages[$moduleName]->setMessageBlob( $blob, $lang );
160 }
161 }
162
180 public static function filter( $filter, $data, array $options = [] ) {
181 if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
182 return $data;
183 }
184
185 if ( isset( $options['cache'] ) && $options['cache'] === false ) {
186 return self::applyFilter( $filter, $data ) ?? $data;
187 }
188
189 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
190 $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
191
192 $key = $cache->makeGlobalKey(
193 'resourceloader-filter',
194 $filter,
195 self::CACHE_VERSION,
196 md5( $data )
197 );
198
199 $result = $cache->get( $key );
200 if ( $result === false ) {
201 $stats->increment( "resourceloader_cache.$filter.miss" );
202 $result = self::applyFilter( $filter, $data );
203 $cache->set( $key, $result, 24 * 3600 );
204 } else {
205 $stats->increment( "resourceloader_cache.$filter.hit" );
206 }
207 if ( $result === null ) {
208 // Cached failure
209 $result = $data;
210 }
211
212 return $result;
213 }
214
220 private static function applyFilter( $filter, $data ) {
221 $data = trim( $data );
222 if ( $data ) {
223 try {
224 $data = ( $filter === 'minify-css' )
225 ? CSSMin::minify( $data )
227 } catch ( Exception $e ) {
228 MWExceptionHandler::logException( $e );
229 return null;
230 }
231 }
232 return $data;
233 }
234
241 public function __construct(
242 Config $config = null,
243 LoggerInterface $logger = null,
245 ) {
246 $this->logger = $logger ?: new NullLogger();
247 $services = MediaWikiServices::getInstance();
248
249 if ( !$config ) {
250 wfDeprecated( __METHOD__ . ' without a Config instance', '1.34' );
251 $config = $services->getMainConfig();
252 }
253 $this->config = $config;
254
255 $this->hookContainer = $services->getHookContainer();
256 $this->hookRunner = new HookRunner( $this->hookContainer );
257
258 // Add 'local' source first
259 $this->addSource( 'local', $config->get( 'LoadScript' ) );
260
261 // Special module that always exists
262 $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
263
264 $this->setMessageBlobStore(
265 new MessageBlobStore( $this, $this->logger, $services->getMainWANObjectCache() )
266 );
267
270 }
271
275 public function getConfig() {
276 return $this->config;
277 }
278
283 public function setLogger( LoggerInterface $logger ) {
284 $this->logger = $logger;
285 }
286
291 public function getLogger() {
292 return $this->logger;
293 }
294
299 public function getMessageBlobStore() {
300 return $this->blobStore;
301 }
302
308 $this->blobStore = $blobStore;
309 }
310
316 $this->depStore = $tracker;
317 }
318
323 public function setModuleSkinStyles( array $moduleSkinStyles ) {
324 $this->moduleSkinStyles = $moduleSkinStyles;
325 }
326
337 public function register( $name, array $info = null ) {
338 // Allow multiple modules to be registered in one call
339 $registrations = is_array( $name ) ? $name : [ $name => $info ];
340 foreach ( $registrations as $name => $info ) {
341 // Warn on duplicate registrations
342 if ( isset( $this->moduleInfos[$name] ) ) {
343 // A module has already been registered by this name
344 $this->logger->warning(
345 'ResourceLoader duplicate registration warning. ' .
346 'Another module has already been registered as ' . $name
347 );
348 }
349
350 // Check validity
351 if ( !self::isValidModuleName( $name ) ) {
352 throw new InvalidArgumentException( "ResourceLoader module name '$name' is invalid, "
353 . "see ResourceLoader::isValidModuleName()" );
354 }
355 if ( !is_array( $info ) ) {
356 throw new InvalidArgumentException(
357 'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
358 );
359 }
360
361 // Attach module
362 $this->moduleInfos[$name] = $info;
363
364 // Last-minute changes
365 // Apply custom skin-defined styles to existing modules.
366 if ( $this->isFileModule( $name ) ) {
367 foreach ( $this->moduleSkinStyles as $skinName => $skinStyles ) {
368 // If this module already defines skinStyles for this skin, ignore ResourceModuleSkinStyles.
369 if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
370 continue;
371 }
372
373 // If $name is preceded with a '+', the defined style files will be added to 'default'
374 // skinStyles, otherwise 'default' will be ignored as it normally would be.
375 if ( isset( $skinStyles[$name] ) ) {
376 $paths = (array)$skinStyles[$name];
377 $styleFiles = [];
378 } elseif ( isset( $skinStyles['+' . $name] ) ) {
379 $paths = (array)$skinStyles['+' . $name];
380 $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
381 (array)$this->moduleInfos[$name]['skinStyles']['default'] :
382 [];
383 } else {
384 continue;
385 }
386
387 // Add new file paths, remapping them to refer to our directories and not use settings
388 // from the module we're modifying, which come from the base definition.
389 list( $localBasePath, $remoteBasePath ) =
391
392 foreach ( $paths as $path ) {
393 $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
394 }
395
396 $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
397 }
398 }
399 }
400 }
401
406 public function registerTestModules() : void {
407 global $IP;
408
409 if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
410 throw new MWException( 'Attempt to register JavaScript test modules '
411 . 'but <code>$wgEnableJavaScriptTest</code> is false. '
412 . 'Edit your <code>LocalSettings.php</code> to enable it.' );
413 }
414
415 // This has a 'qunit' key for compat with the below hook.
416 $testModulesMeta = [ 'qunit' => [] ];
417
418 $this->hookRunner->onResourceLoaderTestModules( $testModulesMeta, $this );
419 $extRegistry = ExtensionRegistry::getInstance();
420 // In case of conflict, the deprecated hook has precedence.
421 $testModules = $testModulesMeta['qunit']
422 + $extRegistry->getAttribute( 'QUnitTestModules' );
423
425 foreach ( $testModules as $name => &$module ) {
426 // Turn any single-module dependency into an array
427 if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
428 $module['dependencies'] = [ $module['dependencies'] ];
429 }
430
431 // Ensure the testrunner loads before any test suites
432 $module['dependencies'][] = 'mediawiki.qunit-testrunner';
433
434 // Keep track of the test suites to load on SpecialJavaScriptTest
435 $testSuiteModuleNames[] = $name;
436 }
437
438 // Core test suites (their names have further precedence).
439 $testModules = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules;
440 $testSuiteModuleNames[] = 'test.MediaWiki';
441
442 $this->register( $testModules );
443 $this->testSuiteModuleNames = $testSuiteModuleNames;
444 }
445
456 public function addSource( $sources, $loadUrl = null ) {
457 if ( !is_array( $sources ) ) {
458 $sources = [ $sources => $loadUrl ];
459 }
460 foreach ( $sources as $id => $source ) {
461 // Disallow duplicates
462 if ( isset( $this->sources[$id] ) ) {
463 throw new RuntimeException( 'Cannot register source ' . $id . ' twice' );
464 }
465
466 // Support: MediaWiki 1.24 and earlier
467 if ( is_array( $source ) ) {
468 if ( !isset( $source['loadScript'] ) ) {
469 throw new InvalidArgumentException( 'Each source must have a "loadScript" key' );
470 }
471 $source = $source['loadScript'];
472 }
473
474 $this->sources[$id] = $source;
475 }
476 }
477
483 public function getModuleNames() {
484 return array_keys( $this->moduleInfos );
485 }
486
494 public function getTestSuiteModuleNames() {
495 return $this->testSuiteModuleNames;
496 }
497
505 public function isModuleRegistered( $name ) {
506 return isset( $this->moduleInfos[$name] );
507 }
508
520 public function getModule( $name ) {
521 if ( !isset( $this->modules[$name] ) ) {
522 if ( !isset( $this->moduleInfos[$name] ) ) {
523 // No such module
524 return null;
525 }
526 // Construct the requested module object
527 $info = $this->moduleInfos[$name];
528 if ( isset( $info['factory'] ) ) {
530 $object = call_user_func( $info['factory'], $info );
531 } else {
532 $class = $info['class'] ?? ResourceLoaderFileModule::class;
534 $object = new $class( $info );
535 }
536 $object->setConfig( $this->getConfig() );
537 $object->setLogger( $this->logger );
538 $object->setHookContainer( $this->hookContainer );
539 $object->setName( $name );
540 $object->setDependencyAccessCallbacks(
541 [ $this, 'loadModuleDependenciesInternal' ],
542 [ $this, 'saveModuleDependenciesInternal' ]
543 );
544 $this->modules[$name] = $object;
545 }
546
547 return $this->modules[$name];
548 }
549
556 public function loadModuleDependenciesInternal( $moduleName, $variant ) {
557 $deps = $this->depStore->retrieve( self::RL_DEP_STORE_PREFIX, "$moduleName|$variant" );
558
559 return ResourceLoaderModule::expandRelativePaths( $deps['paths'] );
560 }
561
569 public function saveModuleDependenciesInternal( $moduleName, $variant, $paths, $priorPaths ) {
570 $hasPendingUpdate = (bool)$this->depStoreUpdateBuffer;
571 $entity = "$moduleName|$variant";
572
573 if ( array_diff( $paths, $priorPaths ) || array_diff( $priorPaths, $paths ) ) {
574 // Dependency store needs to be updated with the new path list
575 if ( $paths ) {
576 $deps = $this->depStore->newEntityDependencies( $paths, time() );
577 $this->depStoreUpdateBuffer[$entity] = $deps;
578 } else {
579 $this->depStoreUpdateBuffer[$entity] = null;
580 }
581 } elseif ( $priorPaths ) {
582 // Dependency store needs to store the existing path list for longer
583 $this->depStoreUpdateBuffer[$entity] = '*';
584 }
585
586 // Use a DeferrableUpdate to flush the buffered dependency updates...
587 if ( !$hasPendingUpdate ) {
588 DeferredUpdates::addCallableUpdate( function () {
589 $updatesByEntity = $this->depStoreUpdateBuffer;
590 $this->depStoreUpdateBuffer = []; // consume
591 $cache = ObjectCache::getLocalClusterInstance();
592
593 $scopeLocks = [];
594 $depsByEntity = [];
595 $entitiesUnreg = [];
596 $entitiesRenew = [];
597 foreach ( $updatesByEntity as $entity => $update ) {
598 $lockKey = $cache->makeKey( 'rl-deps', $entity );
599 $scopeLocks[$entity] = $cache->getScopedLock( $lockKey, 0 );
600 if ( !$scopeLocks[$entity] ) {
601 // avoid duplicate write request slams (T124649)
602 // the lock must be specific to the current wiki (T247028)
603 continue;
604 } elseif ( $update === null ) {
605 $entitiesUnreg[] = $entity;
606 } elseif ( $update === '*' ) {
607 $entitiesRenew[] = $entity;
608 } else {
609 $depsByEntity[$entity] = $update;
610 }
611 }
612
613 $ttl = self::RL_MODULE_DEP_TTL;
614 $this->depStore->storeMulti( self::RL_DEP_STORE_PREFIX, $depsByEntity, $ttl );
615 $this->depStore->remove( self::RL_DEP_STORE_PREFIX, $entitiesUnreg );
616 $this->depStore->renew( self::RL_DEP_STORE_PREFIX, $entitiesRenew, $ttl );
617 } );
618 }
619 }
620
627 protected function isFileModule( $name ) {
628 if ( !isset( $this->moduleInfos[$name] ) ) {
629 return false;
630 }
631 $info = $this->moduleInfos[$name];
632 return !isset( $info['factory'] ) && (
633 // The implied default for 'class' is ResourceLoaderFileModule
634 !isset( $info['class'] ) ||
635 // Explicit default
636 $info['class'] === ResourceLoaderFileModule::class ||
637 is_subclass_of( $info['class'], ResourceLoaderFileModule::class )
638 );
639 }
640
646 public function getSources() {
647 return $this->sources;
648 }
649
658 public function getLoadScript( $source ) {
659 if ( !isset( $this->sources[$source] ) ) {
660 throw new UnexpectedValueException( "Unknown source '$source'" );
661 }
662 return $this->sources[$source];
663 }
664
668 public const HASH_LENGTH = 5;
669
732 public static function makeHash( $value ) {
733 $hash = hash( 'fnv132', $value );
734 // The base_convert will pad it (if too short),
735 // then substr() will trim it (if too long).
736 return substr(
737 Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
738 0,
739 self::HASH_LENGTH
740 );
741 }
742
752 public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
753 MWExceptionHandler::logException( $e );
754 $this->logger->warning(
755 $msg,
756 $context + [ 'exception' => $e ]
757 );
758 $this->errors[] = self::formatExceptionNoComment( $e );
759 }
760
769 public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
770 if ( !$moduleNames ) {
771 return '';
772 }
773 $hashes = array_map( function ( $module ) use ( $context ) {
774 try {
775 return $this->getModule( $module )->getVersionHash( $context );
776 } catch ( Exception $e ) {
777 // If modules fail to compute a version, don't fail the request (T152266)
778 // and still compute versions of other modules.
779 $this->outputErrorAndLog( $e,
780 'Calculating version for "{module}" failed: {exception}',
781 [
782 'module' => $module,
783 ]
784 );
785 return '';
786 }
787 }, $moduleNames );
788 return self::makeHash( implode( '', $hashes ) );
789 }
790
805 public function makeVersionQuery( ResourceLoaderContext $context, array $modules = null ) {
806 if ( $modules === null ) {
807 wfDeprecated( __METHOD__ . ' without $modules', '1.34' );
808 $modules = $context->getModules();
809 }
810 // As of MediaWiki 1.28, the server and client use the same algorithm for combining
811 // version hashes. There is no technical reason for this to be same, and for years the
812 // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
813 // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
814 // query parameter), then this method must continue to match the JS one.
815 $filtered = [];
816 foreach ( $modules as $name ) {
817 if ( !$this->getModule( $name ) ) {
818 // If a versioned request contains a missing module, the version is a mismatch
819 // as the client considered a module (and version) we don't have.
820 return '';
821 }
822 $filtered[] = $name;
823 }
824 return $this->getCombinedVersion( $context, $filtered );
825 }
826
832 public function respond( ResourceLoaderContext $context ) {
833 // Buffer output to catch warnings. Normally we'd use ob_clean() on the
834 // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
835 // is used: ob_clean() will clear the GZIP header in that case and it won't come
836 // back for subsequent output, resulting in invalid GZIP. So we have to wrap
837 // the whole thing in our own output buffer to be sure the active buffer
838 // doesn't use ob_gzhandler.
839 // See https://bugs.php.net/bug.php?id=36514
840 ob_start();
841
842 $this->measureResponseTime( RequestContext::getMain()->getTiming() );
843
844 // Find out which modules are missing and instantiate the others
845 $modules = [];
846 $missing = [];
847 foreach ( $context->getModules() as $name ) {
848 $module = $this->getModule( $name );
849 if ( $module ) {
850 // Do not allow private modules to be loaded from the web.
851 // This is a security issue, see T36907.
852 if ( $module->getGroup() === 'private' ) {
853 // Not a serious error, just means something is trying to access it (T101806)
854 $this->logger->debug( "Request for private module '$name' denied" );
855 $this->errors[] = "Cannot build private module \"$name\"";
856 continue;
857 }
858 $modules[$name] = $module;
859 } else {
860 $missing[] = $name;
861 }
862 }
863
864 try {
865 // Preload for getCombinedVersion() and for batch makeModuleResponse()
866 $this->preloadModuleInfo( array_keys( $modules ), $context );
867 } catch ( Exception $e ) {
868 $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
869 }
870
871 // Combine versions to propagate cache invalidation
872 $versionHash = '';
873 try {
874 $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
875 } catch ( Exception $e ) {
876 $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
877 }
878
879 // See RFC 2616 § 3.11 Entity Tags
880 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
881 $etag = 'W/"' . $versionHash . '"';
882
883 // Try the client-side cache first
884 if ( $this->tryRespondNotModified( $context, $etag ) ) {
885 return; // output handled (buffers cleared)
886 }
887
888 // Use file cache if enabled and available...
889 if ( $this->config->get( 'UseFileCache' ) ) {
890 $fileCache = ResourceFileCache::newFromContext( $context );
891 if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
892 return; // output handled
893 }
894 } else {
895 $fileCache = null;
896 }
897
898 // Generate a response
899 $response = $this->makeModuleResponse( $context, $modules, $missing );
900
901 // Capture any PHP warnings from the output buffer and append them to the
902 // error list if we're in debug mode.
903 if ( $context->getDebug() ) {
904 $warnings = ob_get_contents();
905 if ( strlen( $warnings ) ) {
906 $this->errors[] = $warnings;
907 }
908 }
909
910 // Consider saving the response to file cache (unless there are errors).
911 if ( $fileCache &&
912 !$this->errors &&
913 $missing === [] &&
915 ) {
916 if ( $fileCache->isCacheWorthy() ) {
917 // There were enough hits, save the response to the cache
918 $fileCache->saveText( $response );
919 } else {
920 $fileCache->incrMissesRecent( $context->getRequest() );
921 }
922 }
923
924 $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
925
926 // Remove the output buffer and output the response
927 ob_end_clean();
928
929 if ( $context->getImageObj() && $this->errors ) {
930 // We can't show both the error messages and the response when it's an image.
931 $response = implode( "\n\n", $this->errors );
932 } elseif ( $this->errors ) {
933 $errorText = implode( "\n\n", $this->errors );
934 $errorResponse = self::makeComment( $errorText );
935 if ( $context->shouldIncludeScripts() ) {
936 $errorResponse .= 'if (window.console && console.error) { console.error('
937 . $context->encodeJson( $errorText )
938 . "); }\n";
939 }
940
941 // Prepend error info to the response
942 $response = $errorResponse . $response;
943 }
944
945 $this->errors = [];
946 echo $response;
947 }
948
949 protected function measureResponseTime( Timing $timing ) {
950 DeferredUpdates::addCallableUpdate( function () use ( $timing ) {
951 $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
952 if ( $measure !== false ) {
953 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
954 $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
955 }
956 } );
957 }
958
969 protected function sendResponseHeaders(
970 ResourceLoaderContext $context, $etag, $errors, array $extra = []
971 ) : void {
972 HeaderCallback::warnIfHeadersSent();
973 $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
974 // Use a short cache expiry so that updates propagate to clients quickly, if:
975 // - No version specified (shared resources, e.g. stylesheets)
976 // - There were errors (recover quickly)
977 // - Version mismatch (T117587, T47877)
978 if ( $context->getVersion() === null
979 || $errors
980 || $context->getVersion() !== $this->makeVersionQuery( $context, $context->getModules() )
981 ) {
982 $maxage = $rlMaxage['unversioned'];
983 // If a version was specified we can use a longer expiry time since changing
984 // version numbers causes cache misses
985 } else {
986 $maxage = $rlMaxage['versioned'];
987 }
988 if ( $context->getImageObj() ) {
989 // Output different headers if we're outputting textual errors.
990 if ( $errors ) {
991 header( 'Content-Type: text/plain; charset=utf-8' );
992 } else {
993 $context->getImageObj()->sendResponseHeaders( $context );
994 }
995 } elseif ( $context->getOnly() === 'styles' ) {
996 header( 'Content-Type: text/css; charset=utf-8' );
997 header( 'Access-Control-Allow-Origin: *' );
998 } else {
999 header( 'Content-Type: text/javascript; charset=utf-8' );
1000 }
1001 // See RFC 2616 § 14.19 ETag
1002 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
1003 header( 'ETag: ' . $etag );
1004 if ( $context->getDebug() ) {
1005 // Do not cache debug responses
1006 header( 'Cache-Control: private, no-cache, must-revalidate' );
1007 header( 'Pragma: no-cache' );
1008 } else {
1009 header( "Cache-Control: public, max-age=$maxage, s-maxage=$maxage" );
1010 header( 'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
1011 }
1012 foreach ( $extra as $header ) {
1013 header( $header );
1014 }
1015 }
1016
1027 protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
1028 // See RFC 2616 § 14.26 If-None-Match
1029 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
1030 $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
1031 // Never send 304s in debug mode
1032 if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
1033 // There's another bug in ob_gzhandler (see also the comment at
1034 // the top of this function) that causes it to gzip even empty
1035 // responses, meaning it's impossible to produce a truly empty
1036 // response (because the gzip header is always there). This is
1037 // a problem because 304 responses have to be completely empty
1038 // per the HTTP spec, and Firefox behaves buggily when they're not.
1039 // See also https://bugs.php.net/bug.php?id=51579
1040 // To work around this, we tear down all output buffering before
1041 // sending the 304.
1042 wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
1043
1044 HttpStatus::header( 304 );
1045
1046 $this->sendResponseHeaders( $context, $etag, false );
1047 return true;
1048 }
1049 return false;
1050 }
1051
1060 protected function tryRespondFromFileCache(
1061 ResourceFileCache $fileCache,
1062 ResourceLoaderContext $context,
1063 $etag
1064 ) {
1065 $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
1066 // Buffer output to catch warnings.
1067 ob_start();
1068 // Get the maximum age the cache can be
1069 $maxage = $context->getVersion() === null
1070 ? $rlMaxage['unversioned']
1071 : $rlMaxage['versioned'];
1072 // Minimum timestamp the cache file must have
1073 $minTime = time() - $maxage;
1074 $good = $fileCache->isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1075 if ( !$good ) {
1076 try { // RL always hits the DB on file cache miss...
1078 } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
1079 $good = $fileCache->isCacheGood(); // cache existence check
1080 }
1081 }
1082 if ( $good ) {
1083 $ts = $fileCache->cacheTimestamp();
1084 // Send content type and cache headers
1085 $this->sendResponseHeaders( $context, $etag, false );
1086 $response = $fileCache->fetchText();
1087 // Capture any PHP warnings from the output buffer and append them to the
1088 // response in a comment if we're in debug mode.
1089 if ( $context->getDebug() ) {
1090 $warnings = ob_get_contents();
1091 if ( strlen( $warnings ) ) {
1092 $response = self::makeComment( $warnings ) . $response;
1093 }
1094 }
1095 // Remove the output buffer and output the response
1096 ob_end_clean();
1097 echo $response . "\n/* Cached {$ts} */";
1098 return true; // cache hit
1099 }
1100 // Clear buffer
1101 ob_end_clean();
1102
1103 return false; // cache miss
1104 }
1105
1114 public static function makeComment( $text ) {
1115 $encText = str_replace( '*/', '* /', $text );
1116 return "/*\n$encText\n*/\n";
1117 }
1118
1125 public static function formatException( Throwable $e ) {
1126 return self::makeComment( self::formatExceptionNoComment( $e ) );
1127 }
1128
1136 protected static function formatExceptionNoComment( Throwable $e ) {
1138
1139 if ( !$wgShowExceptionDetails ) {
1140 return MWExceptionHandler::getPublicLogMessage( $e );
1141 }
1142
1143 return MWExceptionHandler::getLogMessage( $e ) .
1144 "\nBacktrace:\n" .
1145 MWExceptionHandler::getRedactedTraceAsString( $e );
1146 }
1147
1160 array $modules, array $missing = []
1161 ) {
1162 $out = '';
1163 $states = [];
1164
1165 if ( $modules === [] && $missing === [] ) {
1166 return <<<MESSAGE
1167/* This file is the Web entry point for MediaWiki's ResourceLoader:
1168 <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1169 no modules were requested. Max made me put this here. */
1170MESSAGE;
1171 }
1172
1173 $image = $context->getImageObj();
1174 if ( $image ) {
1175 $data = $image->getImageData( $context );
1176 if ( $data === false ) {
1177 $data = '';
1178 $this->errors[] = 'Image generation failed';
1179 }
1180 return $data;
1181 }
1182
1183 foreach ( $missing as $name ) {
1184 $states[$name] = 'missing';
1185 }
1186
1187 $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js';
1188
1189 foreach ( $modules as $name => $module ) {
1190 try {
1191 $content = $module->getModuleContent( $context );
1192 $implementKey = $name . '@' . $module->getVersionHash( $context );
1193 $strContent = '';
1194
1195 if ( isset( $content['headers'] ) ) {
1196 $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1197 }
1198
1199 // Append output
1200 switch ( $context->getOnly() ) {
1201 case 'scripts':
1202 $scripts = $content['scripts'];
1203 if ( is_string( $scripts ) ) {
1204 // Load scripts raw...
1205 $strContent = $scripts;
1206 } elseif ( is_array( $scripts ) ) {
1207 // ...except when $scripts is an array of URLs or an associative array
1208 $strContent = self::makeLoaderImplementScript(
1209 $context,
1210 $implementKey,
1211 $scripts,
1212 [],
1213 [],
1214 []
1215 );
1216 }
1217 break;
1218 case 'styles':
1219 $styles = $content['styles'];
1220 // We no longer separate into media, they are all combined now with
1221 // custom media type groups into @media .. {} sections as part of the css string.
1222 // Module returns either an empty array or a numerical array with css strings.
1223 $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1224 break;
1225 default:
1226 $scripts = $content['scripts'] ?? '';
1227 if ( is_string( $scripts ) ) {
1228 if ( $name === 'site' || $name === 'user' ) {
1229 // Legacy scripts that run in the global scope without a closure.
1230 // mw.loader.implement will use globalEval if scripts is a string.
1231 // Minify manually here, because general response minification is
1232 // not effective due it being a string literal, not a function.
1233 if ( !$context->getDebug() ) {
1234 $scripts = self::filter( 'minify-js', $scripts ); // T107377
1235 }
1236 } else {
1237 $scripts = new XmlJsCode( $scripts );
1238 }
1239 }
1240 $strContent = self::makeLoaderImplementScript(
1241 $context,
1242 $implementKey,
1243 $scripts,
1244 $content['styles'] ?? [],
1245 isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1246 $content['templates'] ?? []
1247 );
1248 break;
1249 }
1250
1251 if ( !$context->getDebug() ) {
1252 $strContent = self::filter( $filter, $strContent );
1253 } else {
1254 // In debug mode, separate each response by a new line.
1255 // For example, between 'mw.loader.implement();' statements.
1256 $strContent = $this->ensureNewline( $strContent );
1257 }
1258
1259 if ( $context->getOnly() === 'scripts' ) {
1260 // Use a linebreak between module scripts (T162719)
1261 $out .= $this->ensureNewline( $strContent );
1262 } else {
1263 $out .= $strContent;
1264 }
1265
1266 } catch ( Exception $e ) {
1267 $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1268
1269 // Respond to client with error-state instead of module implementation
1270 $states[$name] = 'error';
1271 unset( $modules[$name] );
1272 }
1273 }
1274
1275 // Update module states
1276 if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1277 if ( $modules && $context->getOnly() === 'scripts' ) {
1278 // Set the state of modules loaded as only scripts to ready as
1279 // they don't have an mw.loader.implement wrapper that sets the state
1280 foreach ( $modules as $name => $module ) {
1281 $states[$name] = 'ready';
1282 }
1283 }
1284
1285 // Set the state of modules we didn't respond to with mw.loader.implement
1286 if ( $states ) {
1287 $stateScript = self::makeLoaderStateScript( $context, $states );
1288 if ( !$context->getDebug() ) {
1289 $stateScript = self::filter( 'minify-js', $stateScript );
1290 }
1291 // Use a linebreak between module script and state script (T162719)
1292 $out = $this->ensureNewline( $out ) . $stateScript;
1293 }
1294 } elseif ( $states ) {
1295 $this->errors[] = 'Problematic modules: '
1296 . $context->encodeJson( $states );
1297 }
1298
1299 return $out;
1300 }
1301
1307 private function ensureNewline( $str ) {
1308 $end = substr( $str, -1 );
1309 if ( $end === false || $end === '' || $end === "\n" ) {
1310 return $str;
1311 }
1312 return $str . "\n";
1313 }
1314
1321 public function getModulesByMessage( $messageKey ) {
1322 $moduleNames = [];
1323 foreach ( $this->getModuleNames() as $moduleName ) {
1324 $module = $this->getModule( $moduleName );
1325 if ( in_array( $messageKey, $module->getMessages() ) ) {
1326 $moduleNames[] = $moduleName;
1327 }
1328 }
1329 return $moduleNames;
1330 }
1331
1349 private static function makeLoaderImplementScript(
1350 ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates
1351 ) {
1352 if ( $scripts instanceof XmlJsCode ) {
1353 if ( $scripts->value === '' ) {
1354 $scripts = null;
1355 } elseif ( $context->getDebug() ) {
1356 $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1357 } else {
1358 $scripts = new XmlJsCode( 'function($,jQuery,require,module){' . $scripts->value . '}' );
1359 }
1360 } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1361 $files = $scripts['files'];
1362 foreach ( $files as $path => &$file ) {
1363 // $file is changed (by reference) from a descriptor array to the content of the file
1364 // All of these essentially do $file = $file['content'];, some just have wrapping around it
1365 if ( $file['type'] === 'script' ) {
1366 // Multi-file modules only get two parameters ($ and jQuery are being phased out)
1367 if ( $context->getDebug() ) {
1368 $file = new XmlJsCode( "function ( require, module ) {\n{$file['content']}\n}" );
1369 } else {
1370 $file = new XmlJsCode( 'function(require,module){' . $file['content'] . '}' );
1371 }
1372 } else {
1373 $file = $file['content'];
1374 }
1375 }
1376 $scripts = XmlJsCode::encodeObject( [
1377 'main' => $scripts['main'],
1378 'files' => XmlJsCode::encodeObject( $files, $context->getDebug() )
1379 ], $context->getDebug() );
1380 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1381 throw new InvalidArgumentException( 'Script must be a string or an array of URLs' );
1382 }
1383
1384 // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1385 // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1386 // of "{}". Force them to objects.
1387 $module = [
1388 $name,
1389 $scripts,
1390 (object)$styles,
1391 (object)$messages,
1392 (object)$templates
1393 ];
1394 self::trimArray( $module );
1395
1396 return Xml::encodeJsCall( 'mw.loader.implement', $module, $context->getDebug() );
1397 }
1398
1405 public static function makeMessageSetScript( $messages ) {
1406 return 'mw.messages.set('
1407 . self::encodeJsonForScript( (object)$messages )
1408 . ');';
1409 }
1410
1418 public static function makeCombinedStyles( array $stylePairs ) {
1419 $out = [];
1420 foreach ( $stylePairs as $media => $styles ) {
1421 // ResourceLoaderFileModule::getStyle can return the styles
1422 // as a string or an array of strings. This is to allow separation in
1423 // the front-end.
1424 $styles = (array)$styles;
1425 foreach ( $styles as $style ) {
1426 $style = trim( $style );
1427 // Don't output an empty "@media print { }" block (T42498)
1428 if ( $style !== '' ) {
1429 // Transform the media type based on request params and config
1430 // The way that this relies on $wgRequest to propagate request params is slightly evil
1431 $media = OutputPage::transformCssMedia( $media );
1432
1433 if ( $media === '' || $media == 'all' ) {
1434 $out[] = $style;
1435 } elseif ( is_string( $media ) ) {
1436 $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1437 }
1438 // else: skip
1439 }
1440 }
1441 }
1442 return $out;
1443 }
1444
1454 public static function encodeJsonForScript( $data ) {
1455 // Keep output as small as possible by disabling needless escape modes
1456 // that PHP uses by default.
1457 // However, while most module scripts are only served on HTTP responses
1458 // for JavaScript, some modules can also be embedded in the HTML as inline
1459 // scripts. This, and the fact that we sometimes need to export strings
1460 // containing user-generated content and labels that may genuinely contain
1461 // a sequences like "</script>", we need to encode either '/' or '<'.
1462 // By default PHP escapes '/'. Let's escape '<' instead which is less common
1463 // and allows URLs to mostly remain readable.
1464 $jsonFlags = JSON_UNESCAPED_SLASHES |
1465 JSON_UNESCAPED_UNICODE |
1466 JSON_HEX_TAG |
1467 JSON_HEX_AMP;
1468 if ( self::inDebugMode() ) {
1469 $jsonFlags |= JSON_PRETTY_PRINT;
1470 }
1471 return json_encode( $data, $jsonFlags );
1472 }
1473
1486 public static function makeLoaderStateScript(
1487 ResourceLoaderContext $context, array $states
1488 ) {
1489 return 'mw.loader.state('
1490 . $context->encodeJson( $states )
1491 . ');';
1493
1494 private static function isEmptyObject( stdClass $obj ) {
1495 foreach ( $obj as $key => $value ) {
1496 return false;
1497 }
1498 return true;
1499 }
1500
1514 private static function trimArray( array &$array ) : void {
1515 $i = count( $array );
1516 while ( $i-- ) {
1517 if ( $array[$i] === null
1518 || $array[$i] === []
1519 || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1520 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1521 ) {
1522 unset( $array[$i] );
1523 } else {
1524 break;
1525 }
1526 }
1527 }
1528
1556 public static function makeLoaderRegisterScript(
1557 ResourceLoaderContext $context, array $modules
1558 ) {
1559 // Optimisation: Transform dependency names into indexes when possible
1560 // to produce smaller output. They are expanded by mw.loader.register on
1561 // the other end using resolveIndexedDependencies().
1562 $index = [];
1563 foreach ( $modules as $i => &$module ) {
1564 // Build module name index
1565 $index[$module[0]] = $i;
1566 }
1567 foreach ( $modules as &$module ) {
1568 if ( isset( $module[2] ) ) {
1569 foreach ( $module[2] as &$dependency ) {
1570 if ( isset( $index[$dependency] ) ) {
1571 // Replace module name in dependency list with index
1572 $dependency = $index[$dependency];
1573 }
1574 }
1575 }
1576 }
1577
1578 array_walk( $modules, [ self::class, 'trimArray' ] );
1579
1580 return 'mw.loader.register('
1581 . $context->encodeJson( $modules )
1582 . ');';
1583 }
1584
1598 public static function makeLoaderSourcesScript(
1599 ResourceLoaderContext $context, array $sources
1600 ) {
1601 return 'mw.loader.addSource('
1602 . $context->encodeJson( $sources )
1603 . ');';
1604 }
1605
1612 public static function makeLoaderConditionalScript( $script ) {
1613 // Adds a function to lazy-created RLQ
1614 return '(RLQ=window.RLQ||[]).push(function(){' .
1615 trim( $script ) . '});';
1616 }
1617
1626 public static function makeInlineCodeWithModule( $modules, $script ) {
1627 // Adds an array to lazy-created RLQ
1628 return '(RLQ=window.RLQ||[]).push(['
1629 . self::encodeJsonForScript( $modules ) . ','
1630 . 'function(){' . trim( $script ) . '}'
1631 . ']);';
1632 }
1633
1645 public static function makeInlineScript( $script, $nonce = null ) {
1646 $js = self::makeLoaderConditionalScript( $script );
1647 $escNonce = '';
1648 if ( $nonce === null ) {
1649 wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1650 } elseif ( $nonce !== false ) {
1651 // If it was false, CSP is disabled, so no nonce attribute.
1652 // Nonce should be only base64 characters, so should be safe,
1653 // but better to be safely escaped than sorry.
1654 $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1655 }
1656
1657 return new WrappedString(
1658 Html::inlineScript( $js, $nonce ),
1659 "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1660 '});</script>'
1661 );
1662 }
1663
1672 public static function makeConfigSetScript( array $configuration ) {
1673 $json = self::encodeJsonForScript( $configuration );
1674 if ( $json === false ) {
1675 $e = new Exception(
1676 'JSON serialization of config data failed. ' .
1677 'This usually means the config data is not valid UTF-8.'
1678 );
1680 return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1681 }
1682 return "mw.config.set($json);";
1683 }
1684
1698 public static function makePackedModulesString( array $modules ) {
1699 $moduleMap = []; // [ prefix => [ suffixes ] ]
1700 foreach ( $modules as $module ) {
1701 $pos = strrpos( $module, '.' );
1702 $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1703 $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1704 $moduleMap[$prefix][] = $suffix;
1705 }
1706
1707 $arr = [];
1708 foreach ( $moduleMap as $prefix => $suffixes ) {
1709 $p = $prefix === '' ? '' : $prefix . '.';
1710 $arr[] = $p . implode( ',', $suffixes );
1711 }
1712 return implode( '|', $arr );
1713 }
1714
1726 public static function expandModuleNames( $modules ) {
1727 $retval = [];
1728 $exploded = explode( '|', $modules );
1729 foreach ( $exploded as $group ) {
1730 if ( strpos( $group, ',' ) === false ) {
1731 // This is not a set of modules in foo.bar,baz notation
1732 // but a single module
1733 $retval[] = $group;
1734 } else {
1735 // This is a set of modules in foo.bar,baz notation
1736 $pos = strrpos( $group, '.' );
1737 if ( $pos === false ) {
1738 // Prefixless modules, i.e. without dots
1739 $retval = array_merge( $retval, explode( ',', $group ) );
1740 } else {
1741 // We have a prefix and a bunch of suffixes
1742 $prefix = substr( $group, 0, $pos ); // 'foo'
1743 $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1744 foreach ( $suffixes as $suffix ) {
1745 $retval[] = "$prefix.$suffix";
1746 }
1747 }
1748 }
1749 }
1750 return $retval;
1751 }
1752
1763 public static function inDebugMode() {
1764 if ( self::$debugMode === null ) {
1766 self::$debugMode = $wgRequest->getFuzzyBool( 'debug',
1767 $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug )
1768 );
1769 }
1770 return self::$debugMode;
1771 }
1772
1783 public static function clearCache() {
1784 self::$debugMode = null;
1785 }
1786
1796 public function createLoaderURL( $source, ResourceLoaderContext $context,
1797 array $extraQuery = []
1798 ) {
1799 $query = self::createLoaderQuery( $context, $extraQuery );
1800 $script = $this->getLoadScript( $source );
1801
1802 return wfAppendQuery( $script, $query );
1803 }
1804
1814 protected static function createLoaderQuery(
1815 ResourceLoaderContext $context, array $extraQuery = []
1816 ) {
1817 return self::makeLoaderQuery(
1818 $context->getModules(),
1819 $context->getLanguage(),
1820 $context->getSkin(),
1821 $context->getUser(),
1822 $context->getVersion(),
1823 $context->getDebug(),
1824 $context->getOnly(),
1825 $context->getRequest()->getBool( 'printable' ),
1826 $context->getRequest()->getBool( 'handheld' ),
1827 $extraQuery
1828 );
1829 }
1830
1847 public static function makeLoaderQuery( array $modules, $lang, $skin, $user = null,
1848 $version = null, $debug = false, $only = null, $printable = false,
1849 $handheld = false, array $extraQuery = []
1850 ) {
1851 $query = [
1852 'modules' => self::makePackedModulesString( $modules ),
1853 ];
1854 // Keep urls short by omitting query parameters that
1855 // match the defaults assumed by ResourceLoaderContext.
1856 // Note: This relies on the defaults either being insignificant or forever constant,
1857 // as otherwise cached urls could change in meaning when the defaults change.
1858 if ( $lang !== ResourceLoaderContext::DEFAULT_LANG ) {
1859 $query['lang'] = $lang;
1860 }
1861 if ( $skin !== ResourceLoaderContext::DEFAULT_SKIN ) {
1862 $query['skin'] = $skin;
1863 }
1864 if ( $debug === true ) {
1865 $query['debug'] = 'true';
1866 }
1867 if ( $user !== null ) {
1868 $query['user'] = $user;
1869 }
1870 if ( $version !== null ) {
1871 $query['version'] = $version;
1872 }
1873 if ( $only !== null ) {
1874 $query['only'] = $only;
1875 }
1876 if ( $printable ) {
1877 $query['printable'] = 1;
1878 }
1879 if ( $handheld ) {
1880 $query['handheld'] = 1;
1881 }
1882 $query += $extraQuery;
1883
1884 // Make queries uniform in order
1885 ksort( $query );
1886 return $query;
1887 }
1888
1898 public static function isValidModuleName( $moduleName ) {
1899 $len = strlen( $moduleName );
1900 return $len <= 255 && strcspn( $moduleName, '!,|', 0, $len ) === $len;
1901 }
1902
1913 public function getLessCompiler( $vars = [] ) {
1914 global $IP;
1915 // When called from the installer, it is possible that a required PHP extension
1916 // is missing (at least for now; see T49564). If this is the case, throw an
1917 // exception (caught by the installer) to prevent a fatal error later on.
1918 if ( !class_exists( 'Less_Parser' ) ) {
1919 throw new MWException( 'MediaWiki requires the less.php parser' );
1920 }
1921
1922 $parser = new Less_Parser;
1923 $parser->ModifyVars( $vars );
1924 $parser->SetImportDirs( [
1925 "$IP/resources/src/mediawiki.less/" => '',
1926 ] );
1927 $parser->SetOption( 'relativeUrls', false );
1928
1929 return $parser;
1930 }
1931
1940 public static function getSiteConfigSettings(
1941 ResourceLoaderContext $context, Config $conf
1942 ) : array {
1943 // Namespace related preparation
1944 // - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
1945 // - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
1946 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1947 $namespaceIds = $contLang->getNamespaceIds();
1948 $caseSensitiveNamespaces = [];
1949 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1950 foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
1951 $namespaceIds[$contLang->lc( $name )] = $index;
1952 if ( !$nsInfo->isCapitalized( $index ) ) {
1953 $caseSensitiveNamespaces[] = $index;
1954 }
1955 }
1956
1957 $illegalFileChars = $conf->get( 'IllegalFileChars' );
1958
1959 // Build list of variables
1960 $skin = $context->getSkin();
1961
1962 // Start of supported and stable config vars (for use by extensions/gadgets).
1963 $vars = [
1964 'debug' => $context->getDebug(),
1965 'skin' => $skin,
1966 'stylepath' => $conf->get( 'StylePath' ),
1967 'wgArticlePath' => $conf->get( 'ArticlePath' ),
1968 'wgScriptPath' => $conf->get( 'ScriptPath' ),
1969 'wgScript' => $conf->get( 'Script' ),
1970 'wgSearchType' => $conf->get( 'SearchType' ),
1971 'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
1972 'wgServer' => $conf->get( 'Server' ),
1973 'wgServerName' => $conf->get( 'ServerName' ),
1974 'wgUserLanguage' => $context->getLanguage(),
1975 'wgContentLanguage' => $contLang->getCode(),
1976 'wgVersion' => MW_VERSION,
1977 'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
1978 'wgNamespaceIds' => $namespaceIds,
1979 'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
1980 'wgSiteName' => $conf->get( 'Sitename' ),
1981 'wgDBname' => $conf->get( 'DBname' ),
1982 'wgWikiID' => WikiMap::getCurrentWikiId(),
1983 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
1984 'wgCommentByteLimit' => null,
1985 'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
1986 'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
1987 ];
1988 // End of stable config vars.
1989
1990 // Internal variables for use by MediaWiki core and/or ResourceLoader.
1991 $vars += [
1992 // @internal For mediawiki.widgets
1993 'wgUrlProtocols' => wfUrlProtocols(),
1994 // @internal For mediawiki.page.watch
1995 // Force object to avoid "empty" associative array from
1996 // becoming [] instead of {} in JS (T36604)
1997 'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
1998 // @internal For mediawiki.language
1999 'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ),
2000 // @internal For mediawiki.Title
2001 'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
2002 // @internal For mediawiki.cookie
2003 'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
2004 'wgCookieDomain' => $conf->get( 'CookieDomain' ),
2005 'wgCookiePath' => $conf->get( 'CookiePath' ),
2006 'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
2007 // @internal For mediawiki.Title
2008 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
2009 'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
2010 // @internal For mediawiki.ForeignUpload
2011 'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
2012 'wgEnableUploads' => $conf->get( 'EnableUploads' ),
2013 ];
2014
2015 Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );
2016
2017 return $vars;
2018 }
2019}
$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 MW_VERSION
The running version of MediaWiki.
Definition Defines.php:40
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...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:643
$IP
Definition WebStart.php:49
static minify( $css)
Removes whitespace from CSS data.
Definition CSSMin.php:476
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:172
static minify( $s)
Returns minified JavaScript code.
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.
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.
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information.
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path,...
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.
static formatException(Throwable $e)
Handle exception display.
makeVersionQuery(ResourceLoaderContext $context, array $modules=null)
Get the expected value of the 'version' query parameter.
tryRespondFromFileCache(ResourceFileCache $fileCache, ResourceLoaderContext $context, $etag)
Send out code for a response from file cache if possible.
isFileModule( $name)
Whether the module is a ResourceLoaderFileModule or subclass thereof.
getModuleNames()
Get a list of module names.
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.
static bool $debugMode
array[] $moduleInfos
Map of (module name => associative info array)
tryRespondNotModified(ResourceLoaderContext $context, $etag)
Respond with HTTP 304 Not Modified if appropiate.
__construct(Config $config=null, LoggerInterface $logger=null, DependencyStore $tracker=null)
Register core modules and runs registration hooks.
getSources()
Get the list of sources.
loadModuleDependenciesInternal( $moduleName, $variant)
DependencyStore $depStore
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.
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:124
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition Title.php:723
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:58
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Definition Xml.php:681
const CACHE_ANYTHING
Definition Defines.php:91
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