MediaWiki REL1_34
ResourceLoader.php
Go to the documentation of this file.
1<?php
24use Psr\Log\LoggerAwareInterface;
25use Psr\Log\LoggerInterface;
26use Psr\Log\NullLogger;
28use Wikimedia\WrappedString;
29
44class ResourceLoader implements LoggerAwareInterface {
46 protected $config;
48 protected $blobStore;
49
51 private $logger;
52
54 protected $modules = [];
56 protected $moduleInfos = [];
62 protected $testModuleNames = [];
64 protected $testSuiteModuleNames = [];
65
67 protected $sources = [];
69 protected $errors = [];
71 protected $extraHeaders = [];
72
74 protected static $debugMode = null;
75
77 const CACHE_VERSION = 8;
78
80 const FILTER_NOMIN = '/*@nomin*/';
81
96 public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
97 if ( !$moduleNames ) {
98 // Or else Database*::select() will explode, plus it's cheaper!
99 return;
100 }
102 $lang = $context->getLanguage();
103
104 // Batched version of ResourceLoaderModule::getFileDependencies
106 $res = $dbr->select( 'module_deps', [ 'md_module', 'md_deps' ], [
107 'md_module' => $moduleNames,
108 'md_skin' => $vary,
109 ], __METHOD__
110 );
111
112 // Prime in-object cache for file dependencies
113 $modulesWithDeps = [];
114 foreach ( $res as $row ) {
115 $module = $this->getModule( $row->md_module );
116 if ( $module ) {
117 $module->setFileDependencies( $context, ResourceLoaderModule::expandRelativePaths(
118 json_decode( $row->md_deps, true )
119 ) );
120 $modulesWithDeps[] = $row->md_module;
121 }
122 }
123 // Register the absence of a dependency row too
124 foreach ( array_diff( $moduleNames, $modulesWithDeps ) as $name ) {
125 $module = $this->getModule( $name );
126 if ( $module ) {
127 $this->getModule( $name )->setFileDependencies( $context, [] );
128 }
129 }
130
131 // Batched version of ResourceLoaderWikiModule::getTitleInfo
132 ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
133
134 // Prime in-object cache for message blobs for modules with messages
135 $modules = [];
136 foreach ( $moduleNames as $name ) {
137 $module = $this->getModule( $name );
138 if ( $module && $module->getMessages() ) {
139 $modules[$name] = $module;
140 }
141 }
142 $store = $this->getMessageBlobStore();
143 $blobs = $store->getBlobs( $modules, $lang );
144 foreach ( $blobs as $name => $blob ) {
145 $modules[$name]->setMessageBlob( $blob, $lang );
146 }
147 }
148
166 public static function filter( $filter, $data, array $options = [] ) {
167 if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
168 return $data;
169 }
170
171 if ( isset( $options['cache'] ) && $options['cache'] === false ) {
172 return self::applyFilter( $filter, $data );
173 }
174
175 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
176 $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
177
178 $key = $cache->makeGlobalKey(
179 'resourceloader-filter',
180 $filter,
181 self::CACHE_VERSION,
182 md5( $data )
183 );
184
185 $result = $cache->get( $key );
186 if ( $result === false ) {
187 $stats->increment( "resourceloader_cache.$filter.miss" );
188 $result = self::applyFilter( $filter, $data );
189 $cache->set( $key, $result, 24 * 3600 );
190 } else {
191 $stats->increment( "resourceloader_cache.$filter.hit" );
192 }
193 if ( $result === null ) {
194 // Cached failure
195 $result = $data;
196 }
197
198 return $result;
199 }
200
201 private static function applyFilter( $filter, $data ) {
202 $data = trim( $data );
203 if ( $data ) {
204 try {
205 $data = ( $filter === 'minify-css' )
206 ? CSSMin::minify( $data )
208 } catch ( Exception $e ) {
209 MWExceptionHandler::logException( $e );
210 return null;
211 }
212 }
213 return $data;
214 }
215
221 public function __construct( Config $config = null, LoggerInterface $logger = null ) {
222 $this->logger = $logger ?: new NullLogger();
223
224 if ( !$config ) {
225 wfDeprecated( __METHOD__ . ' without a Config instance', '1.34' );
226 $config = MediaWikiServices::getInstance()->getMainConfig();
227 }
228 $this->config = $config;
229
230 // Add 'local' source first
231 $this->addSource( 'local', $config->get( 'LoadScript' ) );
232
233 // Special module that always exists
234 $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
235
236 $this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) );
237 }
238
242 public function getConfig() {
243 return $this->config;
244 }
245
250 public function setLogger( LoggerInterface $logger ) {
251 $this->logger = $logger;
252 }
253
258 public function getLogger() {
259 return $this->logger;
260 }
261
266 public function getMessageBlobStore() {
267 return $this->blobStore;
268 }
269
274 public function setMessageBlobStore( MessageBlobStore $blobStore ) {
275 $this->blobStore = $blobStore;
276 }
277
289 public function register( $name, $info = null ) {
290 $moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
291
292 // Allow multiple modules to be registered in one call
293 $registrations = is_array( $name ) ? $name : [ $name => $info ];
294 foreach ( $registrations as $name => $info ) {
295 // Warn on duplicate registrations
296 if ( isset( $this->moduleInfos[$name] ) ) {
297 // A module has already been registered by this name
298 $this->logger->warning(
299 'ResourceLoader duplicate registration warning. ' .
300 'Another module has already been registered as ' . $name
301 );
302 }
303
304 // Check validity
305 if ( !self::isValidModuleName( $name ) ) {
306 throw new MWException( "ResourceLoader module name '$name' is invalid, "
307 . "see ResourceLoader::isValidModuleName()" );
308 }
309 if ( !is_array( $info ) ) {
310 throw new InvalidArgumentException(
311 'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
312 );
313 }
314
315 // Attach module
316 $this->moduleInfos[$name] = $info;
317
318 // Last-minute changes
319 // Apply custom skin-defined styles to existing modules.
320 if ( $this->isFileModule( $name ) ) {
321 foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
322 // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
323 if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
324 continue;
325 }
326
327 // If $name is preceded with a '+', the defined style files will be added to 'default'
328 // skinStyles, otherwise 'default' will be ignored as it normally would be.
329 if ( isset( $skinStyles[$name] ) ) {
330 $paths = (array)$skinStyles[$name];
331 $styleFiles = [];
332 } elseif ( isset( $skinStyles['+' . $name] ) ) {
333 $paths = (array)$skinStyles['+' . $name];
334 $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
335 (array)$this->moduleInfos[$name]['skinStyles']['default'] :
336 [];
337 } else {
338 continue;
339 }
340
341 // Add new file paths, remapping them to refer to our directories and not use settings
342 // from the module we're modifying, which come from the base definition.
343 list( $localBasePath, $remoteBasePath ) =
345
346 foreach ( $paths as $path ) {
347 $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
348 }
349
350 $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
351 }
352 }
353 }
354 }
355
360 public function registerTestModules() {
361 global $IP;
362
363 if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
364 throw new MWException( 'Attempt to register JavaScript test modules '
365 . 'but <code>$wgEnableJavaScriptTest</code> is false. '
366 . 'Edit your <code>LocalSettings.php</code> to enable it.' );
367 }
368
369 // This has a 'qunit' key for compat with the below hook.
370 $testModulesMeta = [ 'qunit' => [] ];
371
372 // Get test suites from extensions
373 // Avoid PHP 7.1 warning from passing $this by reference
374 $rl = $this;
375 Hooks::run( 'ResourceLoaderTestModules', [ &$testModulesMeta, &$rl ] );
376 $extRegistry = ExtensionRegistry::getInstance();
377 // In case of conflict, the deprecated hook has precedence.
378 $testModules = $testModulesMeta['qunit'] + $extRegistry->getAttribute( 'QUnitTestModules' );
379
380 $testSuiteModuleNames = [];
381 foreach ( $testModules as $name => &$module ) {
382 // Turn any single-module dependency into an array
383 if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
384 $module['dependencies'] = [ $module['dependencies'] ];
385 }
386
387 // Ensure the testrunner loads before any test suites
388 $module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
389
390 // Keep track of the test suites to load on SpecialJavaScriptTest
391 $testSuiteModuleNames[] = $name;
392 }
393
394 // Core test suites (their names have further precedence).
395 $testModules = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules;
396 $testSuiteModuleNames[] = 'test.mediawiki.qunit.suites';
397
398 $this->register( $testModules );
399 $this->testSuiteModuleNames = $testSuiteModuleNames;
400 }
401
412 public function addSource( $id, $loadUrl = null ) {
413 // Allow multiple sources to be registered in one call
414 if ( is_array( $id ) ) {
415 foreach ( $id as $key => $value ) {
416 $this->addSource( $key, $value );
417 }
418 return;
419 }
420
421 // Disallow duplicates
422 if ( isset( $this->sources[$id] ) ) {
423 throw new MWException(
424 'ResourceLoader duplicate source addition error. ' .
425 'Another source has already been registered as ' . $id
426 );
427 }
428
429 // Pre 1.24 backwards-compatibility
430 if ( is_array( $loadUrl ) ) {
431 if ( !isset( $loadUrl['loadScript'] ) ) {
432 throw new MWException(
433 __METHOD__ . ' was passed an array with no "loadScript" key.'
434 );
435 }
436
437 $loadUrl = $loadUrl['loadScript'];
438 }
439
440 $this->sources[$id] = $loadUrl;
441 }
442
448 public function getModuleNames() {
449 return array_keys( $this->moduleInfos );
450 }
451
459 public function getTestSuiteModuleNames() {
460 return $this->testSuiteModuleNames;
461 }
462
470 public function isModuleRegistered( $name ) {
471 return isset( $this->moduleInfos[$name] );
472 }
473
485 public function getModule( $name ) {
486 if ( !isset( $this->modules[$name] ) ) {
487 if ( !isset( $this->moduleInfos[$name] ) ) {
488 // No such module
489 return null;
490 }
491 // Construct the requested module object
492 $info = $this->moduleInfos[$name];
493 if ( isset( $info['factory'] ) ) {
495 $object = call_user_func( $info['factory'], $info );
496 } else {
497 $class = $info['class'] ?? ResourceLoaderFileModule::class;
499 $object = new $class( $info );
500 }
501 $object->setConfig( $this->getConfig() );
502 $object->setLogger( $this->logger );
503 $object->setName( $name );
504 $this->modules[$name] = $object;
505 }
506
507 return $this->modules[$name];
508 }
509
516 protected function isFileModule( $name ) {
517 if ( !isset( $this->moduleInfos[$name] ) ) {
518 return false;
519 }
520 $info = $this->moduleInfos[$name];
521 return !isset( $info['factory'] ) && (
522 // The implied default for 'class' is ResourceLoaderFileModule
523 !isset( $info['class'] ) ||
524 // Explicit default
525 $info['class'] === ResourceLoaderFileModule::class ||
526 is_subclass_of( $info['class'], ResourceLoaderFileModule::class )
527 );
528 }
529
535 public function getSources() {
536 return $this->sources;
537 }
538
548 public function getLoadScript( $source ) {
549 if ( !isset( $this->sources[$source] ) ) {
550 throw new MWException( "The $source source was never registered in ResourceLoader." );
551 }
552 return $this->sources[$source];
553 }
554
558 const HASH_LENGTH = 5;
559
622 public static function makeHash( $value ) {
623 $hash = hash( 'fnv132', $value );
624 // The base_convert will pad it (if too short),
625 // then substr() will trim it (if too long).
626 return substr(
627 Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
628 0,
629 self::HASH_LENGTH
630 );
631 }
632
642 public function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
643 MWExceptionHandler::logException( $e );
644 $this->logger->warning(
645 $msg,
646 $context + [ 'exception' => $e ]
647 );
648 $this->errors[] = self::formatExceptionNoComment( $e );
649 }
650
659 public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
660 if ( !$moduleNames ) {
661 return '';
662 }
663 $hashes = array_map( function ( $module ) use ( $context ) {
664 try {
665 return $this->getModule( $module )->getVersionHash( $context );
666 } catch ( Exception $e ) {
667 // If modules fail to compute a version, don't fail the request (T152266)
668 // and still compute versions of other modules.
669 $this->outputErrorAndLog( $e,
670 'Calculating version for "{module}" failed: {exception}',
671 [
672 'module' => $module,
673 ]
674 );
675 return '';
676 }
677 }, $moduleNames );
678 return self::makeHash( implode( '', $hashes ) );
679 }
680
695 public function makeVersionQuery( ResourceLoaderContext $context, array $modules = null ) {
696 if ( $modules === null ) {
697 wfDeprecated( __METHOD__ . ' without $modules', '1.34' );
698 $modules = $context->getModules();
699 }
700 // As of MediaWiki 1.28, the server and client use the same algorithm for combining
701 // version hashes. There is no technical reason for this to be same, and for years the
702 // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
703 // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
704 // query parameter), then this method must continue to match the JS one.
705 $filtered = [];
706 foreach ( $modules as $name ) {
707 if ( !$this->getModule( $name ) ) {
708 // If a versioned request contains a missing module, the version is a mismatch
709 // as the client considered a module (and version) we don't have.
710 return '';
711 }
712 $filtered[] = $name;
713 }
714 return $this->getCombinedVersion( $context, $filtered );
715 }
716
723 // Buffer output to catch warnings. Normally we'd use ob_clean() on the
724 // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
725 // is used: ob_clean() will clear the GZIP header in that case and it won't come
726 // back for subsequent output, resulting in invalid GZIP. So we have to wrap
727 // the whole thing in our own output buffer to be sure the active buffer
728 // doesn't use ob_gzhandler.
729 // See https://bugs.php.net/bug.php?id=36514
730 ob_start();
731
732 $this->measureResponseTime( RequestContext::getMain()->getTiming() );
733
734 // Find out which modules are missing and instantiate the others
735 $modules = [];
736 $missing = [];
737 foreach ( $context->getModules() as $name ) {
738 $module = $this->getModule( $name );
739 if ( $module ) {
740 // Do not allow private modules to be loaded from the web.
741 // This is a security issue, see T36907.
742 if ( $module->getGroup() === 'private' ) {
743 // Not a serious error, just means something is trying to access it (T101806)
744 $this->logger->debug( "Request for private module '$name' denied" );
745 $this->errors[] = "Cannot build private module \"$name\"";
746 continue;
747 }
748 $modules[$name] = $module;
749 } else {
750 $missing[] = $name;
751 }
752 }
753
754 try {
755 // Preload for getCombinedVersion() and for batch makeModuleResponse()
756 $this->preloadModuleInfo( array_keys( $modules ), $context );
757 } catch ( Exception $e ) {
758 $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
759 }
760
761 // Combine versions to propagate cache invalidation
762 $versionHash = '';
763 try {
764 $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
765 } catch ( Exception $e ) {
766 $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
767 }
768
769 // See RFC 2616 § 3.11 Entity Tags
770 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
771 $etag = 'W/"' . $versionHash . '"';
772
773 // Try the client-side cache first
774 if ( $this->tryRespondNotModified( $context, $etag ) ) {
775 return; // output handled (buffers cleared)
776 }
777
778 // Use file cache if enabled and available...
779 if ( $this->config->get( 'UseFileCache' ) ) {
781 if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
782 return; // output handled
783 }
784 } else {
785 $fileCache = null;
786 }
787
788 // Generate a response
789 $response = $this->makeModuleResponse( $context, $modules, $missing );
790
791 // Capture any PHP warnings from the output buffer and append them to the
792 // error list if we're in debug mode.
793 if ( $context->getDebug() ) {
794 $warnings = ob_get_contents();
795 if ( strlen( $warnings ) ) {
796 $this->errors[] = $warnings;
797 }
798 }
799
800 // Consider saving the response to file cache (unless there are errors).
801 if ( $fileCache &&
802 !$this->errors &&
803 $missing === [] &&
805 ) {
806 if ( $fileCache->isCacheWorthy() ) {
807 // There were enough hits, save the response to the cache
808 $fileCache->saveText( $response );
809 } else {
810 $fileCache->incrMissesRecent( $context->getRequest() );
811 }
812 }
813
814 $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
815
816 // Remove the output buffer and output the response
817 ob_end_clean();
818
819 if ( $context->getImageObj() && $this->errors ) {
820 // We can't show both the error messages and the response when it's an image.
821 $response = implode( "\n\n", $this->errors );
822 } elseif ( $this->errors ) {
823 $errorText = implode( "\n\n", $this->errors );
824 $errorResponse = self::makeComment( $errorText );
825 if ( $context->shouldIncludeScripts() ) {
826 $errorResponse .= 'if (window.console && console.error) { console.error('
827 . $context->encodeJson( $errorText )
828 . "); }\n";
829 }
830
831 // Prepend error info to the response
832 $response = $errorResponse . $response;
833 }
834
835 $this->errors = [];
836 echo $response;
837 }
838
839 protected function measureResponseTime( Timing $timing ) {
840 DeferredUpdates::addCallableUpdate( function () use ( $timing ) {
841 $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
842 if ( $measure !== false ) {
843 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
844 $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
845 }
846 } );
847 }
848
860 protected function sendResponseHeaders(
861 ResourceLoaderContext $context, $etag, $errors, array $extra = []
862 ) {
863 \MediaWiki\HeaderCallback::warnIfHeadersSent();
864 $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
865 // Use a short cache expiry so that updates propagate to clients quickly, if:
866 // - No version specified (shared resources, e.g. stylesheets)
867 // - There were errors (recover quickly)
868 // - Version mismatch (T117587, T47877)
869 if ( is_null( $context->getVersion() )
870 || $errors
871 || $context->getVersion() !== $this->makeVersionQuery( $context, $context->getModules() )
872 ) {
873 $maxage = $rlMaxage['unversioned']['client'];
874 $smaxage = $rlMaxage['unversioned']['server'];
875 // If a version was specified we can use a longer expiry time since changing
876 // version numbers causes cache misses
877 } else {
878 $maxage = $rlMaxage['versioned']['client'];
879 $smaxage = $rlMaxage['versioned']['server'];
880 }
881 if ( $context->getImageObj() ) {
882 // Output different headers if we're outputting textual errors.
883 if ( $errors ) {
884 header( 'Content-Type: text/plain; charset=utf-8' );
885 } else {
886 $context->getImageObj()->sendResponseHeaders( $context );
887 }
888 } elseif ( $context->getOnly() === 'styles' ) {
889 header( 'Content-Type: text/css; charset=utf-8' );
890 header( 'Access-Control-Allow-Origin: *' );
891 } else {
892 header( 'Content-Type: text/javascript; charset=utf-8' );
893 }
894 // See RFC 2616 § 14.19 ETag
895 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
896 header( 'ETag: ' . $etag );
897 if ( $context->getDebug() ) {
898 // Do not cache debug responses
899 header( 'Cache-Control: private, no-cache, must-revalidate' );
900 header( 'Pragma: no-cache' );
901 } else {
902 header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
903 $exp = min( $maxage, $smaxage );
904 header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
905 }
906 foreach ( $extra as $header ) {
907 header( $header );
908 }
909 }
910
922 // See RFC 2616 § 14.26 If-None-Match
923 // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
924 $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
925 // Never send 304s in debug mode
926 if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
927 // There's another bug in ob_gzhandler (see also the comment at
928 // the top of this function) that causes it to gzip even empty
929 // responses, meaning it's impossible to produce a truly empty
930 // response (because the gzip header is always there). This is
931 // a problem because 304 responses have to be completely empty
932 // per the HTTP spec, and Firefox behaves buggily when they're not.
933 // See also https://bugs.php.net/bug.php?id=51579
934 // To work around this, we tear down all output buffering before
935 // sending the 304.
936 wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
937
938 HttpStatus::header( 304 );
939
940 $this->sendResponseHeaders( $context, $etag, false );
941 return true;
942 }
943 return false;
944 }
945
954 protected function tryRespondFromFileCache(
955 ResourceFileCache $fileCache,
957 $etag
958 ) {
959 $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
960 // Buffer output to catch warnings.
961 ob_start();
962 // Get the maximum age the cache can be
963 $maxage = is_null( $context->getVersion() )
964 ? $rlMaxage['unversioned']['server']
965 : $rlMaxage['versioned']['server'];
966 // Minimum timestamp the cache file must have
967 $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
968 if ( !$good ) {
969 try { // RL always hits the DB on file cache miss...
971 } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
972 $good = $fileCache->isCacheGood(); // cache existence check
973 }
974 }
975 if ( $good ) {
976 $ts = $fileCache->cacheTimestamp();
977 // Send content type and cache headers
978 $this->sendResponseHeaders( $context, $etag, false );
979 $response = $fileCache->fetchText();
980 // Capture any PHP warnings from the output buffer and append them to the
981 // response in a comment if we're in debug mode.
982 if ( $context->getDebug() ) {
983 $warnings = ob_get_contents();
984 if ( strlen( $warnings ) ) {
985 $response = self::makeComment( $warnings ) . $response;
986 }
987 }
988 // Remove the output buffer and output the response
989 ob_end_clean();
990 echo $response . "\n/* Cached {$ts} */";
991 return true; // cache hit
992 }
993 // Clear buffer
994 ob_end_clean();
995
996 return false; // cache miss
997 }
998
1007 public static function makeComment( $text ) {
1008 $encText = str_replace( '*/', '* /', $text );
1009 return "/*\n$encText\n*/\n";
1010 }
1011
1018 public static function formatException( $e ) {
1019 return self::makeComment( self::formatExceptionNoComment( $e ) );
1020 }
1021
1029 protected static function formatExceptionNoComment( $e ) {
1031
1032 if ( !$wgShowExceptionDetails ) {
1033 return MWExceptionHandler::getPublicLogMessage( $e );
1034 }
1035
1036 return MWExceptionHandler::getLogMessage( $e ) .
1037 "\nBacktrace:\n" .
1038 MWExceptionHandler::getRedactedTraceAsString( $e );
1039 }
1040
1053 array $modules, array $missing = []
1054 ) {
1055 $out = '';
1056 $states = [];
1057
1058 if ( $modules === [] && $missing === [] ) {
1059 return <<<MESSAGE
1060/* This file is the Web entry point for MediaWiki's ResourceLoader:
1061 <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
1062 no modules were requested. Max made me put this here. */
1063MESSAGE;
1064 }
1065
1066 $image = $context->getImageObj();
1067 if ( $image ) {
1068 $data = $image->getImageData( $context );
1069 if ( $data === false ) {
1070 $data = '';
1071 $this->errors[] = 'Image generation failed';
1072 }
1073 return $data;
1074 }
1075
1076 foreach ( $missing as $name ) {
1077 $states[$name] = 'missing';
1078 }
1079
1080 $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js';
1081
1082 foreach ( $modules as $name => $module ) {
1083 try {
1084 $content = $module->getModuleContent( $context );
1085 $implementKey = $name . '@' . $module->getVersionHash( $context );
1086 $strContent = '';
1087
1088 if ( isset( $content['headers'] ) ) {
1089 $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
1090 }
1091
1092 // Append output
1093 switch ( $context->getOnly() ) {
1094 case 'scripts':
1095 $scripts = $content['scripts'];
1096 if ( is_string( $scripts ) ) {
1097 // Load scripts raw...
1098 $strContent = $scripts;
1099 } elseif ( is_array( $scripts ) ) {
1100 // ...except when $scripts is an array of URLs or an associative array
1101 $strContent = self::makeLoaderImplementScript(
1102 $context,
1103 $implementKey,
1104 $scripts,
1105 [],
1106 [],
1107 []
1108 );
1109 }
1110 break;
1111 case 'styles':
1112 $styles = $content['styles'];
1113 // We no longer separate into media, they are all combined now with
1114 // custom media type groups into @media .. {} sections as part of the css string.
1115 // Module returns either an empty array or a numerical array with css strings.
1116 $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
1117 break;
1118 default:
1119 $scripts = $content['scripts'] ?? '';
1120 if ( is_string( $scripts ) ) {
1121 if ( $name === 'site' || $name === 'user' ) {
1122 // Legacy scripts that run in the global scope without a closure.
1123 // mw.loader.implement will use globalEval if scripts is a string.
1124 // Minify manually here, because general response minification is
1125 // not effective due it being a string literal, not a function.
1126 if ( !$context->getDebug() ) {
1127 $scripts = self::filter( 'minify-js', $scripts ); // T107377
1128 }
1129 } else {
1130 $scripts = new XmlJsCode( $scripts );
1131 }
1132 }
1133 $strContent = self::makeLoaderImplementScript(
1134 $context,
1135 $implementKey,
1136 $scripts,
1137 $content['styles'] ?? [],
1138 isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
1139 $content['templates'] ?? []
1140 );
1141 break;
1142 }
1143
1144 if ( !$context->getDebug() ) {
1145 $strContent = self::filter( $filter, $strContent );
1146 } else {
1147 // In debug mode, separate each response by a new line.
1148 // For example, between 'mw.loader.implement();' statements.
1149 $strContent = $this->ensureNewline( $strContent );
1150 }
1151
1152 if ( $context->getOnly() === 'scripts' ) {
1153 // Use a linebreak between module scripts (T162719)
1154 $out .= $this->ensureNewline( $strContent );
1155 } else {
1156 $out .= $strContent;
1157 }
1158
1159 } catch ( Exception $e ) {
1160 $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
1161
1162 // Respond to client with error-state instead of module implementation
1163 $states[$name] = 'error';
1164 unset( $modules[$name] );
1165 }
1166 }
1167
1168 // Update module states
1169 if ( $context->shouldIncludeScripts() && !$context->getRaw() ) {
1170 if ( $modules && $context->getOnly() === 'scripts' ) {
1171 // Set the state of modules loaded as only scripts to ready as
1172 // they don't have an mw.loader.implement wrapper that sets the state
1173 foreach ( $modules as $name => $module ) {
1174 $states[$name] = 'ready';
1175 }
1176 }
1177
1178 // Set the state of modules we didn't respond to with mw.loader.implement
1179 if ( $states ) {
1180 $stateScript = self::makeLoaderStateScript( $context, $states );
1181 if ( !$context->getDebug() ) {
1182 $stateScript = self::filter( 'minify-js', $stateScript );
1183 }
1184 // Use a linebreak between module script and state script (T162719)
1185 $out = $this->ensureNewline( $out ) . $stateScript;
1186 }
1187 } elseif ( $states ) {
1188 $this->errors[] = 'Problematic modules: '
1189 . $context->encodeJson( $states );
1190 }
1191
1192 return $out;
1193 }
1194
1200 private function ensureNewline( $str ) {
1201 $end = substr( $str, -1 );
1202 if ( $end === false || $end === '' || $end === "\n" ) {
1203 return $str;
1204 }
1205 return $str . "\n";
1206 }
1207
1214 public function getModulesByMessage( $messageKey ) {
1215 $moduleNames = [];
1216 foreach ( $this->getModuleNames() as $moduleName ) {
1217 $module = $this->getModule( $moduleName );
1218 if ( in_array( $messageKey, $module->getMessages() ) ) {
1219 $moduleNames[] = $moduleName;
1220 }
1221 }
1222 return $moduleNames;
1223 }
1224
1243 private static function makeLoaderImplementScript(
1244 ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates
1245 ) {
1246 if ( $scripts instanceof XmlJsCode ) {
1247 if ( $scripts->value === '' ) {
1248 $scripts = null;
1249 } elseif ( $context->getDebug() ) {
1250 $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1251 } else {
1252 $scripts = new XmlJsCode( 'function($,jQuery,require,module){' . $scripts->value . '}' );
1253 }
1254 } elseif ( is_array( $scripts ) && isset( $scripts['files'] ) ) {
1255 $files = $scripts['files'];
1256 foreach ( $files as $path => &$file ) {
1257 // $file is changed (by reference) from a descriptor array to the content of the file
1258 // All of these essentially do $file = $file['content'];, some just have wrapping around it
1259 if ( $file['type'] === 'script' ) {
1260 // Multi-file modules only get two parameters ($ and jQuery are being phased out)
1261 if ( $context->getDebug() ) {
1262 $file = new XmlJsCode( "function ( require, module ) {\n{$file['content']}\n}" );
1263 } else {
1264 $file = new XmlJsCode( 'function(require,module){' . $file['content'] . '}' );
1265 }
1266 } else {
1267 $file = $file['content'];
1268 }
1269 }
1270 $scripts = XmlJsCode::encodeObject( [
1271 'main' => $scripts['main'],
1272 'files' => XmlJsCode::encodeObject( $files, $context->getDebug() )
1273 ], $context->getDebug() );
1274 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1275 throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
1276 }
1277
1278 // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
1279 // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
1280 // of "{}". Force them to objects.
1281 $module = [
1282 $name,
1283 $scripts,
1284 (object)$styles,
1285 (object)$messages,
1286 (object)$templates
1287 ];
1288 self::trimArray( $module );
1289
1290 return Xml::encodeJsCall( 'mw.loader.implement', $module, $context->getDebug() );
1291 }
1292
1299 public static function makeMessageSetScript( $messages ) {
1300 return 'mw.messages.set('
1301 . self::encodeJsonForScript( (object)$messages )
1302 . ');';
1303 }
1304
1312 public static function makeCombinedStyles( array $stylePairs ) {
1313 $out = [];
1314 foreach ( $stylePairs as $media => $styles ) {
1315 // ResourceLoaderFileModule::getStyle can return the styles
1316 // as a string or an array of strings. This is to allow separation in
1317 // the front-end.
1318 $styles = (array)$styles;
1319 foreach ( $styles as $style ) {
1320 $style = trim( $style );
1321 // Don't output an empty "@media print { }" block (T42498)
1322 if ( $style !== '' ) {
1323 // Transform the media type based on request params and config
1324 // The way that this relies on $wgRequest to propagate request params is slightly evil
1325 $media = OutputPage::transformCssMedia( $media );
1326
1327 if ( $media === '' || $media == 'all' ) {
1328 $out[] = $style;
1329 } elseif ( is_string( $media ) ) {
1330 $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
1331 }
1332 // else: skip
1333 }
1334 }
1335 }
1336 return $out;
1337 }
1338
1348 public static function encodeJsonForScript( $data ) {
1349 // Keep output as small as possible by disabling needless escape modes
1350 // that PHP uses by default.
1351 // However, while most module scripts are only served on HTTP responses
1352 // for JavaScript, some modules can also be embedded in the HTML as inline
1353 // scripts. This, and the fact that we sometimes need to export strings
1354 // containing user-generated content and labels that may genuinely contain
1355 // a sequences like "</script>", we need to encode either '/' or '<'.
1356 // By default PHP escapes '/'. Let's escape '<' instead which is less common
1357 // and allows URLs to mostly remain readable.
1358 $jsonFlags = JSON_UNESCAPED_SLASHES |
1359 JSON_UNESCAPED_UNICODE |
1360 JSON_HEX_TAG |
1361 JSON_HEX_AMP;
1362 if ( self::inDebugMode() ) {
1363 $jsonFlags |= JSON_PRETTY_PRINT;
1364 }
1365 return json_encode( $data, $jsonFlags );
1366 }
1367
1380 public static function makeLoaderStateScript(
1381 ResourceLoaderContext $context, array $states
1382 ) {
1383 return 'mw.loader.state('
1384 . $context->encodeJson( $states )
1385 . ');';
1387
1388 private static function isEmptyObject( stdClass $obj ) {
1389 foreach ( $obj as $key => $value ) {
1390 return false;
1391 }
1392 return true;
1393 }
1394
1407 private static function trimArray( array &$array ) {
1408 $i = count( $array );
1409 while ( $i-- ) {
1410 if ( $array[$i] === null
1411 || $array[$i] === []
1412 || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
1413 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1414 ) {
1415 unset( $array[$i] );
1416 } else {
1417 break;
1418 }
1419 }
1420 }
1421
1447 public static function makeLoaderRegisterScript(
1449 ) {
1450 // Optimisation: Transform dependency names into indexes when possible
1451 // to produce smaller output. They are expanded by mw.loader.register on
1452 // the other end using resolveIndexedDependencies().
1453 $index = [];
1454 foreach ( $modules as $i => &$module ) {
1455 // Build module name index
1456 $index[$module[0]] = $i;
1457 }
1458 foreach ( $modules as &$module ) {
1459 if ( isset( $module[2] ) ) {
1460 foreach ( $module[2] as &$dependency ) {
1461 if ( isset( $index[$dependency] ) ) {
1462 // Replace module name in dependency list with index
1463 $dependency = $index[$dependency];
1464 }
1465 }
1466 }
1467 }
1468
1469 array_walk( $modules, [ self::class, 'trimArray' ] );
1470
1471 return 'mw.loader.register('
1472 . $context->encodeJson( $modules )
1473 . ');';
1474 }
1475
1490 public static function makeLoaderSourcesScript(
1491 ResourceLoaderContext $context, array $sources
1492 ) {
1493 return 'mw.loader.addSource('
1494 . $context->encodeJson( $sources )
1495 . ');';
1496 }
1497
1504 public static function makeLoaderConditionalScript( $script ) {
1505 // Adds a function to lazy-created RLQ
1506 return '(RLQ=window.RLQ||[]).push(function(){' .
1507 trim( $script ) . '});';
1508 }
1509
1518 public static function makeInlineCodeWithModule( $modules, $script ) {
1519 // Adds an array to lazy-created RLQ
1520 return '(RLQ=window.RLQ||[]).push(['
1521 . self::encodeJsonForScript( $modules ) . ','
1522 . 'function(){' . trim( $script ) . '}'
1523 . ']);';
1524 }
1525
1537 public static function makeInlineScript( $script, $nonce = null ) {
1538 $js = self::makeLoaderConditionalScript( $script );
1539 $escNonce = '';
1540 if ( $nonce === null ) {
1541 wfWarn( __METHOD__ . " did not get nonce. Will break CSP" );
1542 } elseif ( $nonce !== false ) {
1543 // If it was false, CSP is disabled, so no nonce attribute.
1544 // Nonce should be only base64 characters, so should be safe,
1545 // but better to be safely escaped than sorry.
1546 $escNonce = ' nonce="' . htmlspecialchars( $nonce ) . '"';
1547 }
1548
1549 return new WrappedString(
1550 Html::inlineScript( $js, $nonce ),
1551 "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1552 '});</script>'
1553 );
1554 }
1555
1564 public static function makeConfigSetScript( array $configuration ) {
1565 $json = self::encodeJsonForScript( $configuration );
1566 if ( $json === false ) {
1567 $e = new Exception(
1568 'JSON serialization of config data failed. ' .
1569 'This usually means the config data is not valid UTF-8.'
1570 );
1572 return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) . ');';
1573 }
1574 return "mw.config.set($json);";
1575 }
1576
1590 public static function makePackedModulesString( $modules ) {
1591 $moduleMap = []; // [ prefix => [ suffixes ] ]
1592 foreach ( $modules as $module ) {
1593 $pos = strrpos( $module, '.' );
1594 $prefix = $pos === false ? '' : substr( $module, 0, $pos );
1595 $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
1596 $moduleMap[$prefix][] = $suffix;
1597 }
1598
1599 $arr = [];
1600 foreach ( $moduleMap as $prefix => $suffixes ) {
1601 $p = $prefix === '' ? '' : $prefix . '.';
1602 $arr[] = $p . implode( ',', $suffixes );
1603 }
1604 return implode( '|', $arr );
1605 }
1606
1618 public static function expandModuleNames( $modules ) {
1619 $retval = [];
1620 $exploded = explode( '|', $modules );
1621 foreach ( $exploded as $group ) {
1622 if ( strpos( $group, ',' ) === false ) {
1623 // This is not a set of modules in foo.bar,baz notation
1624 // but a single module
1625 $retval[] = $group;
1626 } else {
1627 // This is a set of modules in foo.bar,baz notation
1628 $pos = strrpos( $group, '.' );
1629 if ( $pos === false ) {
1630 // Prefixless modules, i.e. without dots
1631 $retval = array_merge( $retval, explode( ',', $group ) );
1632 } else {
1633 // We have a prefix and a bunch of suffixes
1634 $prefix = substr( $group, 0, $pos ); // 'foo'
1635 $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
1636 foreach ( $suffixes as $suffix ) {
1637 $retval[] = "$prefix.$suffix";
1638 }
1639 }
1640 }
1641 }
1642 return $retval;
1643 }
1644
1650 public static function inDebugMode() {
1651 if ( self::$debugMode === null ) {
1653 self::$debugMode = $wgRequest->getFuzzyBool( 'debug',
1654 $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug )
1655 );
1656 }
1657 return self::$debugMode;
1658 }
1659
1670 public static function clearCache() {
1671 self::$debugMode = null;
1672 }
1673
1684 $extraQuery = []
1685 ) {
1686 $query = self::createLoaderQuery( $context, $extraQuery );
1687 $script = $this->getLoadScript( $source );
1688
1689 return wfAppendQuery( $script, $query );
1690 }
1691
1701 protected static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = [] ) {
1702 return self::makeLoaderQuery(
1703 $context->getModules(),
1704 $context->getLanguage(),
1705 $context->getSkin(),
1706 $context->getUser(),
1707 $context->getVersion(),
1708 $context->getDebug(),
1709 $context->getOnly(),
1710 $context->getRequest()->getBool( 'printable' ),
1711 $context->getRequest()->getBool( 'handheld' ),
1712 $extraQuery
1713 );
1714 }
1715
1732 public static function makeLoaderQuery( $modules, $lang, $skin, $user = null,
1733 $version = null, $debug = false, $only = null, $printable = false,
1734 $handheld = false, $extraQuery = []
1735 ) {
1736 $query = [
1737 'modules' => self::makePackedModulesString( $modules ),
1738 ];
1739 // Keep urls short by omitting query parameters that
1740 // match the defaults assumed by ResourceLoaderContext.
1741 // Note: This relies on the defaults either being insignificant or forever constant,
1742 // as otherwise cached urls could change in meaning when the defaults change.
1744 $query['lang'] = $lang;
1745 }
1746 if ( $skin !== ResourceLoaderContext::DEFAULT_SKIN ) {
1747 $query['skin'] = $skin;
1748 }
1749 if ( $debug === true ) {
1750 $query['debug'] = 'true';
1751 }
1752 if ( $user !== null ) {
1753 $query['user'] = $user;
1754 }
1755 if ( $version !== null ) {
1756 $query['version'] = $version;
1757 }
1758 if ( $only !== null ) {
1759 $query['only'] = $only;
1760 }
1761 if ( $printable ) {
1762 $query['printable'] = 1;
1763 }
1764 if ( $handheld ) {
1765 $query['handheld'] = 1;
1766 }
1767 $query += $extraQuery;
1768
1769 // Make queries uniform in order
1770 ksort( $query );
1771 return $query;
1772 }
1773
1783 public static function isValidModuleName( $moduleName ) {
1784 return strcspn( $moduleName, '!,|', 0, 255 ) === strlen( $moduleName );
1785 }
1786
1797 public function getLessCompiler( $vars = [] ) {
1798 global $IP;
1799 // When called from the installer, it is possible that a required PHP extension
1800 // is missing (at least for now; see T49564). If this is the case, throw an
1801 // exception (caught by the installer) to prevent a fatal error later on.
1802 if ( !class_exists( 'Less_Parser' ) ) {
1803 throw new MWException( 'MediaWiki requires the less.php parser' );
1804 }
1805
1806 $parser = new Less_Parser;
1807 $parser->ModifyVars( $vars );
1808 $parser->SetImportDirs( [
1809 "$IP/resources/src/mediawiki.less/" => '',
1810 ] );
1811 $parser->SetOption( 'relativeUrls', false );
1812
1813 return $parser;
1814 }
1815
1823 public function getLessVars() {
1824 wfDeprecated( __METHOD__, '1.32' );
1825 return [];
1826 }
1827}
$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...
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.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
$debug
Definition Setup.php:783
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:751
$IP
Definition WebStart.php:41
static minify( $css)
Removes whitespace from CSS data.
Definition CSSMin.php:540
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.
static minify( $s)
Returns minified JavaScript code.
static logException( $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log an exception 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 extractBasePaths( $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.
static makeLoaderSourcesScript(ResourceLoaderContext $context, array $sources)
Returns JS code which calls mw.loader.addSource() with the given parameters.
static makeInlineScript( $script, $nonce=null)
Returns an HTML script tag that runs given JS code after startup and base modules.
static expandModuleNames( $modules)
Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to an array of module names like ‘[ 'jq...
__construct(Config $config=null, LoggerInterface $logger=null)
Register core modules and runs registration hooks.
static formatExceptionNoComment( $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 (including subclasses).
getModuleNames()
Get a list of module names.
static makeLoaderConditionalScript( $script)
Wraps JavaScript code to run after the startup module.
static trimArray(array &$array)
Remove empty values from the end of an array.
setMessageBlobStore(MessageBlobStore $blobStore)
static inDebugMode()
Determine whether debug mode was requested Order of priority is 1) request param, 2) cookie,...
static makeConfigSetScript(array $configuration)
Returns JS code which will set the MediaWiki configuration array to the given value.
static makeCombinedStyles(array $stylePairs)
Combines an associative array mapping media type to CSS into a single stylesheet with "@media" blocks...
makeModuleResponse(ResourceLoaderContext $context, array $modules, array $missing=[])
Generate code for a response.
LoggerInterface $logger
setLogger(LoggerInterface $logger)
getLoadScript( $source)
Get the URL to the load.php endpoint for the given ResourceLoader source.
static makeLoaderRegisterScript(ResourceLoaderContext $context, array $modules)
Returns JS code which calls mw.loader.register with the given parameter.
static createLoaderQuery(ResourceLoaderContext $context, $extraQuery=[])
Helper for createLoaderURL()
sendResponseHeaders(ResourceLoaderContext $context, $etag, $errors, array $extra=[])
Send main response headers to the client.
getLessVars()
Get global LESS variables.
static bool $debugMode
static makeMessageSetScript( $messages)
Returns JS code which, when called, will register a given list of messages.
array[] $moduleInfos
Map of (module name => associative info array)
tryRespondNotModified(ResourceLoaderContext $context, $etag)
Respond with HTTP 304 Not Modified if appropiate.
static makeLoaderQuery( $modules, $lang, $skin, $user=null, $version=null, $debug=false, $only=null, $printable=false, $handheld=false, $extraQuery=[])
Build a query array (array representation of query string) for load.php.
getSources()
Get the list of sources.
addSource( $id, $loadUrl=null)
Add a foreign source of modules.
static makeInlineCodeWithModule( $modules, $script)
Wraps JavaScript code to run after a required module.
static clearCache()
Reset static members used for caching.
ensureNewline( $str)
Ensure the string is either empty or ends in a line break.
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' => [ ...
static makePackedModulesString( $modules)
Convert an array of module names to a packed query string.
getTestSuiteModuleNames()
Get a list of module names with QUnit test suites.
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)
createLoaderURL( $source, ResourceLoaderContext $context, $extraQuery=[])
Build a load.php URL.
static makeLoaderImplementScript(ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates)
Return JS code that calls mw.loader.implement with given module properties.
measureResponseTime(Timing $timing)
static formatException( $e)
Handle exception display.
getModulesByMessage( $messageKey)
Get names of modules that use a certain message.
static encodeJsonForScript( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
MessageBlobStore $blobStore
array $errors
Errors accumulated during current respond() call.
static makeComment( $text)
Generate a CSS or JS comment block.
getLessCompiler( $vars=[])
Returns LESS compiler set up for use with MediaWiki.
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 isValidModuleName( $moduleName)
Check a module name for validity.
static makeLoaderStateScript(ResourceLoaderContext $context, array $states)
Returns a JS call to mw.loader.state, which sets the state of modules to a given value:
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 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
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:677
const CACHE_ANYTHING
Definition Defines.php:90
Interface for configuration instances.
Definition Config.php:28
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
$context
Definition load.php:45
$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...
$filter
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:78
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
$header