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' ) );
254 Hooks::run(
'ResourceLoaderRegisterModules', [ &$this ] );
256 if (
$config->get(
'EnableJavaScriptTest' ) ===
true ) {
315 public function register(
$name, $info = null ) {
319 foreach ( $registrations
as $name => $info ) {
321 if ( isset( $this->moduleInfos[
$name] ) ) {
323 $this->logger->warning(
324 'ResourceLoader duplicate registration warning. ' .
325 'Another module has already been registered as ' . $name
330 if ( !self::isValidModuleName( $name ) ) {
331 throw new MWException(
"ResourceLoader module name '$name' is invalid, "
332 .
"see ResourceLoader::isValidModuleName()" );
337 $this->moduleInfos[
$name] = [
'object' => $info ];
338 $info->setName( $name );
339 $this->modules[
$name] = $info;
340 } elseif ( is_array( $info ) ) {
342 $this->moduleInfos[
$name] = $info;
345 'ResourceLoader module info type error for module \'' . $name .
346 '\': expected ResourceLoaderModule
or array (got:
' . gettype( $info ) . ')
'
350 // Last-minute changes
352 // Apply custom skin-defined styles to existing modules.
353 if ( $this->isFileModule( $name ) ) {
354 foreach ( $this->config->get( 'ResourceModuleSkinStyles
' ) as $skinName => $skinStyles ) {
355 // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
356 if ( isset( $this->moduleInfos[$name]['skinStyles
'][$skinName] ) ) {
360 // If $name is preceded with a '+
', the defined style files will be added to 'default'
361 // skinStyles, otherwise 'default' will be ignored as it normally would be.
362 if ( isset( $skinStyles[$name] ) ) {
363 $paths = (array)$skinStyles[$name];
365 } elseif ( isset( $skinStyles['+
' . $name] ) ) {
366 $paths = (array)$skinStyles['+
' . $name];
367 $styleFiles = isset( $this->moduleInfos[$name]['skinStyles
']['default'] ) ?
368 (array)$this->moduleInfos[$name]['skinStyles
']['default'] :
374 // Add new file paths, remapping them to refer to our directories and not use settings
376 list( $localBasePath, $remoteBasePath ) =
383 $this->moduleInfos[
$name][
'skinStyles'][$skinName] = $styleFiles;
395 if ( $this->config->get(
'EnableJavaScriptTest' ) !==
true ) {
396 throw new MWException(
'Attempt to register JavaScript test modules '
397 .
'but <code>$wgEnableJavaScriptTest</code> is false. '
398 .
'Edit your <code>LocalSettings.php</code> to enable it.' );
403 $testModules[
'qunit'] = [];
405 Hooks::run(
'ResourceLoaderTestModules', [ &$testModules, &$this ] );
409 foreach ( $testModules[
'qunit']
as &$module ) {
413 $module[
'position'] =
'top';
414 $module[
'dependencies'][] =
'test.mediawiki.qunit.testrunner';
417 $testModules[
'qunit'] =
418 ( include
"$IP/tests/qunit/QUnitTestResources.php" ) + $testModules[
'qunit'];
420 foreach ( $testModules
as $id => $names ) {
422 $this->
register( $testModules[$id] );
425 $this->testModuleNames[$id] = array_keys( $testModules[$id] );
442 if ( is_array( $id ) ) {
450 if ( isset( $this->sources[$id] ) ) {
452 'ResourceLoader duplicate source addition error. ' .
453 'Another source has already been registered as ' . $id
458 if ( is_array( $loadUrl ) ) {
459 if ( !isset( $loadUrl[
'loadScript'] ) ) {
461 __METHOD__ .
' was passed an array with no "loadScript" key.'
465 $loadUrl = $loadUrl[
'loadScript'];
468 $this->sources[$id] = $loadUrl;
477 return array_keys( $this->moduleInfos );
492 if ( $framework ==
'all' ) {
493 return $this->testModuleNames;
494 } elseif ( isset( $this->testModuleNames[$framework] )
495 && is_array( $this->testModuleNames[$framework] )
497 return $this->testModuleNames[$framework];
511 return isset( $this->moduleInfos[
$name] );
526 if ( !isset( $this->modules[
$name] ) ) {
527 if ( !isset( $this->moduleInfos[$name] ) ) {
532 $info = $this->moduleInfos[
$name];
534 if ( isset( $info[
'object'] ) ) {
536 $object = $info[
'object'];
538 if ( !isset( $info[
'class'] ) ) {
539 $class =
'ResourceLoaderFileModule';
541 $class = $info[
'class'];
544 $object =
new $class( $info );
545 $object->setConfig( $this->getConfig() );
546 $object->setLogger( $this->logger );
548 $object->setName( $name );
549 $this->modules[
$name] = $object;
552 return $this->modules[
$name];
562 if ( !isset( $this->moduleInfos[
$name] ) ) {
565 $info = $this->moduleInfos[
$name];
566 if ( isset( $info[
'object'] ) || isset( $info[
'class'] ) ) {
578 return $this->sources;
591 if ( !isset( $this->sources[
$source] ) ) {
592 throw new MWException(
"The $source source was never registered in ResourceLoader." );
594 return $this->sources[
$source];
606 return substr( base64_encode( sha1(
$value,
true ) ), 0, 8 );
621 $hashes = array_map(
function ( $module )
use ( $context ) {
622 return $this->getModule( $module )->getVersionHash( $context );
624 return self::makeHash( implode(
$hashes ) );
646 $module = $this->getModule(
$name );
650 if ( $module->getGroup() ===
'private' ) {
651 $this->logger->debug(
"Request for private module '$name' denied" );
652 $this->
errors[] =
"Cannot show private module \"$name\"";
655 $modules[
$name] = $module;
663 $this->preloadModuleInfo( array_keys( $modules ), $context );
666 $this->logger->warning(
'Preloading module info failed: {exception}', [
669 $this->
errors[] = self::formatExceptionNoComment( $e );
675 $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
678 $this->logger->warning(
'Calculating version hash failed: {exception}', [
681 $this->
errors[] = self::formatExceptionNoComment( $e );
686 $etag =
'W/"' . $versionHash .
'"';
689 if ( $this->tryRespondNotModified( $context, $etag ) ) {
694 if ( $this->config->get(
'UseFileCache' ) ) {
696 if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
702 $response = $this->makeModuleResponse( $context, $modules, $missing );
707 $warnings = ob_get_contents();
708 if ( strlen( $warnings ) ) {
709 $this->
errors[] = $warnings;
714 if ( isset( $fileCache ) && !$this->
errors && !count( $missing ) ) {
717 if ( $fileCache->isCacheWorthy() ) {
720 $fileCache->incrMissesRecent( $context->
getRequest() );
725 $this->sendResponseHeaders( $context, $etag, (
bool)$this->
errors );
732 $response = implode(
"\n\n", $this->errors );
733 } elseif ( $this->errors ) {
734 $errorText = implode(
"\n\n", $this->errors );
735 $errorResponse = self::makeComment( $errorText );
737 $errorResponse .=
'if (window.console && console.error) {'
762 $rlMaxage = $this->config->get(
'ResourceLoaderMaxage' );
766 if ( is_null( $context->
getVersion() ) || $errors ) {
767 $maxage = $rlMaxage[
'unversioned'][
'client'];
768 $smaxage = $rlMaxage[
'unversioned'][
'server'];
772 $maxage = $rlMaxage[
'versioned'][
'client'];
773 $smaxage = $rlMaxage[
'versioned'][
'server'];
778 header(
'Content-Type: text/plain; charset=utf-8' );
780 $context->
getImageObj()->sendResponseHeaders( $context );
782 } elseif ( $context->
getOnly() ===
'styles' ) {
783 header(
'Content-Type: text/css; charset=utf-8' );
784 header(
'Access-Control-Allow-Origin: *' );
786 header(
'Content-Type: text/javascript; charset=utf-8' );
790 header(
'ETag: ' . $etag );
793 header(
'Cache-Control: private, no-cache, must-revalidate' );
794 header(
'Pragma: no-cache' );
796 header(
"Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
797 $exp = min( $maxage, $smaxage );
817 if ( $clientKeys !==
false && !$context->
getDebug() && in_array( $etag, $clientKeys ) ) {
831 $this->sendResponseHeaders( $context, $etag,
false );
850 $rlMaxage = $this->config->get(
'ResourceLoaderMaxage' );
855 ? $rlMaxage[
'unversioned'][
'server']
856 : $rlMaxage[
'versioned'][
'server'];
869 $this->sendResponseHeaders( $context, $etag,
false );
874 $warnings = ob_get_contents();
875 if ( strlen( $warnings ) ) {
899 $encText = str_replace(
'*/',
'* /', $text );
900 return "/*\n$encText\n*/\n";
910 return self::makeComment( self::formatExceptionNoComment(
$e ) );
921 global $wgShowExceptionDetails;
923 if ( !$wgShowExceptionDetails ) {
944 if ( !count( $modules ) && !count( $missing ) ) {
954 $data =
$image->getImageData( $context );
955 if ( $data ===
false ) {
957 $this->
errors[] =
'Image generation failed';
962 foreach ( $missing
as $name ) {
963 $states[
$name] =
'missing';
969 $filter = $context->
getOnly() ===
'styles' ?
'minify-css' :
'minify-js';
971 foreach ( $modules
as $name => $module ) {
973 $content = $module->getModuleContent( $context );
977 switch ( $context->
getOnly() ) {
980 if ( is_string( $scripts ) ) {
982 $strContent = $scripts;
983 } elseif ( is_array( $scripts ) ) {
985 $strContent = self::makeLoaderImplementScript( $name, $scripts, [], [], [] );
993 $strContent = isset( $styles[
'css'] ) ? implode(
'', $styles[
'css'] ) :
'';
996 $strContent = self::makeLoaderImplementScript(
999 isset( $content[
'styles'] ) ? $content[
'styles'] : [],
1000 isset( $content[
'messagesBlob'] ) ?
new XmlJsCode( $content[
'messagesBlob'] ) : [],
1001 isset( $content[
'templates'] ) ? $content[
'templates'] : []
1007 $strContent = self::filter( $filter, $strContent );
1010 $out .= $strContent;
1014 $this->logger->warning(
'Generating module package failed: {exception}', [
1017 $this->
errors[] = self::formatExceptionNoComment( $e );
1020 $states[
$name] =
'error';
1021 unset( $modules[$name] );
1023 $isRaw |= $module->isRaw();
1028 if ( count( $modules ) && $context->
getOnly() ===
'scripts' ) {
1031 foreach ( $modules
as $name => $module ) {
1032 $states[
$name] =
'ready';
1037 if ( count( $states ) ) {
1038 $stateScript = self::makeLoaderStateScript( $states );
1040 $stateScript = self::filter(
'minify-js', $stateScript );
1042 $out .= $stateScript;
1045 if ( count( $states ) ) {
1046 $this->
errors[] =
'Problematic modules: ' .
1060 public function getModulesByMessage( $messageKey ) {
1062 foreach ( $this->getModuleNames()
as $moduleName ) {
1063 $module = $this->getModule( $moduleName );
1064 if ( in_array( $messageKey, $module->getMessages() ) ) {
1065 $moduleNames[] = $moduleName;
1068 return $moduleNames;
1088 public static function makeLoaderImplementScript(
1089 $name, $scripts, $styles, $messages, $templates
1091 if ( is_string( $scripts ) ) {
1094 if (
$name ===
'site' ||
$name ===
'user' ) {
1098 $scripts = self::filter(
'minify-js', $scripts );
1101 $scripts =
new XmlJsCode(
"function ( $, jQuery, require, module ) {\n{$scripts}\n}" );
1103 } elseif ( !is_array( $scripts ) ) {
1104 throw new MWException(
'Invalid scripts error. Array of URLs or string of code expected.' );
1116 self::trimArray( $module );
1128 public static function makeMessageSetScript( $messages ) {
1131 [ (
object)$messages ],
1143 public static function makeCombinedStyles(
array $stylePairs ) {
1145 foreach ( $stylePairs
as $media => $styles ) {
1149 $styles = (
array)$styles;
1150 foreach ( $styles
as $style ) {
1151 $style = trim( $style );
1153 if ( $style !==
'' ) {
1158 if ( $media ===
'' || $media ==
'all' ) {
1160 } elseif ( is_string( $media ) ) {
1161 $out[] =
"@media $media {\n" . str_replace(
"\n",
"\n\t",
"\t" . $style ) .
"}";
1184 public static function makeLoaderStateScript(
$name, $state = null ) {
1185 if ( is_array(
$name ) ) {
1214 public static function makeCustomLoaderScript(
$name,
$version, $dependencies,
1217 $script = str_replace(
"\n",
"\n\t", trim( $script ) );
1219 "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
1225 private static function isEmptyObject( stdClass $obj ) {
1244 private static function trimArray(
array &$array ) {
1245 $i = count( $array );
1247 if ( $array[$i] === null
1248 || $array[$i] === []
1249 || ( $array[$i] instanceof
XmlJsCode && $array[$i]->value ===
'{}' )
1250 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1252 unset( $array[$i] );
1286 public static function makeLoaderRegisterScript(
$name,
$version = null,
1287 $dependencies = null, $group = null,
$source = null, $skip = null
1289 if ( is_array(
$name ) ) {
1292 foreach (
$name as $i => &$module ) {
1293 $index[$module[0]] = $i;
1298 foreach (
$name as &$module ) {
1299 if ( isset( $module[2] ) ) {
1300 foreach ( $module[2]
as &$dependency ) {
1301 if ( isset( $index[$dependency] ) ) {
1302 $dependency = $index[$dependency];
1308 array_walk(
$name, [
'self',
'trimArray' ] );
1311 'mw.loader.register',
1317 self::trimArray( $registration );
1319 'mw.loader.register',
1340 public static function makeLoaderSourcesScript( $id, $properties = null ) {
1341 if ( is_array( $id ) ) {
1343 'mw.loader.addSource',
1349 'mw.loader.addSource',
1350 [ $id, $properties ],
1364 public static function makeLoaderConditionalScript( $script ) {
1365 return '(window.RLQ=window.RLQ||[]).push(function(){' .
1366 trim( $script ) .
'});';
1378 public static function makeInlineScript( $script ) {
1379 $js = self::makeLoaderConditionalScript( $script );
1380 return new WrappedString(
1382 '<script>(window.RLQ=window.RLQ||[]).push(function(){',
1394 public static function makeConfigSetScript(
array $configuration ) {
1410 public static function makePackedModulesString( $modules ) {
1412 foreach ( $modules
as $module ) {
1413 $pos = strrpos( $module,
'.' );
1414 $prefix = $pos ===
false ?
'' : substr( $module, 0, $pos );
1415 $suffix = $pos ===
false ? $module : substr( $module, $pos + 1 );
1416 $groups[$prefix][] = $suffix;
1420 foreach ( $groups
as $prefix => $suffixes ) {
1421 $p = $prefix ===
'' ?
'' : $prefix .
'.';
1422 $arr[] = $p . implode(
',', $suffixes );
1424 $str = implode(
'|', $arr );
1433 public static function inDebugMode() {
1434 if ( self::$debugMode === null ) {
1436 self::$debugMode = $wgRequest->getFuzzyBool(
'debug',
1437 $wgRequest->getCookie(
'resourceLoaderDebug',
'', $wgResourceLoaderDebug )
1440 return self::$debugMode;
1450 public static function clearCache() {
1451 self::$debugMode = null;
1466 $query = self::createLoaderQuery( $context, $extraQuery );
1467 $script = $this->getLoadScript(
$source );
1487 public static function makeLoaderURL( $modules,
$lang,
$skin,
$user = null,
1488 $version = null,
$debug =
false, $only = null, $printable =
false,
1489 $handheld =
false, $extraQuery = []
1494 $only, $printable, $handheld, $extraQuery
1510 return self::makeLoaderQuery(
1518 $context->
getRequest()->getBool(
'printable' ),
1519 $context->
getRequest()->getBool(
'handheld' ),
1541 public static function makeLoaderQuery( $modules,
$lang,
$skin,
$user = null,
1542 $version = null,
$debug =
false, $only = null, $printable =
false,
1543 $handheld =
false, $extraQuery = []
1546 'modules' => self::makePackedModulesString( $modules ),
1549 'debug' =>
$debug ?
'true' :
'false',
1551 if (
$user !== null ) {
1557 if ( $only !== null ) {
1582 public static function isValidModuleName( $moduleName ) {
1583 return strcspn( $moduleName,
'!,|', 0, 255 ) === strlen( $moduleName );
1595 public function getLessCompiler( $extraVars = [] ) {
1599 if ( !class_exists(
'Less_Parser' ) ) {
1600 throw new MWException(
'MediaWiki requires the less.php parser' );
1604 $parser->ModifyVars( array_merge( $this->getLessVars(), $extraVars ) );
1606 array_fill_keys( $this->config->get(
'ResourceLoaderLESSImportPaths' ),
'' )
1608 $parser->SetOption(
'relativeUrls',
false );
1609 $parser->SetCacheDir( $this->config->get(
'CacheDirectory' ) ?:
wfTempDir() );
1620 public function getLessVars() {
1621 if ( !$this->lessVars ) {
1622 $lessVars = $this->config->get(
'ResourceLoaderLESSVars' );
1623 Hooks::run(
'ResourceLoaderGetLessVars', [ &$lessVars ] );
1624 $this->lessVars = $lessVars;
1626 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 "