Merge "resourceloader: Add $context to static functions in ResourceLoader"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 27 Sep 2019 20:48:15 +0000 (20:48 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 27 Sep 2019 20:48:15 +0000 (20:48 +0000)
1  2 
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderClientHtml.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderFileModule.php
includes/resourceloader/ResourceLoaderLanguageDataModule.php
includes/resourceloader/ResourceLoaderModule.php
includes/resourceloader/ResourceLoaderStartUpModule.php
includes/resourceloader/ResourceLoaderUserDefaultsModule.php
includes/resourceloader/ResourceLoaderUserOptionsModule.php
includes/resourceloader/ResourceLoaderUserTokensModule.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php

@@@ -1,5 -1,7 +1,5 @@@
  <?php
  /**
 - * Base class for resource loading system.
 - *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
@@@ -28,21 -30,13 +28,21 @@@ use Wikimedia\Rdbms\DBConnectionError
  use Wikimedia\WrappedString;
  
  /**
 - * Dynamic JavaScript and CSS resource loading system.
 + * @defgroup ResourceLoader ResourceLoader
 + *
 + * For higher level documentation, see <https://www.mediawiki.org/wiki/ResourceLoader/Architecture>.
 + */
 +
 +/**
 + * ResourceLoader is a loading system for JavaScript and CSS resources.
 + *
 + * For higher level documentation, see <https://www.mediawiki.org/wiki/ResourceLoader/Architecture>.
   *
 - * Most of the documentation is on the MediaWiki documentation wiki starting at:
 - *    https://www.mediawiki.org/wiki/ResourceLoader
 + * @ingroup ResourceLoader
 + * @since 1.17
   */
  class ResourceLoader implements LoggerAwareInterface {
 -      /** @var Config $config */
 +      /** @var Config */
        protected $config;
        /** @var MessageBlobStore */
        protected $blobStore;
                        $errorResponse = self::makeComment( $errorText );
                        if ( $context->shouldIncludeScripts() ) {
                                $errorResponse .= 'if (window.console && console.error) { console.error('
-                                       . self::encodeJsonForScript( $errorText )
+                                       . $context->encodeJson( $errorText )
                                        . "); }\n";
                        }
  
@@@ -1093,7 -1087,14 +1093,14 @@@ MESSAGE
                                                        $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 )
                        . ');';
        }
  
@@@ -24,7 -24,6 +24,7 @@@ use Wikimedia\WrappedStringList
  /**
   * Load and configure a ResourceLoader client on an HTML page.
   *
 + * @ingroup ResourceLoader
   * @since 1.28
   */
  class ResourceLoaderClientHtml {
                                // 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
                                                        );
                                                }
