91 const FILTER_NOMIN =
'/*@nomin*/';
108 if ( !$moduleNames ) {
117 $vary =
"$skin|$lang";
118 $res =
$dbr->select(
'module_deps', [
'md_module',
'md_deps' ], [
119 'md_module' => $moduleNames,
125 $modulesWithDeps = [];
126 foreach (
$res as $row ) {
127 $module = $this->
getModule( $row->md_module );
132 $modulesWithDeps[] = $row->md_module;
136 foreach ( array_diff( $moduleNames, $modulesWithDeps )
as $name ) {
139 $this->
getModule( $name )->setFileDependencies( $context, [] );
145 foreach ( $moduleNames
as $name ) {
147 if ( $module && $module->getMessages() ) {
153 foreach ( $blobs
as $name =>
$blob ) {
176 if ( strpos( $data, ResourceLoader::FILTER_NOMIN ) !==
false ) {
181 return self::applyFilter( $filter, $data );
191 self::$filterCacheVersion, md5( $data )
196 $stats->increment(
"resourceloader_cache.$filter.miss" );
197 $result = self::applyFilter( $filter, $data );
200 $stats->increment(
"resourceloader_cache.$filter.hit" );
211 $data = trim( $data );
214 $data = ( $filter ===
'minify-css' )
235 $this->logger =
$logger ?:
new NullLogger();
238 $this->logger->debug( __METHOD__ .
' was called without providing a Config instance' );
250 $this->
register( include
"$IP/resources/Resources.php" );
251 $this->
register( include
"$IP/resources/ResourcesOOUI.php" );
253 $this->
register(
$config->get(
'ResourceModules' ) );
257 Hooks::run(
'ResourceLoaderRegisterModules', [ &$rl ] );
259 if (
$config->get(
'EnableJavaScriptTest' ) ===
true ) {
318 public function register(
$name, $info = null ) {
322 foreach ( $registrations
as $name => $info ) {
324 if ( isset( $this->moduleInfos[
$name] ) ) {
326 $this->logger->warning(
327 'ResourceLoader duplicate registration warning. ' .
328 'Another module has already been registered as ' . $name
333 if ( !self::isValidModuleName( $name ) ) {
334 throw new MWException(
"ResourceLoader module name '$name' is invalid, "
335 .
"see ResourceLoader::isValidModuleName()" );
340 $this->moduleInfos[
$name] = [
'object' => $info ];
341 $info->setName( $name );
342 $this->modules[
$name] = $info;
343 } elseif ( is_array( $info ) ) {
345 $this->moduleInfos[
$name] = $info;
348 'ResourceLoader module info type error for module \'' . $name .
349 '\': expected ResourceLoaderModule
or array (got:
' . gettype( $info ) . ')
'
353 // Last-minute changes
355 // Apply custom skin-defined styles to existing modules.
356 if ( $this->isFileModule( $name ) ) {
357 foreach ( $this->config->get( 'ResourceModuleSkinStyles
' ) as $skinName => $skinStyles ) {
358 // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
359 if ( isset( $this->moduleInfos[$name]['skinStyles
'][$skinName] ) ) {
363 // If $name is preceded with a '+
', the defined style files will be added to 'default'
364 // skinStyles, otherwise 'default' will be ignored as it normally would be.
365 if ( isset( $skinStyles[$name] ) ) {
366 $paths = (array)$skinStyles[$name];
368 } elseif ( isset( $skinStyles['+
' . $name] ) ) {
369 $paths = (array)$skinStyles['+
' . $name];
370 $styleFiles = isset( $this->moduleInfos[$name]['skinStyles
']['default'] ) ?
371 (array)$this->moduleInfos[$name]['skinStyles
']['default'] :
377 // Add new file paths, remapping them to refer to our directories and not use settings
379 list( $localBasePath, $remoteBasePath ) =
386 $this->moduleInfos[
$name][
'skinStyles'][$skinName] = $styleFiles;
398 if ( $this->config->get(
'EnableJavaScriptTest' ) !==
true ) {
399 throw new MWException(
'Attempt to register JavaScript test modules '
400 .
'but <code>$wgEnableJavaScriptTest</code> is false. '
401 .
'Edit your <code>LocalSettings.php</code> to enable it.' );
406 $testModules[
'qunit'] = [];
410 Hooks::run(
'ResourceLoaderTestModules', [ &$testModules, &$rl ] );
414 foreach ( $testModules[
'qunit']
as &$module ) {
418 $module[
'position'] =
'top';
419 $module[
'dependencies'][] =
'test.mediawiki.qunit.testrunner';
422 $testModules[
'qunit'] =
423 ( include
"$IP/tests/qunit/QUnitTestResources.php" ) + $testModules[
'qunit'];
425 foreach ( $testModules
as $id => $names ) {
427 $this->
register( $testModules[$id] );
430 $this->testModuleNames[$id] = array_keys( $testModules[$id] );
447 if ( is_array( $id ) ) {
455 if ( isset( $this->sources[$id] ) ) {
457 'ResourceLoader duplicate source addition error. ' .
458 'Another source has already been registered as ' . $id
463 if ( is_array( $loadUrl ) ) {
464 if ( !isset( $loadUrl[
'loadScript'] ) ) {
466 __METHOD__ .
' was passed an array with no "loadScript" key.'
470 $loadUrl = $loadUrl[
'loadScript'];
473 $this->sources[$id] = $loadUrl;
482 return array_keys( $this->moduleInfos );
497 if ( $framework ==
'all' ) {
498 return $this->testModuleNames;
499 } elseif ( isset( $this->testModuleNames[$framework] )
500 && is_array( $this->testModuleNames[$framework] )
502 return $this->testModuleNames[$framework];
516 return isset( $this->moduleInfos[
$name] );
531 if ( !isset( $this->modules[
$name] ) ) {
532 if ( !isset( $this->moduleInfos[$name] ) ) {
537 $info = $this->moduleInfos[
$name];
539 if ( isset( $info[
'object'] ) ) {
541 $object = $info[
'object'];
543 if ( !isset( $info[
'class'] ) ) {
544 $class =
'ResourceLoaderFileModule';
546 $class = $info[
'class'];
549 $object =
new $class( $info );
550 $object->setConfig( $this->getConfig() );
551 $object->setLogger( $this->logger );
553 $object->setName( $name );
554 $this->modules[
$name] = $object;
557 return $this->modules[
$name];
567 if ( !isset( $this->moduleInfos[
$name] ) ) {
570 $info = $this->moduleInfos[
$name];
571 if ( isset( $info[
'object'] ) || isset( $info[
'class'] ) ) {
583 return $this->sources;
596 if ( !isset( $this->sources[
$source] ) ) {
597 throw new MWException(
"The $source source was never registered in ResourceLoader." );
599 return $this->sources[
$source];
611 return substr( base64_encode( sha1(
$value,
true ) ), 0, 8 );
626 $hashes = array_map(
function ( $module )
use ( $context ) {
627 return $this->getModule( $module )->getVersionHash( $context );
629 return self::makeHash( implode(
$hashes ) );
651 $module = $this->getModule(
$name );
655 if ( $module->getGroup() ===
'private' ) {
656 $this->logger->debug(
"Request for private module '$name' denied" );
657 $this->
errors[] =
"Cannot show private module \"$name\"";
660 $modules[
$name] = $module;
668 $this->preloadModuleInfo( array_keys( $modules ), $context );
671 $this->logger->warning(
'Preloading module info failed: {exception}', [
674 $this->
errors[] = self::formatExceptionNoComment( $e );
680 $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
683 $this->logger->warning(
'Calculating version hash failed: {exception}', [
686 $this->
errors[] = self::formatExceptionNoComment( $e );
691 $etag =
'W/"' . $versionHash .
'"';
694 if ( $this->tryRespondNotModified( $context, $etag ) ) {
699 if ( $this->config->get(
'UseFileCache' ) ) {
701 if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
707 $response = $this->makeModuleResponse( $context, $modules, $missing );
712 $warnings = ob_get_contents();
713 if ( strlen( $warnings ) ) {
714 $this->
errors[] = $warnings;
719 if ( isset( $fileCache ) && !$this->
errors && !count( $missing ) ) {
722 if ( $fileCache->isCacheWorthy() ) {
725 $fileCache->incrMissesRecent( $context->
getRequest() );
730 $this->sendResponseHeaders( $context, $etag, (
bool)$this->
errors );
737 $response = implode(
"\n\n", $this->errors );
738 } elseif ( $this->errors ) {
739 $errorText = implode(
"\n\n", $this->errors );
740 $errorResponse = self::makeComment( $errorText );
742 $errorResponse .=
'if (window.console && console.error) {'
767 $rlMaxage = $this->config->get(
'ResourceLoaderMaxage' );
771 if ( is_null( $context->
getVersion() ) || $errors ) {
772 $maxage = $rlMaxage[
'unversioned'][
'client'];
773 $smaxage = $rlMaxage[
'unversioned'][
'server'];
777 $maxage = $rlMaxage[
'versioned'][
'client'];
778 $smaxage = $rlMaxage[
'versioned'][
'server'];
783 header(
'Content-Type: text/plain; charset=utf-8' );
785 $context->
getImageObj()->sendResponseHeaders( $context );
787 } elseif ( $context->
getOnly() ===
'styles' ) {
788 header(
'Content-Type: text/css; charset=utf-8' );
789 header(
'Access-Control-Allow-Origin: *' );
791 header(
'Content-Type: text/javascript; charset=utf-8' );
795 header(
'ETag: ' . $etag );
798 header(
'Cache-Control: private, no-cache, must-revalidate' );
799 header(
'Pragma: no-cache' );
801 header(
"Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
802 $exp = min( $maxage, $smaxage );
822 if ( $clientKeys !==
false && !$context->
getDebug() && in_array( $etag, $clientKeys ) ) {
836 $this->sendResponseHeaders( $context, $etag,
false );
855 $rlMaxage = $this->config->get(
'ResourceLoaderMaxage' );
860 ? $rlMaxage[
'unversioned'][
'server']
861 : $rlMaxage[
'versioned'][
'server'];
874 $this->sendResponseHeaders( $context, $etag,
false );
879 $warnings = ob_get_contents();
880 if ( strlen( $warnings ) ) {
904 $encText = str_replace(
'*/',
'* /', $text );
905 return "/*\n$encText\n*/\n";
915 return self::makeComment( self::formatExceptionNoComment(
$e ) );
926 global $wgShowExceptionDetails;
928 if ( !$wgShowExceptionDetails ) {
949 if ( !count( $modules ) && !count( $missing ) ) {
959 $data =
$image->getImageData( $context );
960 if ( $data ===
false ) {
962 $this->
errors[] =
'Image generation failed';
967 foreach ( $missing
as $name ) {
968 $states[
$name] =
'missing';
974 $filter = $context->
getOnly() ===
'styles' ?
'minify-css' :
'minify-js';
976 foreach ( $modules
as $name => $module ) {
978 $content = $module->getModuleContent( $context );
982 switch ( $context->
getOnly() ) {
985 if ( is_string( $scripts ) ) {
987 $strContent = $scripts;
988 } elseif ( is_array( $scripts ) ) {
990 $strContent = self::makeLoaderImplementScript( $name, $scripts, [], [], [] );
998 $strContent = isset( $styles[
'css'] ) ? implode(
'', $styles[
'css'] ) :
'';
1001 $strContent = self::makeLoaderImplementScript(
1004 isset( $content[
'styles'] ) ? $content[
'styles'] : [],
1005 isset( $content[
'messagesBlob'] ) ?
new XmlJsCode( $content[
'messagesBlob'] ) : [],
1006 isset( $content[
'templates'] ) ? $content[
'templates'] : []
1012 $strContent = self::filter( $filter, $strContent );
1015 $out .= $strContent;
1019 $this->logger->warning(
'Generating module package failed: {exception}', [
1022 $this->
errors[] = self::formatExceptionNoComment( $e );
1025 $states[
$name] =
'error';
1026 unset( $modules[$name] );
1028 $isRaw |= $module->isRaw();
1033 if ( count( $modules ) && $context->
getOnly() ===
'scripts' ) {
1036 foreach ( $modules
as $name => $module ) {
1037 $states[
$name] =
'ready';
1042 if ( count( $states ) ) {
1043 $stateScript = self::makeLoaderStateScript( $states );
1045 $stateScript = self::filter(
'minify-js', $stateScript );
1047 $out .= $stateScript;
1050 if ( count( $states ) ) {
1051 $this->
errors[] =
'Problematic modules: ' .
1065 public function getModulesByMessage( $messageKey ) {
1067 foreach ( $this->getModuleNames()
as $moduleName ) {
1068 $module = $this->getModule( $moduleName );
1069 if ( in_array( $messageKey, $module->getMessages() ) ) {
1070 $moduleNames[] = $moduleName;
1073 return $moduleNames;
1093 public static function makeLoaderImplementScript(
1094 $name, $scripts, $styles, $messages, $templates
1096 if ( is_string( $scripts ) ) {
1099 if (
$name ===
'site' ||
$name ===
'user' ) {
1103 $scripts = self::filter(
'minify-js', $scripts );
1106 $scripts =
new XmlJsCode(
"function ( $, jQuery, require, module ) {\n{$scripts}\n}" );
1108 } elseif ( !is_array( $scripts ) ) {
1109 throw new MWException(
'Invalid scripts error. Array of URLs or string of code expected.' );
1121 self::trimArray( $module );
1133 public static function makeMessageSetScript( $messages ) {
1136 [ (
object)$messages ],
1148 public static function makeCombinedStyles(
array $stylePairs ) {
1150 foreach ( $stylePairs
as $media => $styles ) {
1154 $styles = (
array)$styles;
1155 foreach ( $styles
as $style ) {
1156 $style = trim( $style );
1158 if ( $style !==
'' ) {
1163 if ( $media ===
'' || $media ==
'all' ) {
1165 } elseif ( is_string( $media ) ) {
1166 $out[] =
"@media $media {\n" . str_replace(
"\n",
"\n\t",
"\t" . $style ) .
"}";
1189 public static function makeLoaderStateScript(
$name, $state = null ) {
1190 if ( is_array(
$name ) ) {
1219 public static function makeCustomLoaderScript(
$name,
$version, $dependencies,
1222 $script = str_replace(
"\n",
"\n\t", trim( $script ) );
1224 "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
1230 private static function isEmptyObject( stdClass $obj ) {
1249 private static function trimArray(
array &$array ) {
1250 $i = count( $array );
1252 if ( $array[$i] === null
1253 || $array[$i] === []
1254 || ( $array[$i] instanceof
XmlJsCode && $array[$i]->value ===
'{}' )
1255 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1257 unset( $array[$i] );
1291 public static function makeLoaderRegisterScript(
$name,
$version = null,
1292 $dependencies = null, $group = null,
$source = null, $skip = null
1294 if ( is_array(
$name ) ) {
1297 foreach (
$name as $i => &$module ) {
1298 $index[$module[0]] = $i;
1303 foreach (
$name as &$module ) {
1304 if ( isset( $module[2] ) ) {
1305 foreach ( $module[2]
as &$dependency ) {
1306 if ( isset( $index[$dependency] ) ) {
1307 $dependency = $index[$dependency];
1313 array_walk(
$name, [
'self',
'trimArray' ] );
1316 'mw.loader.register',
1322 self::trimArray( $registration );
1324 'mw.loader.register',
1345 public static function makeLoaderSourcesScript( $id, $properties = null ) {
1346 if ( is_array( $id ) ) {
1348 'mw.loader.addSource',
1354 'mw.loader.addSource',
1355 [ $id, $properties ],
1369 public static function makeLoaderConditionalScript( $script ) {
1370 return '(window.RLQ=window.RLQ||[]).push(function(){' .
1371 trim( $script ) .
'});';
1383 public static function makeInlineScript( $script ) {
1384 $js = self::makeLoaderConditionalScript( $script );
1385 return new WrappedString(
1387 '<script>(window.RLQ=window.RLQ||[]).push(function(){',
1399 public static function makeConfigSetScript(
array $configuration ) {
1415 public static function makePackedModulesString( $modules ) {
1417 foreach ( $modules
as $module ) {
1418 $pos = strrpos( $module,
'.' );
1419 $prefix = $pos ===
false ?
'' : substr( $module, 0, $pos );
1420 $suffix = $pos ===
false ? $module : substr( $module, $pos + 1 );
1421 $groups[$prefix][] = $suffix;
1425 foreach ( $groups
as $prefix => $suffixes ) {
1426 $p = $prefix ===
'' ?
'' : $prefix .
'.';
1427 $arr[] = $p . implode(
',', $suffixes );
1429 $str = implode(
'|', $arr );
1438 public static function inDebugMode() {
1439 if ( self::$debugMode === null ) {
1441 self::$debugMode = $wgRequest->getFuzzyBool(
'debug',
1442 $wgRequest->getCookie(
'resourceLoaderDebug',
'', $wgResourceLoaderDebug )
1445 return self::$debugMode;
1455 public static function clearCache() {
1456 self::$debugMode = null;
1471 $query = self::createLoaderQuery( $context, $extraQuery );
1472 $script = $this->getLoadScript(
$source );
1492 public static function makeLoaderURL( $modules,
$lang,
$skin,
$user = null,
1493 $version = null,
$debug =
false, $only = null, $printable =
false,
1494 $handheld =
false, $extraQuery = []
1499 $only, $printable, $handheld, $extraQuery
1515 return self::makeLoaderQuery(
1523 $context->
getRequest()->getBool(
'printable' ),
1524 $context->
getRequest()->getBool(
'handheld' ),
1546 public static function makeLoaderQuery( $modules,
$lang,
$skin,
$user = null,
1547 $version = null,
$debug =
false, $only = null, $printable =
false,
1548 $handheld =
false, $extraQuery = []
1551 'modules' => self::makePackedModulesString( $modules ),
1554 'debug' =>
$debug ?
'true' :
'false',
1556 if (
$user !== null ) {
1562 if ( $only !== null ) {
1587 public static function isValidModuleName( $moduleName ) {
1588 return strcspn( $moduleName,
'!,|', 0, 255 ) === strlen( $moduleName );
1600 public function getLessCompiler( $extraVars = [] ) {
1604 if ( !class_exists(
'Less_Parser' ) ) {
1605 throw new MWException(
'MediaWiki requires the less.php parser' );
1609 $parser->ModifyVars( array_merge( $this->getLessVars(), $extraVars ) );
1611 array_fill_keys( $this->config->get(
'ResourceLoaderLESSImportPaths' ),
'' )
1613 $parser->SetOption(
'relativeUrls',
false );
1614 $parser->SetCacheDir( $this->config->get(
'CacheDirectory' ) ?:
wfTempDir() );
1625 public function getLessVars() {
1626 if ( !$this->lessVars ) {
1627 $lessVars = $this->config->get(
'ResourceLoaderLESSVars' );
1628 Hooks::run(
'ResourceLoaderGetLessVars', [ &$lessVars ] );
1629 $this->lessVars = $lessVars;
1631 return $this->lessVars;
const TS_RFC2822
RFC 2822 format, for E-mail and HTTP headers.
#define the
table suitable for use with IDatabase::select()
This class generates message blobs for use by ResourceLoader modules.
getModuleNames()
Get a list of module names.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
array $modules
Module name/ResourceLoaderModule object pairs.
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
static inlineScript($contents)
Output a "