$errorResponse = self::makeComment( $errorText );
if ( $context->shouldIncludeScripts() ) {
$errorResponse .= 'if (window.console && console.error) { console.error('
- . self::encodeJsonForScript( $errorText )
+ . $context->encodeJson( $errorText )
. "); }\n";
}
$strContent = $scripts;
} elseif ( is_array( $scripts ) ) {
// ...except when $scripts is an array of URLs or an associative array
- $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
+ $strContent = self::makeLoaderImplementScript(
+ $context,
+ $implementKey,
+ $scripts,
+ [],
+ [],
+ []
+ );
}
break;
case 'styles':
}
}
$strContent = self::makeLoaderImplementScript(
+ $context,
$implementKey,
$scripts,
$content['styles'] ?? [],
// Set the state of modules we didn't respond to with mw.loader.implement
if ( $states ) {
- $stateScript = self::makeLoaderStateScript( $states );
+ $stateScript = self::makeLoaderStateScript( $context, $states );
if ( !$context->getDebug() ) {
$stateScript = self::filter( 'minify-js', $stateScript );
}
}
} elseif ( $states ) {
$this->errors[] = 'Problematic modules: '
- . self::encodeJsonForScript( $states );
+ . $context->encodeJson( $states );
}
return $out;
/**
* Return JS code that calls mw.loader.implement with given module properties.
*
+ * @param ResourceLoaderContext $context
* @param string $name Module name or implement key (format "`[name]@[version]`")
* @param XmlJsCode|array|string $scripts Code as XmlJsCode (to be wrapped in a closure),
* list of URLs to JavaScript files, string of JavaScript for `$.globalEval`, or array with
* @throws MWException
* @return string JavaScript code
*/
- protected static function makeLoaderImplementScript(
- $name, $scripts, $styles, $messages, $templates
+ private static function makeLoaderImplementScript(
+ ResourceLoaderContext $context, $name, $scripts, $styles, $messages, $templates
) {
if ( $scripts instanceof XmlJsCode ) {
if ( $scripts->value === '' ) {
$scripts = null;
- } elseif ( self::inDebugMode() ) {
+ } elseif ( $context->getDebug() ) {
$scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
} else {
$scripts = new XmlJsCode( 'function($,jQuery,require,module){' . $scripts->value . '}' );
// All of these essentially do $file = $file['content'];, some just have wrapping around it
if ( $file['type'] === 'script' ) {
// Multi-file modules only get two parameters ($ and jQuery are being phased out)
- if ( self::inDebugMode() ) {
+ if ( $context->getDebug() ) {
$file = new XmlJsCode( "function ( require, module ) {\n{$file['content']}\n}" );
} else {
$file = new XmlJsCode( 'function(require,module){' . $file['content'] . '}' );
}
$scripts = XmlJsCode::encodeObject( [
'main' => $scripts['main'],
- 'files' => XmlJsCode::encodeObject( $files, self::inDebugMode() )
- ], self::inDebugMode() );
+ 'files' => XmlJsCode::encodeObject( $files, $context->getDebug() )
+ ], $context->getDebug() );
} elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
}
];
self::trimArray( $module );
- return Xml::encodeJsCall( 'mw.loader.implement', $module, self::inDebugMode() );
+ return Xml::encodeJsCall( 'mw.loader.implement', $module, $context->getDebug() );
}
/**
* Returns a JS call to mw.loader.state, which sets the state of one
* ore more modules to a given value. Has two calling conventions:
*
- * - ResourceLoader::makeLoaderStateScript( $name, $state ):
+ * - ResourceLoader::makeLoaderStateScript( $context, $name, $state ):
* Set the state of a single module called $name to $state
*
- * - ResourceLoader::makeLoaderStateScript( [ $name => $state, ... ] ):
+ * - ResourceLoader::makeLoaderStateScript( $context, [ $name => $state, ... ] ):
* Set the state of modules with the given names to the given states
*
+ * @internal
+ * @param ResourceLoaderContext $context
* @param array|string $states
* @param string|null $state
* @return string JavaScript code
*/
- public static function makeLoaderStateScript( $states, $state = null ) {
+ public static function makeLoaderStateScript(
+ ResourceLoaderContext $context, $states, $state = null
+ ) {
if ( !is_array( $states ) ) {
$states = [ $states => $state ];
}
return 'mw.loader.state('
- . self::encodeJsonForScript( $states )
+ . $context->encodeJson( $states )
. ');';
}
* @par Example
* @code
*
- * ResourceLoader::makeLoaderRegisterScript( [
+ * ResourceLoader::makeLoaderRegisterScript( $context, [
* [ $name1, $version1, $dependencies1, $group1, $source1, $skip1 ],
* [ $name2, $version2, $dependencies1, $group2, $source2, $skip2 ],
* ...
* ] ):
* @endcode
*
- * @internal
- * @since 1.32
+ * @internal For use by ResourceLoaderStartUpModule only
+ * @param ResourceLoaderContext $context
* @param array $modules Array of module registration arrays, each containing
* - string: module name
* - string: module version
* - string|null: Script body of a skip function (optional)
* @return string JavaScript code
*/
- public static function makeLoaderRegisterScript( array $modules ) {
+ public static function makeLoaderRegisterScript(
+ ResourceLoaderContext $context, array $modules
+ ) {
// Optimisation: Transform dependency names into indexes when possible
// to produce smaller output. They are expanded by mw.loader.register on
// the other end using resolveIndexedDependencies().
array_walk( $modules, [ self::class, 'trimArray' ] );
return 'mw.loader.register('
- . self::encodeJsonForScript( $modules )
+ . $context->encodeJson( $modules )
. ');';
}
* Returns JS code which calls mw.loader.addSource() with the given
* parameters. Has two calling conventions:
*
- * - ResourceLoader::makeLoaderSourcesScript( $id, $properties ):
+ * - ResourceLoader::makeLoaderSourcesScript( $context, $id, $properties ):
* Register a single source
*
- * - ResourceLoader::makeLoaderSourcesScript( [ $id1 => $loadUrl, $id2 => $loadUrl, ... ] );
+ * - ResourceLoader::makeLoaderSourcesScript( $context,
+ * [ $id1 => $loadUrl, $id2 => $loadUrl, ... ]
+ * );
* Register sources with the given IDs and properties.
*
+ * @internal For use by ResourceLoaderStartUpModule only
+ * @param ResourceLoaderContext $context
* @param string|array $sources Source ID
* @param string|null $loadUrl load.php url
* @return string JavaScript code
*/
- public static function makeLoaderSourcesScript( $sources, $loadUrl = null ) {
+ public static function makeLoaderSourcesScript(
+ ResourceLoaderContext $context, $sources, $loadUrl = null
+ ) {
if ( !is_array( $sources ) ) {
$sources = [ $sources => $loadUrl ];
}
return 'mw.loader.addSource('
- . self::encodeJsonForScript( $sources )
+ . $context->encodeJson( $sources )
. ');';
}
// Load from load.php?only=styles via <link rel=stylesheet>
$data['styles'][] = $name;
}
- $deprecation = $module->getDeprecationInformation();
+ $deprecation = $module->getDeprecationInformation( $context );
if ( $deprecation ) {
$data['styleDeprecations'][] = $deprecation;
}
// See also startup/startup.js.
$nojsClass = $nojsClass ?? $this->getDocumentAttributes()['class'];
$jsClass = preg_replace( '/(^|\s)client-nojs(\s|$)/', '$1client-js$2', $nojsClass );
- $jsClassJson = ResourceLoader::encodeJsonForScript( $jsClass );
+ $jsClassJson = $this->context->encodeJson( $jsClass );
$script = <<<JAVASCRIPT
document.documentElement.className = {$jsClassJson};
JAVASCRIPT;
// Inline script: Declare mw.config variables for this page.
if ( $this->config ) {
- $confJson = ResourceLoader::encodeJsonForScript( $this->config );
+ $confJson = $this->context->encodeJson( $this->config );
$script .= <<<JAVASCRIPT
RLCONF = {$confJson};
JAVASCRIPT;
// Inline script: Declare initial module states for this page.
$states = array_merge( $this->exemptStates, $data['states'] );
if ( $states ) {
- $stateJson = ResourceLoader::encodeJsonForScript( $states );
+ $stateJson = $this->context->encodeJson( $states );
$script .= <<<JAVASCRIPT
RLSTATE = {$stateJson};
JAVASCRIPT;
// Inline script: Declare general modules to load on this page.
if ( $data['general'] ) {
- $pageModulesJson = ResourceLoader::encodeJsonForScript( $data['general'] );
+ $pageModulesJson = $this->context->encodeJson( $data['general'] );
$script .= <<<JAVASCRIPT
RLPAGEMODULES = {$pageModulesJson};
JAVASCRIPT;
] );
} else {
$chunk = ResourceLoader::makeInlineScript(
- 'mw.loader.load(' . ResourceLoader::encodeJsonForScript( $url ) . ');',
+ 'mw.loader.load(' . $mainContext->encodeJson( $url ) . ');',
$nonce
);
}
/**
* Get the request base parameters, omitting any defaults.
*
- * @internal For internal use by ResourceLoaderStartUpModule only
+ * @internal For use by ResourceLoaderStartUpModule only
* @return array
*/
public function getReqBase() {
}
return $reqBase;
}
+
+ /**
+ * Wrapper around json_encode that avoids needless escapes,
+ * and pretty-prints in debug mode.
+ *
+ * @internal
+ * @param mixed $data
+ * @return string|false JSON string, false on error
+ */
+ public function encodeJson( $data ) {
+ // Keep output as small as possible by disabling needless escape modes
+ // that PHP uses by default.
+ // However, while most module scripts are only served on HTTP responses
+ // for JavaScript, some modules can also be embedded in the HTML as inline
+ // scripts. This, and the fact that we sometimes need to export strings
+ // containing user-generated content and labels that may genuinely contain
+ // a sequences like "</script>", we need to encode either '/' or '<'.
+ // By default PHP escapes '/'. Let's escape '<' instead which is less common
+ // and allows URLs to mostly remain readable.
+ $jsonFlags = JSON_UNESCAPED_SLASHES |
+ JSON_UNESCAPED_UNICODE |
+ JSON_HEX_TAG |
+ JSON_HEX_AMP;
+ if ( $this->getDebug() ) {
+ $jsonFlags |= JSON_PRETTY_PRINT;
+ }
+ return json_encode( $data, $jsonFlags );
+ }
}
* @return string|array JavaScript code for $context, or package files data structure
*/
public function getScript( ResourceLoaderContext $context ) {
- $deprecationScript = $this->getDeprecationInformation();
+ $deprecationScript = $this->getDeprecationInformation( $context );
if ( $this->packageFiles !== null ) {
$packageFiles = $this->getPackageFiles( $context );
if ( $deprecationScript ) {
public function getScript( ResourceLoaderContext $context ) {
return parent::getScript( $context )
. 'mw.language.setData('
- . ResourceLoader::encodeJsonForScript( $context->getLanguage() ) . ','
- . ResourceLoader::encodeJsonForScript( $this->getData( $context ) )
+ . $context->encodeJson( $context->getLanguage() ) . ','
+ . $context->encodeJson( $this->getData( $context ) )
. ');';
}
/**
* Get JS representing deprecation information for the current module if available
*
+ * @param ResourceLoaderContext|null $context Missing $context is deprecated in 1.34
* @return string JavaScript code
*/
- public function getDeprecationInformation() {
+ public function getDeprecationInformation( ResourceLoaderContext $context = null ) {
+ if ( $context === null ) {
+ wfDeprecated( __METHOD__ . ' without a ResourceLoader context', '1.34' );
+ }
$deprecationInfo = $this->deprecated;
if ( $deprecationInfo ) {
$name = $this->getName();
if ( is_string( $deprecationInfo ) ) {
$warning .= "\n" . $deprecationInfo;
}
- return 'mw.log.warn(' . ResourceLoader::encodeJsonForScript( $warning ) . ');';
+ if ( $context === null ) {
+ return 'mw.log.warn(' . ResourceLoader::encodeJsonForScript( $warning ) . ');';
+ }
+ return 'mw.log.warn(' . $context->encodeJson( $warning ) . ');';
} else {
return '';
}
}
$skipFunction = $module->getSkipFunction();
- if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) {
+ if ( $skipFunction !== null && !$context->getDebug() ) {
$skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
}
self::compileUnresolvedDependencies( $registryData );
// Register sources
- $out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
+ $out .= ResourceLoader::makeLoaderSourcesScript( $context, $resourceLoader->getSources() );
// Figure out the different call signatures for mw.loader.register
$registrations = [];
}
// Register modules
- $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations );
+ $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $context, $registrations );
if ( $states ) {
- $out .= "\n" . ResourceLoader::makeLoaderStateScript( $states );
+ $out .= "\n" . ResourceLoader::makeLoaderStateScript( $context, $states );
}
return $out;
// Perform replacements for mediawiki.js
$mwLoaderPairs = [
- '$VARS.reqBase' => ResourceLoader::encodeJsonForScript( $context->getReqBase() ),
- '$VARS.baseModules' => ResourceLoader::encodeJsonForScript( $this->getBaseModules() ),
- '$VARS.maxQueryLength' => ResourceLoader::encodeJsonForScript(
+ '$VARS.reqBase' => $context->encodeJson( $context->getReqBase() ),
+ '$VARS.baseModules' => $context->encodeJson( $this->getBaseModules() ),
+ '$VARS.maxQueryLength' => $context->encodeJson(
$conf->get( 'ResourceLoaderMaxQueryLength' )
),
// The client-side module cache can be disabled by site configuration.
// It is also always disabled in debug mode.
- '$VARS.storeEnabled' => ResourceLoader::encodeJsonForScript(
+ '$VARS.storeEnabled' => $context->encodeJson(
$conf->get( 'ResourceLoaderStorageEnabled' ) && !$context->getDebug()
),
- '$VARS.wgLegacyJavaScriptGlobals' => ResourceLoader::encodeJsonForScript(
+ '$VARS.wgLegacyJavaScriptGlobals' => $context->encodeJson(
$conf->get( 'LegacyJavaScriptGlobals' )
),
- '$VARS.storeKey' => ResourceLoader::encodeJsonForScript( $this->getStoreKey() ),
- '$VARS.storeVary' => ResourceLoader::encodeJsonForScript( $this->getStoreVary( $context ) ),
- '$VARS.groupUser' => ResourceLoader::encodeJsonForScript( $this->getGroupId( 'user' ) ),
- '$VARS.groupPrivate' => ResourceLoader::encodeJsonForScript( $this->getGroupId( 'private' ) ),
+ '$VARS.storeKey' => $context->encodeJson( $this->getStoreKey() ),
+ '$VARS.storeVary' => $context->encodeJson( $this->getStoreVary( $context ) ),
+ '$VARS.groupUser' => $context->encodeJson( $this->getGroupId( 'user' ) ),
+ '$VARS.groupPrivate' => $context->encodeJson( $this->getGroupId( 'private' ) ),
];
$profilerStubs = [
'$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
// Perform string replacements for startup.js
$pairs = [
- '$VARS.configuration' => ResourceLoader::encodeJsonForScript(
+ '$VARS.configuration' => $context->encodeJson(
$this->getConfigSettings( $context )
),
// Raw JavaScript code (not JSON)
*/
public function getScript( ResourceLoaderContext $context ) {
return 'mw.user.options.set('
- . ResourceLoader::encodeJsonForScript( User::getDefaultOptions() )
+ . $context->encodeJson( User::getDefaultOptions() )
. ');';
}
}
// Use FILTER_NOMIN annotation to prevent needless minification and caching (T84960).
return ResourceLoader::FILTER_NOMIN
. 'mw.user.options.set('
- . ResourceLoader::encodeJsonForScript(
+ . $context->encodeJson(
$context->getUserObj()->getOptions( User::GETOPTIONS_EXCLUDE_DEFAULTS )
)
. ');';
// Use FILTER_NOMIN annotation to prevent needless minification and caching (T84960).
return ResourceLoader::FILTER_NOMIN
. 'mw.user.tokens.set('
- . ResourceLoader::encodeJsonForScript( $this->contextUserTokens( $context ) )
+ . $context->encodeJson( $this->contextUserTokens( $context ) )
. ');';
}
* @dataProvider provideRegistrations
*/
public function testRegistrationsMinified( $modules ) {
- $this->setMwGlobals( 'wgResourceLoaderDebug', false );
-
- $context = $this->getResourceLoaderContext();
+ $context = $this->getResourceLoaderContext( [
+ 'debug' => 'false',
+ ] );
$rl = $context->getResourceLoader();
$rl->register( $modules );
$module = new ResourceLoaderStartUpModule();
* @dataProvider provideRegistrations
*/
public function testRegistrationsUnminified( $modules ) {
- $context = $this->getResourceLoaderContext();
+ $context = $this->getResourceLoaderContext( [
+ 'debug' => 'true',
+ ] );
$rl = $context->getResourceLoader();
$rl->register( $modules );
$module = new ResourceLoaderStartUpModule();
'wrap' => true,
'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ), 'packageFiles' => [],
];
- ResourceLoader::clearCache();
- $this->setMwGlobals( 'wgResourceLoaderDebug', true );
-
$rl = TestingAccessWrapper::newFromClass( ResourceLoader::class );
+ $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest( [
+ 'debug' => 'true',
+ ] ) );
$this->assertEquals(
$case['expected'],
$rl->makeLoaderImplementScript(
+ $context,
$case['name'],
( $case['wrap'] && is_string( $case['scripts'] ) )
? new XmlJsCode( $case['scripts'] )
public function testMakeLoaderImplementScriptInvalid() {
$this->setExpectedException( MWException::class, 'Invalid scripts error' );
$rl = TestingAccessWrapper::newFromClass( ResourceLoader::class );
+ $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest() );
$rl->makeLoaderImplementScript(
+ $context,
'test', // name
123, // scripts
null, // styles
* @covers ResourceLoader::makeLoaderRegisterScript
*/
public function testMakeLoaderRegisterScript() {
+ $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest( [
+ 'debug' => 'true',
+ ] ) );
$this->assertEquals(
'mw.loader.register([
[
"1234567"
]
]);',
- ResourceLoader::makeLoaderRegisterScript( [
+ ResourceLoader::makeLoaderRegisterScript( $context, [
[ 'test.name', '1234567' ],
] ),
'Nested array parameter'
"return true;"
]
]);',
- ResourceLoader::makeLoaderRegisterScript( [
+ ResourceLoader::makeLoaderRegisterScript( $context, [
[ 'test.foo', '100' , [], null, null ],
[ 'test.bar', '200', [ 'test.unknown' ], null ],
[ 'test.baz', '300', [ 'test.quux', 'test.foo' ], null ],
* @covers ResourceLoader::makeLoaderSourcesScript
*/
public function testMakeLoaderSourcesScript() {
+ $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest( [
+ 'debug' => 'true',
+ ] ) );
$this->assertEquals(
'mw.loader.addSource({
"local": "/w/load.php"
});',
- ResourceLoader::makeLoaderSourcesScript( 'local', '/w/load.php' )
+ ResourceLoader::makeLoaderSourcesScript( $context, 'local', '/w/load.php' )
);
$this->assertEquals(
'mw.loader.addSource({
"local": "/w/load.php"
});',
- ResourceLoader::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
+ ResourceLoader::makeLoaderSourcesScript( $context, [ 'local' => '/w/load.php' ] )
);
$this->assertEquals(
'mw.loader.addSource({
"local": "/w/load.php",
"example": "https://example.org/w/load.php"
});',
- ResourceLoader::makeLoaderSourcesScript( [
+ ResourceLoader::makeLoaderSourcesScript( $context, [
'local' => '/w/load.php',
'example' => 'https://example.org/w/load.php'
] )
);
$this->assertEquals(
'mw.loader.addSource([]);',
- ResourceLoader::makeLoaderSourcesScript( [] )
+ ResourceLoader::makeLoaderSourcesScript( $context, [] )
);
}