* @file
*/
+use MediaWiki\Linker\LinkTarget;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Session\SessionManager;
/** @var ResourceLoaderContext */
private $rlClientContext;
- /** @var string */
- private $rlUserModuleState;
-
/** @var array */
private $rlExemptStyleModules;
/** @var array Profiling data */
private $limitReportJSData = [];
+ /** @var array Map Title to Content */
+ private $contentOverrides = [];
+
+ /** @var callable[] */
+ private $contentOverrideCallbacks = [];
+
/**
* Link: header contents
*/
private $mLinkHeader = [];
+ /**
+ * @var string The nonce for Content-Security-Policy
+ */
+ private $CSPNonce;
+
/**
* Constructor for OutputPage. This should not be called directly.
* Instead a new RequestContext should be created and it will implicitly create
if ( is_null( $version ) ) {
$version = $this->getConfig()->get( 'StyleVersion' );
}
- $this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ) ) );
+ $this->addScript( Html::linkedScript( wfAppendQuery( $path, $version ), $this->getCSPNonce() ) );
}
/**
* @param string $script JavaScript text, no script tags
*/
public function addInlineScript( $script ) {
- $this->mScripts .= Html::inlineScript( $script );
+ $this->mScripts .= Html::inlineScript( "\n$script\n", $this->getCSPNonce() ) . "\n";
}
/**
}
/**
- * Add one or more modules recognized by ResourceLoader. Modules added
- * through this function will be loaded by ResourceLoader when the
- * page loads.
+ * Load one or more ResourceLoader modules on this page.
*
* @param string|array $modules Module name (string) or array of module names
*/
}
/**
- * Get the list of module JS to include on this page
+ * Get the list of script-only modules to load on this page.
*
* @param bool $filter
* @param string|null $position Unused
}
/**
- * Add only JS of one or more modules recognized by ResourceLoader. Module
- * scripts added through this function will be loaded by ResourceLoader when
- * the page loads.
+ * Load the scripts of one or more ResourceLoader modules, on this page.
*
+ * This method exists purely to provide the legacy behaviour of loading
+ * a module's scripts in the global scope, and without dependency resolution.
+ * See <https://phabricator.wikimedia.org/T188689>.
+ *
+ * @deprecated since 1.31 Use addModules() instead.
* @param string|array $modules Module name (string) or array of module names
*/
public function addModuleScripts( $modules ) {
}
/**
- * Get the list of module CSS to include on this page
+ * Get the list of style-only modules to load on this page.
*
* @param bool $filter
* @param string|null $position Unused
}
/**
- * Add only CSS of one or more modules recognized by ResourceLoader.
+ * Load the styles of one or more ResourceLoader modules on this page.
*
- * Module styles added through this function will be added using standard link CSS
- * tags, rather than as a combined Javascript and CSS package. Thus, they will
- * load when JavaScript is disabled (unless CSS also happens to be disabled).
+ * Module styles added through this function will be loaded as a stylesheet,
+ * using a standard `<link rel=stylesheet>` HTML tag, rather than as a combined
+ * Javascript and CSS package. Thus, they will even load when JavaScript is disabled.
*
* @param string|array $modules Module name (string) or array of module names
*/
$this->mTarget = $target;
}
+ /**
+ * Add a mapping from a LinkTarget to a Content, for things like page preview.
+ * @see self::addContentOverrideCallback()
+ * @since 1.32
+ * @param LinkTarget $target
+ * @param Content $content
+ */
+ public function addContentOverride( LinkTarget $target, Content $content ) {
+ if ( !$this->contentOverrides ) {
+ // Register a callback for $this->contentOverrides on the first call
+ $this->addContentOverrideCallback( function ( LinkTarget $target ) {
+ $key = $target->getNamespace() . ':' . $target->getDBkey();
+ return isset( $this->contentOverrides[$key] )
+ ? $this->contentOverrides[$key]
+ : null;
+ } );
+ }
+
+ $key = $target->getNamespace() . ':' . $target->getDBkey();
+ $this->contentOverrides[$key] = $content;
+ }
+
+ /**
+ * Add a callback for mapping from a Title to a Content object, for things
+ * like page preview.
+ * @see ResourceLoaderContext::getContentOverrideCallback()
+ * @since 1.32
+ * @param callable $callback
+ */
+ public function addContentOverrideCallback( callable $callback ) {
+ $this->contentOverrideCallbacks[] = $callback;
+ }
+
/**
* Get an array of head items
*
}
}
+ /**
+ * Transfer styles and JavaScript modules from skin.
+ *
+ * @param Skin $sk to load modules for
+ */
+ public function loadSkinModules( $sk ) {
+ foreach ( $sk->getDefaultModules() as $group => $modules ) {
+ if ( $group === 'styles' ) {
+ foreach ( $modules as $key => $moduleMembers ) {
+ $this->addModuleStyles( $moduleMembers );
+ }
+ } else {
+ $this->addModules( $modules );
+ }
+ }
+ }
+
/**
* Finally, all the text has been munged and accumulated into
* the object, let's actually output it:
$response->header( "X-Frame-Options: $frameOptions" );
}
+ ContentSecurityPolicy::sendHeaders( $this );
+
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();
}
$sk = $this->getSkin();
- foreach ( $sk->getDefaultModules() as $group ) {
- $this->addModules( $group );
- }
+ $this->loadSkinModules( $sk );
MWDebug::addModules( $this );
$this->getResourceLoader(),
new FauxRequest( $query )
);
+ if ( $this->contentOverrideCallbacks ) {
+ $this->rlClientContext = new DerivativeResourceLoaderContext( $this->rlClientContext );
+ $this->rlClientContext->setContentOverrideCallback( function ( Title $title ) {
+ foreach ( $this->contentOverrideCallbacks as $callback ) {
+ $content = call_user_func( $callback, $title );
+ if ( $content !== null ) {
+ return $content;
+ }
+ }
+ return null;
+ } );
+ }
}
return $this->rlClientContext;
}
$context = $this->getRlClientContext();
$rl = $this->getResourceLoader();
$this->addModules( [
+ 'user',
'user.options',
'user.tokens',
] );
function ( $name ) use ( $rl, $context, &$exemptGroups, &$exemptStates ) {
$module = $rl->getModule( $name );
if ( $module ) {
- if ( $name === 'user.styles' && $this->isUserCssPreview() ) {
- $exemptStates[$name] = 'ready';
- // Special case in buildExemptModules()
- return false;
- }
$group = $module->getGroup();
if ( isset( $exemptGroups[$group] ) ) {
$exemptStates[$name] = 'ready';
);
$this->rlExemptStyleModules = $exemptGroups;
- $isUserModuleFiltered = !$this->filterModules( [ 'user' ] );
- // If this page filters out 'user', makeResourceLoaderLink will drop it.
- // Avoid indefinite "loading" state or untrue "ready" state (T145368).
- if ( !$isUserModuleFiltered ) {
- // Manually handled by getBottomScripts()
- $userModule = $rl->getModule( 'user' );
- $userState = $userModule->isKnownEmpty( $context ) && !$this->isUserJsPreview()
- ? 'ready'
- : 'loading';
- $this->rlUserModuleState = $exemptStates['user'] = $userState;
- }
-
$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 ) );
ResourceLoaderContext::newDummyContext(),
[ 'html5shiv' ],
ResourceLoaderModule::TYPE_SCRIPTS,
- [ 'sync' => true ]
+ [ 'sync' => true ],
+ $this->getCSPNonce()
) .
'<![endif]-->';
$this->getRlClientContext(),
$modules,
$only,
- $extraQuery
+ $extraQuery,
+ $this->getCSPNonce()
);
}
return WrappedString::join( "\n", $chunks );
}
- private function isUserJsPreview() {
- return $this->getConfig()->get( 'AllowUserJs' )
- && $this->getTitle()
- && $this->getTitle()->isUserJsConfigPage()
- && $this->userCanPreview();
- }
-
- protected function isUserCssPreview() {
- return $this->getConfig()->get( 'AllowUserCss' )
- && $this->getTitle()
- && $this->getTitle()->isUserCssConfigPage()
- && $this->userCanPreview();
- }
-
/**
* JS stuff to put at the bottom of the `<body>`.
* These are legacy scripts ($this->mScripts), and user JS.
// Legacy non-ResourceLoader scripts
$chunks[] = $this->mScripts;
- // Exempt 'user' module
- // - May need excludepages for live preview. (T28283)
- // - Must use TYPE_COMBINED so its response is handled by mw.loader.implement() which
- // ensures execution is scheduled after the "site" module.
- // - Don't load if module state is already resolved as "ready".
- if ( $this->rlUserModuleState === 'loading' ) {
- if ( $this->isUserJsPreview() ) {
- $chunks[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED,
- [ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
- );
- $chunks[] = ResourceLoader::makeInlineScript(
- Xml::encodeJsCall( 'mw.loader.using', [
- [ 'user', 'site' ],
- new XmlJsCode(
- 'function () {'
- . Xml::encodeJsCall( '$.globalEval', [
- $this->getRequest()->getText( 'wpTextbox1' )
- ] )
- . '}'
- )
- ] )
- );
- // FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded
- // asynchronously and may arrive *after* the inline script here. So the previewed code
- // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js.
- // Similarly, when previewing ./common.js and the user module does arrive first,
- // it will arrive without common.js and the inline script runs after.
- // Thus running common after the excluded subpage.
- } else {
- // Load normally
- $chunks[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED );
- }
- }
-
if ( $this->limitReportJSData ) {
$chunks[] = ResourceLoader::makeInlineScript(
ResourceLoader::makeConfigSetScript(
[ 'wgPageParseReport' => $this->limitReportJSData ]
- )
+ ),
+ $this->getCSPNonce()
);
}
/**
* To make it harder for someone to slip a user a fake
- * user-JavaScript or user-CSS preview, a random token
+ * JavaScript or CSS preview, a random token
* is associated with the login session. If it's not
* passed back with the preview request, we won't render
* the code.
$request = $this->getRequest();
if (
$request->getVal( 'action' ) !== 'submit' ||
- !$request->getCheck( 'wpPreview' ) ||
!$request->wasPosted()
) {
return false;
}
$title = $this->getTitle();
- if (
- !$title->isUserJsConfigPage()
- && !$title->isUserCssConfigPage()
- ) {
- return false;
- }
- if ( !$title->isSubpageOf( $user->getUserPage() ) ) {
- // Don't execute another user's CSS or JS on preview (T85855)
- return false;
- }
-
$errors = $title->getUserPermissionsErrors( 'edit', $user );
if ( count( $errors ) !== 0 ) {
return false;
* @return string|WrappedStringList HTML
*/
protected function buildExemptModules() {
- global $wgContLang;
-
$chunks = [];
// Things that go after the ResourceLoaderDynamicStyles marker
$append = [];
- // Exempt 'user' styles module (may need 'excludepages' for live preview)
- if ( $this->isUserCssPreview() ) {
- $append[] = $this->makeResourceLoaderLink(
- 'user.styles',
- ResourceLoaderModule::TYPE_STYLES,
- [ 'excludepage' => $this->getTitle()->getPrefixedDBkey() ]
- );
-
- // Load the previewed CSS. Janus it if needed.
- // User-supplied CSS is assumed to in the wiki's content language.
- $previewedCSS = $this->getRequest()->getText( 'wpTextbox1' );
- if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) {
- $previewedCSS = CSSJanus::transform( $previewedCSS, true, false );
- }
- $append[] = Html::inlineStyle( $previewedCSS );
- }
-
// We want site, private and user styles to override dynamically added styles from
// general modules, but we want dynamically added styles to override statically added
// style modules. So the order has to be:
);
}
}
+
+ /**
+ * Get (and set if not yet set) the CSP nonce.
+ *
+ * This value needs to be included in any <script> tags on the
+ * page.
+ *
+ * @return string|bool Nonce or false to mean don't output nonce
+ * @since 1.32
+ */
+ public function getCSPNonce() {
+ if ( !ContentSecurityPolicy::isEnabled( $this->getConfig() ) ) {
+ return false;
+ }
+ if ( $this->CSPNonce === null ) {
+ // XXX It might be expensive to generate randomness
+ // on every request, on windows.
+ $rand = MWCryptRand::generate( 15 );
+ $this->CSPNonce = base64_encode( $rand );
+ }
+ return $this->CSPNonce;
+ }
}