if ( $this->mArticleBodyOnly ) {
echo $this->mBodytext;
} else {
- // Enable safe mode if requested
+ // Enable safe mode if requested (T152169)
if ( $this->getRequest()->getBool( 'safemode' ) ) {
$this->disallowUserJs();
}
$rlClient = new ResourceLoaderClientHtml( $context, [
'target' => $this->getTarget(),
'nonce' => $this->getCSPNonce(),
+ // When 'safemode', disallowUserJs(), or reduceAllowedModules() is used
+ // to only restrict modules to ORIGIN_CORE (ie. disallow ORIGIN_USER), the list of
+ // modules enqueud for loading on this page is filtered to just those.
+ // However, to make sure we also apply the restriction to dynamic dependencies and
+ // lazy-loaded modules at run-time on the client-side, pass 'safemode' down to the
+ // StartupModule so that the client-side registry will not contain any restricted
+ // modules either. (T152169, T185303)
+ 'safemode' => ( $this->getAllowedModules( ResourceLoaderModule::TYPE_COMBINED )
+ <= ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
+ ) ? '1' : null,
] );
$rlClient->setConfig( $this->getJSVars() );
$rlClient->setModules( $this->getModules( /*filter*/ true ) );
/**
* @param ResourceLoaderContext $context
* @param array $options [optional] Array of options
- * - 'target': Custom parameter passed to StartupModule.
+ * - 'target': Parameter for modules=startup request, see ResourceLoaderStartUpModule.
+ * - 'safemode': Parameter for modules=startup request, see ResourceLoaderStartUpModule.
* - 'nonce': From OutputPage::getCSPNonce().
*/
public function __construct( ResourceLoaderContext $context, array $options = [] ) {
$this->resourceLoader = $context->getResourceLoader();
$this->options = $options + [
'target' => null,
+ 'safemode' => null,
'nonce' => null,
];
}
// Async scripts. Once the startup is loaded, inline RLQ scripts will run.
// Pass-through a custom 'target' from OutputPage (T143066).
- $startupQuery = $this->options['target'] !== null
- ? [ 'target' => (string)$this->options['target'] ]
- : [];
+ $startupQuery = [];
+ foreach ( [ 'target', 'safemode' ] as $param ) {
+ if ( $this->options[$param] !== null ) {
+ $startupQuery[$param] = (string)$this->options[$param];
+ }
+ }
$chunks[] = $this->getLoad(
'startup',
ResourceLoaderModule::TYPE_SCRIPTS,
}
/**
- * Get the origin of this module. Should only be overridden for foreign modules.
+ * Get the source of this module. Should only be overridden for foreign modules.
*
- * @return string Origin name, 'local' for local modules
+ * @return string Source name, 'local' for local modules
*/
public function getSource() {
// Stub, override expected
* the ability to vary based extra query parameters, in addition to those
* from ResourceLoaderContext:
*
- * - target: Only register modules in the client allowed within this target.
+ * - target: Only register modules in the client intended for this target.
* Default: "desktop".
* See also: OutputPage::setTarget(), ResourceLoaderModule::getTargets().
+ *
+ * - safemode: Only register modules that have ORIGIN_CORE as their origin.
+ * This effectively disables ORIGIN_USER modules. (T185303)
+ * See also: OutputPage::disallowUserJs()
*/
class ResourceLoaderStartUpModule extends ResourceLoaderModule {
// Future developers: Use WebRequest::getRawVal() instead getVal().
// The getVal() method performs slow Language+UTF logic. (f303bb9360)
$target = $context->getRequest()->getRawVal( 'target', 'desktop' );
+ $safemode = $context->getRequest()->getRawVal( 'safemode' ) === '1';
// Bypass target filter if this request is Special:JavaScriptTest.
// To prevent misuse in production, this is only allowed if testing is enabled server-side.
$byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
foreach ( $resourceLoader->getModuleNames() as $name ) {
$module = $resourceLoader->getModule( $name );
$moduleTargets = $module->getTargets();
- if ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) ) {
+ if (
+ ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) )
+ || ( $safemode && $module->getOrigin() > ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL )
+ ) {
continue;
}
'skin' => 'vector',
'modules' => 'startup',
'only' => 'scripts',
+ 'safemode' => null,
];
$resourceLoader = $rl ?: new ResourceLoader();
$request = new FauxRequest( [
'lang' => $options['lang'],
'modules' => $options['modules'],
'only' => $options['only'],
+ 'safemode' => $options['safemode'],
'skin' => $options['skin'],
'target' => 'phpunit',
] );
$this->assertEquals( $expected, $client->getHeadHtml() );
}
+ /**
+ * Confirm that 'safemode' is passed down to startup.
+ *
+ * @covers ResourceLoaderClientHtml::getHeadHtml
+ */
+ public function testGetHeadHtmlWithSafemode() {
+ $client = new ResourceLoaderClientHtml(
+ self::makeContext(),
+ [ 'safemode' => '1' ]
+ );
+
+ // phpcs:disable Generic.Files.LineLength
+ $expected = '<script>document.documentElement.className = document.documentElement.className.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );</script>' . "\n"
+ . '<script async="" src="/w/load.php?debug=false&lang=nl&modules=startup&only=scripts&safemode=1&skin=fallback"></script>';
+ // phpcs:enable
+
+ $this->assertEquals( $expected, $client->getHeadHtml() );
+ }
+
/**
* Confirm that a null 'target' is the same as no target.
*
"test.blank",
"{blankVer}"
]
+] );'
+ ] ],
+ [ [
+ 'msg' => 'Safemode disabled (default; register all modules)',
+ 'modules' => [
+ // Default origin: ORIGIN_CORE_SITEWIDE
+ 'test.blank' => new ResourceLoaderTestModule(),
+ 'test.core-generated' => new ResourceLoaderTestModule( [
+ 'origin' => ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
+ ] ),
+ 'test.sitewide' => new ResourceLoaderTestModule( [
+ 'origin' => ResourceLoaderModule::ORIGIN_USER_SITEWIDE
+ ] ),
+ 'test.user' => new ResourceLoaderTestModule( [
+ 'origin' => ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL
+ ] ),
+ ],
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php"
+} );
+mw.loader.register( [
+ [
+ "test.blank",
+ "{blankVer}"
+ ],
+ [
+ "test.core-generated",
+ "{blankVer}"
+ ],
+ [
+ "test.sitewide",
+ "{blankVer}"
+ ],
+ [
+ "test.user",
+ "{blankVer}"
+ ]
+] );'
+ ] ],
+ [ [
+ 'msg' => 'Safemode enabled (filter modules with user/site origin)',
+ 'extraQuery' => [ 'safemode' => '1' ],
+ 'modules' => [
+ // Default origin: ORIGIN_CORE_SITEWIDE
+ 'test.blank' => new ResourceLoaderTestModule(),
+ 'test.core-generated' => new ResourceLoaderTestModule( [
+ 'origin' => ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
+ ] ),
+ 'test.sitewide' => new ResourceLoaderTestModule( [
+ 'origin' => ResourceLoaderModule::ORIGIN_USER_SITEWIDE
+ ] ),
+ 'test.user' => new ResourceLoaderTestModule( [
+ 'origin' => ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL
+ ] ),
+ ],
+ 'out' => '
+mw.loader.addSource( {
+ "local": "/w/load.php"
+} );
+mw.loader.register( [
+ [
+ "test.blank",
+ "{blankVer}"
+ ],
+ [
+ "test.core-generated",
+ "{blankVer}"
+ ]
] );'
] ],
[ [
$this->setMwGlobals( 'wgResourceLoaderSources', $case['sources'] );
}
- $context = $this->getResourceLoaderContext();
+ $extraQuery = isset( $case['extraQuery'] ) ? $case['extraQuery'] : [];
+ $context = $this->getResourceLoaderContext( $extraQuery );
$rl = $context->getResourceLoader();
$rl->register( $case['modules'] );
$module = new ResourceLoaderStartUpModule();