@@@ -1,5 -1,7 +1,5 @@@
  <?php
  /**
 - * Context for ResourceLoader modules.
 - *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
@@@ -24,11 -26,8 +24,11 @@@ use MediaWiki\Logger\LoggerFactory
  use MediaWiki\MediaWikiServices;
  
  /**
 - * Object passed around to modules which contains information about the state
 - * of a specific loader request.
 + * Context object that contains information about the state of a specific
 + * ResourceLoader web request. Passed around to ResourceLoaderModule methods.
 + *
 + * @ingroup ResourceLoader
 + * @since 1.17
   */
  class ResourceLoaderContext implements MessageLocalizer {
        const DEFAULT_LANG = 'qqx';
        /**
         * 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 );
+       }
  }
@@@ -1,5 -1,7 +1,5 @@@
  <?php
  /**
 - * ResourceLoader module based on local JavaScript/CSS files.
 - *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
   */
  
  /**
 - * ResourceLoader module based on local JavaScript/CSS files.
 + * Module based on local JavaScript/CSS files.
   *
   * The following public methods can query the database:
   *
   * - getDefinitionSummary / â€¦ / ResourceLoaderModule::getFileDependencies.
   * - getVersionHash / getDefinitionSummary / â€¦ / ResourceLoaderModule::getFileDependencies.
   * - getStyles / ResourceLoaderModule::saveFileDependencies.
 + *
 + * @ingroup ResourceLoader
 + * @since 1.17
   */
  class ResourceLoaderFileModule extends ResourceLoaderModule {
  
         * @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 ) {
         *     keyed by media type
         * @throws MWException
         */
 -      public function readStyleFiles( array $styles, $flip, $context ) {
 +      public function readStyleFiles( array $styles, $flip, ResourceLoaderContext $context ) {
                if ( !$styles ) {
                        return [];
                }
         * @return string CSS data in script file
         * @throws MWException If the file doesn't exist
         */
 -      protected function readStyleFile( $path, $flip, $context ) {
 +      protected function readStyleFile( $path, $flip, ResourceLoaderContext $context ) {
                $localPath = $this->getLocalPath( $path );
                $remotePath = $this->getRemotePath( $path );
                if ( !file_exists( $localPath ) ) {
         * @param ResourceLoaderContext $context
         * @return bool
         */
 -      public function getFlip( $context ) {
 +      public function getFlip( ResourceLoaderContext $context ) {
                return $context->getDirection() === 'rtl' && !$this->noflip;
        }
  
   */
  
  /**
 - * ResourceLoader module for populating language specific data, such as grammar forms.
 + * Module for populating language specific data, such as grammar forms.
 + *
 + * @ingroup ResourceLoader
 + * @internal
   */
  class ResourceLoaderLanguageDataModule extends ResourceLoaderFileModule {
  
@@@ -57,8 -54,8 +57,8 @@@
        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 ) )
                        . ');';
        }
  
@@@ -1,5 -1,7 +1,5 @@@
  <?php
  /**
 - * Abstraction for ResourceLoader modules.
 - *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
@@@ -30,9 -32,6 +30,9 @@@ use Wikimedia\ScopedCallback
  
  /**
   * Abstraction for ResourceLoader modules, with name registration and maxage functionality.
 + *
 + * @ingroup ResourceLoader
 + * @since 1.17
   */
  abstract class ResourceLoaderModule implements LoggerAwareInterface {
        /** @var Config */
         * @param ResourceLoaderContext $context
         * @return bool
         */
 -      public function getFlip( $context ) {
 +      public function getFlip( ResourceLoaderContext $context ) {
                return MediaWikiServices::getInstance()->getContentLanguage()->getDir() !==
                        $context->getDirection();
        }
        /**
         * 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 '';
                }
@@@ -38,9 -38,6 +38,9 @@@ use MediaWiki\MediaWikiServices
   * - safemode: Only register modules that have ORIGIN_CORE as their origin.
   *   This effectively disables ORIGIN_USER modules. (T185303)
   *   See also: OutputPage::disallowUserJs()
 + *
 + * @ingroup ResourceLoader
 + * @internal
   */
  class ResourceLoaderStartUpModule extends ResourceLoaderModule {
        protected $targets = [ 'desktop', 'mobile' ];
@@@ -58,7 -55,7 +58,7 @@@
         * @param ResourceLoaderContext $context
         * @return array
         */
 -      private function getConfigSettings( $context ) {
 +      private function getConfigSettings( ResourceLoaderContext $context ) {
                $conf = $this->getConfig();
  
                /**
                        }
  
                        $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)
@@@ -1,5 -1,7 +1,5 @@@
  <?php
  /**
 - * ResourceLoader module for default user preferences.
 - *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
@@@ -21,9 -23,6 +21,9 @@@
  
  /**
   * Module for default user preferences.
 + *
 + * @ingroup ResourceLoader
 + * @internal
   */
  class ResourceLoaderUserDefaultsModule extends ResourceLoaderModule {
  
@@@ -42,7 -41,7 +42,7 @@@
         */
        public function getScript( ResourceLoaderContext $context ) {
                return 'mw.user.options.set('
-                       . ResourceLoader::encodeJsonForScript( User::getDefaultOptions() )
+                       . $context->encodeJson( User::getDefaultOptions() )
                        . ');';
        }
  }
@@@ -1,5 -1,7 +1,5 @@@
  <?php
  /**
 - * ResourceLoader module for user preference customizations.
 - *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
   */
  
  /**
 - * Module for user preference customizations
 + * Module for user preferences.
 + *
 + * @ingroup ResourceLoader
 + * @internal
   */
  class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
  
@@@ -55,7 -54,7 +55,7 @@@
                // 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 )
                        )
                        . ');';
@@@ -1,5 -1,7 +1,5 @@@
  <?php
  /**
 - * ResourceLoader module for user tokens.
 - *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
   * http://www.gnu.org/copyleft/gpl.html
   *
   * @file
 - * @author Krinkle
   */
  
  /**
 - * Module for user tokens
 + * Module for user authorization tokens.
 + *
 + * @ingroup ResourceLoader
 + * @internal
   */
  class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
  
@@@ -55,7 -55,7 +55,7 @@@
                // 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 ) )
                        . ');';
        }
  
@@@ -518,13 -518,14 +518,14 @@@ EN
                        '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, [] )
                );
        }
  
                ] );
                $context = $this->getResourceLoaderContext( [], $rl );
  
 -              $this->assertEquals(
 +              $this->assertSame(
                        '',
                        $rl->getCombinedVersion( $context, [] ),
                        'empty list'