This change allows to use the context in the functions.
The following internal static functions from ResourceLoader get now a
reference to the ResourceLoaderContext object:
* makeLoaderImplementScript
* makeLoaderStateScript
* makeLoaderRegisterScript
* makeLoaderSourcesScript
ResouceLoader::encodeJsonForScript is duplicated to
ResourceLoaderContext::encodeJson loading the debug mode from context.
ResourceLoader::encodeJsonForScript is kept for other usages without
context.
The debug mode is loaded from $context->getDebug() instead of from
ResourceLoader::inDebugMode(). This does not support to enable the debug
mode by setting the cookie 'resourceLoaderDebug' or the configuration
variable wgResourceLoaderDebug. Only the URL parameter debug=true
enables the debug mode. This should be sufficient for the subsequent
ResourceLoader requests. The tests don't need the global variable
wgResourceLoaderDebug anymore. The initial ResourceLoader context in
OutputPage still uses ResourceLoader::inDebugMode() with cookie and
global configuration variable.
This change adds the parameter $context with a ResourceLoaderContext
object to ResourceLoaderModule::getDeprecationInformation and deprecates
omitting the parameter. Ifa1a3bb56b731b83864022a358916c6aca5d7c10
updates this in extension ExtJSBase.
Bug: T229311
Change-Id: I5341f18625209446a6d006f60244990f65530319
$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, [] )
);
}