diff --git a/docs/config-schema.yaml b/docs/config-schema.yaml index 0ddb330bb47..1767975e882 100644 --- a/docs/config-schema.yaml +++ b/docs/config-schema.yaml @@ -3205,8 +3205,13 @@ config-schema: ResourceLoaderClientPreferences: default: false description: |- - Whether skins support client side (anonymous) preferences. - @see RL/ClientHtml + Enable client-side preferences for unregistered users. + This is only supported for unregistered users. For registered users, skins + and extensions must use user preferences (e.g. hidden or API-only options) + and swap class names server-side through the Skin interface. + @warning EXPERIMENTAL! + @since 1.40 + @see \MediaWiki\ResourceLoader\ClientHtml DisableOutputCompression: default: false description: 'Disable output compression (enabled by default if zlib is available)' diff --git a/includes/MainConfigSchema.php b/includes/MainConfigSchema.php index e9162b4961a..3010f9efb78 100644 --- a/includes/MainConfigSchema.php +++ b/includes/MainConfigSchema.php @@ -5153,9 +5153,15 @@ class MainConfigSchema { ]; /** - * Whether skins support client side (anonymous) preferences. + * Enable client-side preferences for unregistered users. * - * @see RL/ClientHtml + * This is only supported for unregistered users. For registered users, skins + * and extensions must use user preferences (e.g. hidden or API-only options) + * and swap class names server-side through the Skin interface. + * + * @warning EXPERIMENTAL! + * @since 1.40 + * @see \MediaWiki\ResourceLoader\ClientHtml */ public const ResourceLoaderClientPreferences = [ 'default' => false, diff --git a/includes/OutputPage.php b/includes/OutputPage.php index a016e29ce16..1072511f693 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -3319,6 +3319,13 @@ class OutputPage extends ContextSource { ); $this->rlExemptStyleModules = $exemptGroups; + $config = $this->getConfig(); + $clientPrefEnabled = ( + $config->get( MainConfigNames::ResourceLoaderClientPreferences ) && + !$this->getUser()->isRegistered() + ); + $clientPrefCookiePrefix = $config->get( MainConfigNames::CookiePrefix ); + $rlClient = new RL\ClientHtml( $context, [ 'target' => $this->getTarget(), 'nonce' => $this->CSP->getNonce(), @@ -3332,6 +3339,8 @@ class OutputPage extends ContextSource { 'safemode' => ( $this->getAllowedModules( RL\Module::TYPE_COMBINED ) <= RL\Module::ORIGIN_CORE_INDIVIDUAL ) ? '1' : null, + 'clientPrefEnabled' => $clientPrefEnabled, + 'clientPrefCookiePrefix' => $clientPrefCookiePrefix, ] ); $rlClient->setConfig( $this->getJSVars( self::JS_VAR_EARLY ) ); $rlClient->setModules( $this->getModules( /*filter*/ true ) ); diff --git a/includes/ResourceLoader/ClientHtml.php b/includes/ResourceLoader/ClientHtml.php index 4ca1ebd12ba..260d5ce9c6a 100644 --- a/includes/ResourceLoader/ClientHtml.php +++ b/includes/ResourceLoader/ClientHtml.php @@ -21,7 +21,6 @@ namespace MediaWiki\ResourceLoader; use Html; -use MediaWiki\MainConfigNames; use Wikimedia\WrappedString; use Wikimedia\WrappedStringList; @@ -62,6 +61,8 @@ class ClientHtml { * - 'target': Parameter for modules=startup request, see StartUpModule. * - 'safemode': Parameter for modules=startup request, see StartUpModule. * - 'nonce': From OutputPage->getCSP->getNonce(). + * - 'clientPrefEnabled': See $wgResourceLoaderClientPreferences. + * - 'clientPrefCookiePrefix': See $wgResourceLoaderClientPreferences. */ public function __construct( Context $context, array $options = [] ) { $this->context = $context; @@ -70,6 +71,8 @@ class ClientHtml { 'target' => null, 'safemode' => null, 'nonce' => null, + 'clientPrefEnabled' => false, + 'clientPrefCookiePrefix' => '', ]; } @@ -235,6 +238,52 @@ class ClientHtml { return [ 'class' => 'client-nojs' ]; } + /** + * Set relevant classes on document.documentElement + * + * @param string|null $nojsClass Class name that Skin will set on HTML document + * @return string + */ + private function getDocumentClassNameScript( $nojsClass ) { + // Change "client-nojs" to "client-js". + // This enables server rendering of UI components, even for those that should be hidden + // in Grade C where JavaScript is unsupported, whilst avoiding a flash of wrong content. + // + // See also Skin:getHtmlElementAttributes() and startup/startup.js. + // + // Optimisation: Produce shorter and faster JS by only writing to DOM. Avoid reading + // HTMLElement.className and executing JS regexes by doing the string replace in PHP. + // This is possible because Skin informs RL about the final value of , and + // because RL already controls the first element in HTML for performance reasons. + $nojsClass ??= $this->getDocumentAttributes()['class']; + $jsClass = preg_replace( '/(^|\s)client-nojs(\s|$)/', '$1client-js$2', $nojsClass ); + $jsClassJson = $this->context->encodeJson( $jsClass ); + $script = " +document.documentElement.className = {$jsClassJson}; +"; + + if ( $this->options['clientPrefEnabled'] ) { + $cookiePrefix = $this->options['clientPrefCookiePrefix']; + $script .= <<getData(); $chunks = []; - // Change "client-nojs" class to client-js. This allows easy toggling of UI components. - // This must happen synchronously on every page view to avoid flashes of wrong content. - // See also startup/startup.js. - $nojsClass ??= $this->getDocumentAttributes()['class']; - $jsClass = preg_replace( '/(^|\s)client-nojs(\s|$)/', '$1client-js$2', $nojsClass ); - $jsClassJson = $this->context->encodeJson( $jsClass ); - $script = " -document.documentElement.className = {$jsClassJson}; -"; + $script = $this->getDocumentClassNameScript( $nojsClass ); // Inline script: Declare mw.config variables for this page. if ( $this->config ) { @@ -290,15 +331,6 @@ RLPAGEMODULES = {$pageModulesJson}; "; } - $config = $this->resourceLoader->getConfig(); - $user = $this->context->getUserIdentity(); - $isAnon = !$user || !$user->isRegistered(); - // This code is only loaded for anonymous users. Logged in users should use preferences. - if ( $config->get( MainConfigNames::ResourceLoaderClientPreferences ) && $isAnon ) { - $script .= $this->getClientSidePreferencesScript( - $config->get( MainConfigNames::CookiePrefix ) - ); - } if ( !$this->context->getDebug() ) { $script = ResourceLoader::filter( 'minify-js', $script, [ 'cache' => false ] ); } @@ -393,32 +425,6 @@ RLPAGEMODULES = {$pageModulesJson}; return $ret; } - /** - * Adds ability for anonymous users to change classes on document.documentElement - * - * @param string $cookiePrefix - * @return string - */ - private function getClientSidePreferencesScript( string $cookiePrefix ) { - return <<