summaryrefslogtreecommitdiff
path: root/www/wiki/includes/resourceloader
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/resourceloader
first commit
Diffstat (limited to 'www/wiki/includes/resourceloader')
-rw-r--r--www/wiki/includes/resourceloader/DerivativeResourceLoaderContext.php199
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoader.php1731
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php472
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderContext.php395
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderEditToolbarModule.php44
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderFileModule.php1038
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderFilePath.php71
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderForeignApiModule.php33
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderImage.php413
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderImageModule.php471
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderJqueryMsgModule.php82
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php81
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderLanguageNamesModule.php77
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php53
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderModule.php1044
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderOOUIFileModule.php98
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php110
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderOOUIModule.php146
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderRawFileModule.php52
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderSiteModule.php52
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderSiteStylesModule.php60
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php167
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php102
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php458
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUploadDialogModule.php49
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserDefaultsModule.php49
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserModule.php87
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php83
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserStylesModule.php86
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php76
-rw-r--r--www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php473
31 files changed, 8352 insertions, 0 deletions
diff --git a/www/wiki/includes/resourceloader/DerivativeResourceLoaderContext.php b/www/wiki/includes/resourceloader/DerivativeResourceLoaderContext.php
new file mode 100644
index 00000000..418d17f3
--- /dev/null
+++ b/www/wiki/includes/resourceloader/DerivativeResourceLoaderContext.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * Derivative 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Kunal Mehta
+ */
+
+/**
+ * Allows changing specific properties of a context object,
+ * without changing the main one. Inspired by DerivativeContext.
+ *
+ * @since 1.24
+ */
+class DerivativeResourceLoaderContext extends ResourceLoaderContext {
+ const INHERIT_VALUE = -1;
+
+ /**
+ * @var ResourceLoaderContext
+ */
+ private $context;
+
+ protected $modules = self::INHERIT_VALUE;
+ protected $language = self::INHERIT_VALUE;
+ protected $direction = self::INHERIT_VALUE;
+ protected $skin = self::INHERIT_VALUE;
+ protected $user = self::INHERIT_VALUE;
+ protected $debug = self::INHERIT_VALUE;
+ protected $only = self::INHERIT_VALUE;
+ protected $version = self::INHERIT_VALUE;
+ protected $raw = self::INHERIT_VALUE;
+
+ public function __construct( ResourceLoaderContext $context ) {
+ $this->context = $context;
+ }
+
+ public function getModules() {
+ if ( $this->modules === self::INHERIT_VALUE ) {
+ return $this->context->getModules();
+ }
+ return $this->modules;
+ }
+
+ /**
+ * @param string[] $modules
+ */
+ public function setModules( array $modules ) {
+ $this->modules = $modules;
+ }
+
+ public function getLanguage() {
+ if ( $this->language === self::INHERIT_VALUE ) {
+ return $this->context->getLanguage();
+ }
+ return $this->language;
+ }
+
+ /**
+ * @param string $language
+ */
+ public function setLanguage( $language ) {
+ $this->language = $language;
+ // Invalidate direction since it is based on language
+ $this->direction = null;
+ $this->hash = null;
+ }
+
+ public function getDirection() {
+ if ( $this->direction === self::INHERIT_VALUE ) {
+ return $this->context->getDirection();
+ }
+ if ( $this->direction === null ) {
+ $this->direction = Language::factory( $this->getLanguage() )->getDir();
+ }
+ return $this->direction;
+ }
+
+ /**
+ * @param string $direction
+ */
+ public function setDirection( $direction ) {
+ $this->direction = $direction;
+ $this->hash = null;
+ }
+
+ public function getSkin() {
+ if ( $this->skin === self::INHERIT_VALUE ) {
+ return $this->context->getSkin();
+ }
+ return $this->skin;
+ }
+
+ /**
+ * @param string $skin
+ */
+ public function setSkin( $skin ) {
+ $this->skin = $skin;
+ $this->hash = null;
+ }
+
+ public function getUser() {
+ if ( $this->user === self::INHERIT_VALUE ) {
+ return $this->context->getUser();
+ }
+ return $this->user;
+ }
+
+ /**
+ * @param string|null $user
+ */
+ public function setUser( $user ) {
+ $this->user = $user;
+ $this->hash = null;
+ $this->userObj = null;
+ }
+
+ public function getDebug() {
+ if ( $this->debug === self::INHERIT_VALUE ) {
+ return $this->context->getDebug();
+ }
+ return $this->debug;
+ }
+
+ /**
+ * @param bool $debug
+ */
+ public function setDebug( $debug ) {
+ $this->debug = $debug;
+ $this->hash = null;
+ }
+
+ public function getOnly() {
+ if ( $this->only === self::INHERIT_VALUE ) {
+ return $this->context->getOnly();
+ }
+ return $this->only;
+ }
+
+ /**
+ * @param string|null $only
+ */
+ public function setOnly( $only ) {
+ $this->only = $only;
+ $this->hash = null;
+ }
+
+ public function getVersion() {
+ if ( $this->version === self::INHERIT_VALUE ) {
+ return $this->context->getVersion();
+ }
+ return $this->version;
+ }
+
+ /**
+ * @param string|null $version
+ */
+ public function setVersion( $version ) {
+ $this->version = $version;
+ $this->hash = null;
+ }
+
+ public function getRaw() {
+ if ( $this->raw === self::INHERIT_VALUE ) {
+ return $this->context->getRaw();
+ }
+ return $this->raw;
+ }
+
+ /**
+ * @param bool $raw
+ */
+ public function setRaw( $raw ) {
+ $this->raw = $raw;
+ }
+
+ public function getRequest() {
+ return $this->context->getRequest();
+ }
+
+ public function getResourceLoader() {
+ return $this->context->getResourceLoader();
+ }
+
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoader.php b/www/wiki/includes/resourceloader/ResourceLoader.php
new file mode 100644
index 00000000..90c31406
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoader.php
@@ -0,0 +1,1731 @@
+<?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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Roan Kattouw
+ * @author Trevor Parscal
+ */
+
+use MediaWiki\MediaWikiServices;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Wikimedia\Rdbms\DBConnectionError;
+use Wikimedia\WrappedString;
+
+/**
+ * Dynamic JavaScript and CSS resource loading system.
+ *
+ * Most of the documentation is on the MediaWiki documentation wiki starting at:
+ * https://www.mediawiki.org/wiki/ResourceLoader
+ */
+class ResourceLoader implements LoggerAwareInterface {
+ /** @var int */
+ protected static $filterCacheVersion = 7;
+
+ /** @var bool */
+ protected static $debugMode = null;
+
+ /** @var array */
+ private $lessVars = null;
+
+ /**
+ * Module name/ResourceLoaderModule object pairs
+ * @var array
+ */
+ protected $modules = [];
+
+ /**
+ * Associative array mapping module name to info associative array
+ * @var array
+ */
+ protected $moduleInfos = [];
+
+ /** @var Config $config */
+ protected $config;
+
+ /**
+ * Associative array mapping framework ids to a list of names of test suite modules
+ * like [ 'qunit' => [ 'mediawiki.tests.qunit.suites', 'ext.foo.tests', ... ], ... ]
+ * @var array
+ */
+ protected $testModuleNames = [];
+
+ /**
+ * E.g. [ 'source-id' => 'http://.../load.php' ]
+ * @var array
+ */
+ protected $sources = [];
+
+ /**
+ * Errors accumulated during current respond() call.
+ * @var array
+ */
+ protected $errors = [];
+
+ /**
+ * List of extra HTTP response headers provided by loaded modules.
+ *
+ * Populated by makeModuleResponse().
+ *
+ * @var array
+ */
+ protected $extraHeaders = [];
+
+ /**
+ * @var MessageBlobStore
+ */
+ protected $blobStore;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /** @var string JavaScript / CSS pragma to disable minification. **/
+ const FILTER_NOMIN = '/*@nomin*/';
+
+ /**
+ * Load information stored in the database about modules.
+ *
+ * This method grabs modules dependencies from the database and updates modules
+ * objects.
+ *
+ * This is not inside the module code because it is much faster to
+ * request all of the information at once than it is to have each module
+ * requests its own information. This sacrifice of modularity yields a substantial
+ * performance improvement.
+ *
+ * @param array $moduleNames List of module names to preload information for
+ * @param ResourceLoaderContext $context Context to load the information within
+ */
+ public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) {
+ if ( !$moduleNames ) {
+ // Or else Database*::select() will explode, plus it's cheaper!
+ return;
+ }
+ $dbr = wfGetDB( DB_REPLICA );
+ $skin = $context->getSkin();
+ $lang = $context->getLanguage();
+
+ // Batched version of ResourceLoaderModule::getFileDependencies
+ $vary = "$skin|$lang";
+ $res = $dbr->select( 'module_deps', [ 'md_module', 'md_deps' ], [
+ 'md_module' => $moduleNames,
+ 'md_skin' => $vary,
+ ], __METHOD__
+ );
+
+ // Prime in-object cache for file dependencies
+ $modulesWithDeps = [];
+ foreach ( $res as $row ) {
+ $module = $this->getModule( $row->md_module );
+ if ( $module ) {
+ $module->setFileDependencies( $context, ResourceLoaderModule::expandRelativePaths(
+ FormatJson::decode( $row->md_deps, true )
+ ) );
+ $modulesWithDeps[] = $row->md_module;
+ }
+ }
+ // Register the absence of a dependency row too
+ foreach ( array_diff( $moduleNames, $modulesWithDeps ) as $name ) {
+ $module = $this->getModule( $name );
+ if ( $module ) {
+ $this->getModule( $name )->setFileDependencies( $context, [] );
+ }
+ }
+
+ // Batched version of ResourceLoaderWikiModule::getTitleInfo
+ ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames );
+
+ // Prime in-object cache for message blobs for modules with messages
+ $modules = [];
+ foreach ( $moduleNames as $name ) {
+ $module = $this->getModule( $name );
+ if ( $module && $module->getMessages() ) {
+ $modules[$name] = $module;
+ }
+ }
+ $store = $this->getMessageBlobStore();
+ $blobs = $store->getBlobs( $modules, $lang );
+ foreach ( $blobs as $name => $blob ) {
+ $modules[$name]->setMessageBlob( $blob, $lang );
+ }
+ }
+
+ /**
+ * Run JavaScript or CSS data through a filter, caching the filtered result for future calls.
+ *
+ * Available filters are:
+ *
+ * - minify-js \see JavaScriptMinifier::minify
+ * - minify-css \see CSSMin::minify
+ *
+ * If $data is empty, only contains whitespace or the filter was unknown,
+ * $data is returned unmodified.
+ *
+ * @param string $filter Name of filter to run
+ * @param string $data Text to filter, such as JavaScript or CSS text
+ * @param array $options Keys:
+ * - (bool) cache: Whether to allow caching this data. Default: true.
+ * @return string Filtered data, or a comment containing an error message
+ */
+ public static function filter( $filter, $data, array $options = [] ) {
+ if ( strpos( $data, self::FILTER_NOMIN ) !== false ) {
+ return $data;
+ }
+
+ if ( isset( $options['cache'] ) && $options['cache'] === false ) {
+ return self::applyFilter( $filter, $data );
+ }
+
+ $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
+
+ $key = $cache->makeGlobalKey(
+ 'resourceloader',
+ 'filter',
+ $filter,
+ self::$filterCacheVersion, md5( $data )
+ );
+
+ $result = $cache->get( $key );
+ if ( $result === false ) {
+ $stats->increment( "resourceloader_cache.$filter.miss" );
+ $result = self::applyFilter( $filter, $data );
+ $cache->set( $key, $result, 24 * 3600 );
+ } else {
+ $stats->increment( "resourceloader_cache.$filter.hit" );
+ }
+ if ( $result === null ) {
+ // Cached failure
+ $result = $data;
+ }
+
+ return $result;
+ }
+
+ private static function applyFilter( $filter, $data ) {
+ $data = trim( $data );
+ if ( $data ) {
+ try {
+ $data = ( $filter === 'minify-css' )
+ ? CSSMin::minify( $data )
+ : JavaScriptMinifier::minify( $data );
+ } catch ( Exception $e ) {
+ MWExceptionHandler::logException( $e );
+ return null;
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Register core modules and runs registration hooks.
+ * @param Config $config [optional]
+ * @param LoggerInterface $logger [optional]
+ */
+ public function __construct( Config $config = null, LoggerInterface $logger = null ) {
+ global $IP;
+
+ $this->logger = $logger ?: new NullLogger();
+
+ if ( !$config ) {
+ $this->logger->debug( __METHOD__ . ' was called without providing a Config instance' );
+ $config = MediaWikiServices::getInstance()->getMainConfig();
+ }
+ $this->config = $config;
+
+ // Add 'local' source first
+ $this->addSource( 'local', $config->get( 'LoadScript' ) );
+
+ // Add other sources
+ $this->addSource( $config->get( 'ResourceLoaderSources' ) );
+
+ // Register core modules
+ $this->register( include "$IP/resources/Resources.php" );
+ // Register extension modules
+ $this->register( $config->get( 'ResourceModules' ) );
+
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $rl = $this;
+ Hooks::run( 'ResourceLoaderRegisterModules', [ &$rl ] );
+
+ if ( $config->get( 'EnableJavaScriptTest' ) === true ) {
+ $this->registerTestModules();
+ }
+
+ $this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) );
+ }
+
+ /**
+ * @return Config
+ */
+ public function getConfig() {
+ return $this->config;
+ }
+
+ /**
+ * @since 1.26
+ * @param LoggerInterface $logger
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @since 1.27
+ * @return LoggerInterface
+ */
+ public function getLogger() {
+ return $this->logger;
+ }
+
+ /**
+ * @since 1.26
+ * @return MessageBlobStore
+ */
+ public function getMessageBlobStore() {
+ return $this->blobStore;
+ }
+
+ /**
+ * @since 1.25
+ * @param MessageBlobStore $blobStore
+ */
+ public function setMessageBlobStore( MessageBlobStore $blobStore ) {
+ $this->blobStore = $blobStore;
+ }
+
+ /**
+ * Register a module with the ResourceLoader system.
+ *
+ * @param mixed $name Name of module as a string or List of name/object pairs as an array
+ * @param array $info Module info array. For backwards compatibility with 1.17alpha,
+ * this may also be a ResourceLoaderModule object. Optional when using
+ * multiple-registration calling style.
+ * @throws MWException If a duplicate module registration is attempted
+ * @throws MWException If a module name contains illegal characters (pipes or commas)
+ * @throws MWException If something other than a ResourceLoaderModule is being registered
+ * @return bool False if there were any errors, in which case one or more modules were
+ * not registered
+ */
+ public function register( $name, $info = null ) {
+ $moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
+
+ // Allow multiple modules to be registered in one call
+ $registrations = is_array( $name ) ? $name : [ $name => $info ];
+ foreach ( $registrations as $name => $info ) {
+ // Warn on duplicate registrations
+ if ( isset( $this->moduleInfos[$name] ) ) {
+ // A module has already been registered by this name
+ $this->logger->warning(
+ 'ResourceLoader duplicate registration warning. ' .
+ 'Another module has already been registered as ' . $name
+ );
+ }
+
+ // Check $name for validity
+ if ( !self::isValidModuleName( $name ) ) {
+ throw new MWException( "ResourceLoader module name '$name' is invalid, "
+ . "see ResourceLoader::isValidModuleName()" );
+ }
+
+ // Attach module
+ if ( $info instanceof ResourceLoaderModule ) {
+ $this->moduleInfos[$name] = [ 'object' => $info ];
+ $info->setName( $name );
+ $this->modules[$name] = $info;
+ } elseif ( is_array( $info ) ) {
+ // New calling convention
+ $this->moduleInfos[$name] = $info;
+ } else {
+ throw new MWException(
+ 'ResourceLoader module info type error for module \'' . $name .
+ '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')'
+ );
+ }
+
+ // Last-minute changes
+
+ // Apply custom skin-defined styles to existing modules.
+ if ( $this->isFileModule( $name ) ) {
+ foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
+ // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
+ if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) {
+ continue;
+ }
+
+ // If $name is preceded with a '+', the defined style files will be added to 'default'
+ // skinStyles, otherwise 'default' will be ignored as it normally would be.
+ if ( isset( $skinStyles[$name] ) ) {
+ $paths = (array)$skinStyles[$name];
+ $styleFiles = [];
+ } elseif ( isset( $skinStyles['+' . $name] ) ) {
+ $paths = (array)$skinStyles['+' . $name];
+ $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ?
+ (array)$this->moduleInfos[$name]['skinStyles']['default'] :
+ [];
+ } else {
+ continue;
+ }
+
+ // Add new file paths, remapping them to refer to our directories and not use settings
+ // from the module we're modifying, which come from the base definition.
+ list( $localBasePath, $remoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths( $skinStyles );
+
+ foreach ( $paths as $path ) {
+ $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
+ }
+
+ $this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles;
+ }
+ }
+ }
+ }
+
+ public function registerTestModules() {
+ global $IP;
+
+ if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) {
+ throw new MWException( 'Attempt to register JavaScript test modules '
+ . 'but <code>$wgEnableJavaScriptTest</code> is false. '
+ . 'Edit your <code>LocalSettings.php</code> to enable it.' );
+ }
+
+ // Get core test suites
+ $testModules = [];
+ $testModules['qunit'] = [];
+ // Get other test suites (e.g. from extensions)
+ // Avoid PHP 7.1 warning from passing $this by reference
+ $rl = $this;
+ Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$rl ] );
+
+ // Add the testrunner (which configures QUnit) to the dependencies.
+ // Since it must be ready before any of the test suites are executed.
+ foreach ( $testModules['qunit'] as &$module ) {
+ // Make sure all test modules are top-loading so that when QUnit starts
+ // on document-ready, it will run once and finish. If some tests arrive
+ // later (possibly after QUnit has already finished) they will be ignored.
+ $module['position'] = 'top';
+ $module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
+ }
+
+ $testModules['qunit'] =
+ ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit'];
+
+ foreach ( $testModules as $id => $names ) {
+ // Register test modules
+ $this->register( $testModules[$id] );
+
+ // Keep track of their names so that they can be loaded together
+ $this->testModuleNames[$id] = array_keys( $testModules[$id] );
+ }
+ }
+
+ /**
+ * Add a foreign source of modules.
+ *
+ * Source IDs are typically the same as the Wiki ID or database name (e.g. lowercase a-z).
+ *
+ * @param array|string $id Source ID (string), or [ id1 => loadUrl, id2 => loadUrl, ... ]
+ * @param string|array $loadUrl load.php url (string), or array with loadUrl key for
+ * backwards-compatibility.
+ * @throws MWException
+ */
+ public function addSource( $id, $loadUrl = null ) {
+ // Allow multiple sources to be registered in one call
+ if ( is_array( $id ) ) {
+ foreach ( $id as $key => $value ) {
+ $this->addSource( $key, $value );
+ }
+ return;
+ }
+
+ // Disallow duplicates
+ if ( isset( $this->sources[$id] ) ) {
+ throw new MWException(
+ 'ResourceLoader duplicate source addition error. ' .
+ 'Another source has already been registered as ' . $id
+ );
+ }
+
+ // Pre 1.24 backwards-compatibility
+ if ( is_array( $loadUrl ) ) {
+ if ( !isset( $loadUrl['loadScript'] ) ) {
+ throw new MWException(
+ __METHOD__ . ' was passed an array with no "loadScript" key.'
+ );
+ }
+
+ $loadUrl = $loadUrl['loadScript'];
+ }
+
+ $this->sources[$id] = $loadUrl;
+ }
+
+ /**
+ * Get a list of module names.
+ *
+ * @return array List of module names
+ */
+ public function getModuleNames() {
+ return array_keys( $this->moduleInfos );
+ }
+
+ /**
+ * Get a list of test module names for one (or all) frameworks.
+ *
+ * If the given framework id is unknkown, or if the in-object variable is not an array,
+ * then it will return an empty array.
+ *
+ * @param string $framework Get only the test module names for one
+ * particular framework (optional)
+ * @return array
+ */
+ public function getTestModuleNames( $framework = 'all' ) {
+ /** @todo api siteinfo prop testmodulenames modulenames */
+ if ( $framework == 'all' ) {
+ return $this->testModuleNames;
+ } elseif ( isset( $this->testModuleNames[$framework] )
+ && is_array( $this->testModuleNames[$framework] )
+ ) {
+ return $this->testModuleNames[$framework];
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Check whether a ResourceLoader module is registered
+ *
+ * @since 1.25
+ * @param string $name
+ * @return bool
+ */
+ public function isModuleRegistered( $name ) {
+ return isset( $this->moduleInfos[$name] );
+ }
+
+ /**
+ * Get the ResourceLoaderModule object for a given module name.
+ *
+ * If an array of module parameters exists but a ResourceLoaderModule object has not
+ * yet been instantiated, this method will instantiate and cache that object such that
+ * subsequent calls simply return the same object.
+ *
+ * @param string $name Module name
+ * @return ResourceLoaderModule|null If module has been registered, return a
+ * ResourceLoaderModule instance. Otherwise, return null.
+ */
+ public function getModule( $name ) {
+ if ( !isset( $this->modules[$name] ) ) {
+ if ( !isset( $this->moduleInfos[$name] ) ) {
+ // No such module
+ return null;
+ }
+ // Construct the requested object
+ $info = $this->moduleInfos[$name];
+ /** @var ResourceLoaderModule $object */
+ if ( isset( $info['object'] ) ) {
+ // Object given in info array
+ $object = $info['object'];
+ } elseif ( isset( $info['factory'] ) ) {
+ $object = call_user_func( $info['factory'], $info );
+ $object->setConfig( $this->getConfig() );
+ $object->setLogger( $this->logger );
+ } else {
+ if ( !isset( $info['class'] ) ) {
+ $class = ResourceLoaderFileModule::class;
+ } else {
+ $class = $info['class'];
+ }
+ /** @var ResourceLoaderModule $object */
+ $object = new $class( $info );
+ $object->setConfig( $this->getConfig() );
+ $object->setLogger( $this->logger );
+ }
+ $object->setName( $name );
+ $this->modules[$name] = $object;
+ }
+
+ return $this->modules[$name];
+ }
+
+ /**
+ * Return whether the definition of a module corresponds to a simple ResourceLoaderFileModule
+ * or one of its subclasses.
+ *
+ * @param string $name Module name
+ * @return bool
+ */
+ protected function isFileModule( $name ) {
+ if ( !isset( $this->moduleInfos[$name] ) ) {
+ return false;
+ }
+ $info = $this->moduleInfos[$name];
+ if ( isset( $info['object'] ) ) {
+ return false;
+ }
+ if (
+ isset( $info['class'] ) &&
+ $info['class'] !== ResourceLoaderFileModule::class &&
+ !is_subclass_of( $info['class'], ResourceLoaderFileModule::class )
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get the list of sources.
+ *
+ * @return array Like [ id => load.php url, ... ]
+ */
+ public function getSources() {
+ return $this->sources;
+ }
+
+ /**
+ * Get the URL to the load.php endpoint for the given
+ * ResourceLoader source
+ *
+ * @since 1.24
+ * @param string $source
+ * @throws MWException On an invalid $source name
+ * @return string
+ */
+ public function getLoadScript( $source ) {
+ if ( !isset( $this->sources[$source] ) ) {
+ throw new MWException( "The $source source was never registered in ResourceLoader." );
+ }
+ return $this->sources[$source];
+ }
+
+ /**
+ * @since 1.26
+ * @param string $value
+ * @return string Hash
+ */
+ public static function makeHash( $value ) {
+ $hash = hash( 'fnv132', $value );
+ return Wikimedia\base_convert( $hash, 16, 36, 7 );
+ }
+
+ /**
+ * Add an error to the 'errors' array and log it.
+ *
+ * Should only be called from within respond().
+ *
+ * @since 1.29
+ * @param Exception $e
+ * @param string $msg
+ * @param array $context
+ */
+ protected function outputErrorAndLog( Exception $e, $msg, array $context = [] ) {
+ MWExceptionHandler::logException( $e );
+ $this->logger->warning(
+ $msg,
+ $context + [ 'exception' => $e ]
+ );
+ $this->errors[] = self::formatExceptionNoComment( $e );
+ }
+
+ /**
+ * Helper method to get and combine versions of multiple modules.
+ *
+ * @since 1.26
+ * @param ResourceLoaderContext $context
+ * @param string[] $moduleNames List of known module names
+ * @return string Hash
+ */
+ public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) {
+ if ( !$moduleNames ) {
+ return '';
+ }
+ $hashes = array_map( function ( $module ) use ( $context ) {
+ try {
+ return $this->getModule( $module )->getVersionHash( $context );
+ } catch ( Exception $e ) {
+ // If modules fail to compute a version, do still consider the versions
+ // of other modules - don't set an empty string E-Tag for the whole request.
+ // See also T152266 and StartupModule::getModuleRegistrations().
+ $this->outputErrorAndLog( $e,
+ 'Calculating version for "{module}" failed: {exception}',
+ [
+ 'module' => $module,
+ ]
+ );
+ return '';
+ }
+ }, $moduleNames );
+ return self::makeHash( implode( '', $hashes ) );
+ }
+
+ /**
+ * Get the expected value of the 'version' query parameter.
+ *
+ * This is used by respond() to set a short Cache-Control header for requests with
+ * information newer than the current server has. This avoids pollution of edge caches.
+ * Typically during deployment. (T117587)
+ *
+ * This MUST match return value of `mw.loader#getCombinedVersion()` client-side.
+ *
+ * @since 1.28
+ * @param ResourceLoaderContext $context
+ * @return string Hash
+ */
+ public function makeVersionQuery( ResourceLoaderContext $context ) {
+ // As of MediaWiki 1.28, the server and client use the same algorithm for combining
+ // version hashes. There is no technical reason for this to be same, and for years the
+ // implementations differed. If getCombinedVersion in PHP (used for StartupModule and
+ // E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version'
+ // query parameter), then this method must continue to match the JS one.
+ $moduleNames = [];
+ foreach ( $context->getModules() as $name ) {
+ if ( !$this->getModule( $name ) ) {
+ // If a versioned request contains a missing module, the version is a mismatch
+ // as the client considered a module (and version) we don't have.
+ return '';
+ }
+ $moduleNames[] = $name;
+ }
+ return $this->getCombinedVersion( $context, $moduleNames );
+ }
+
+ /**
+ * Output a response to a load request, including the content-type header.
+ *
+ * @param ResourceLoaderContext $context Context in which a response should be formed
+ */
+ public function respond( ResourceLoaderContext $context ) {
+ // Buffer output to catch warnings. Normally we'd use ob_clean() on the
+ // top-level output buffer to clear warnings, but that breaks when ob_gzhandler
+ // is used: ob_clean() will clear the GZIP header in that case and it won't come
+ // back for subsequent output, resulting in invalid GZIP. So we have to wrap
+ // the whole thing in our own output buffer to be sure the active buffer
+ // doesn't use ob_gzhandler.
+ // See https://bugs.php.net/bug.php?id=36514
+ ob_start();
+
+ $this->measureResponseTime( RequestContext::getMain()->getTiming() );
+
+ // Find out which modules are missing and instantiate the others
+ $modules = [];
+ $missing = [];
+ foreach ( $context->getModules() as $name ) {
+ $module = $this->getModule( $name );
+ if ( $module ) {
+ // Do not allow private modules to be loaded from the web.
+ // This is a security issue, see T36907.
+ if ( $module->getGroup() === 'private' ) {
+ $this->logger->debug( "Request for private module '$name' denied" );
+ $this->errors[] = "Cannot show private module \"$name\"";
+ continue;
+ }
+ $modules[$name] = $module;
+ } else {
+ $missing[] = $name;
+ }
+ }
+
+ try {
+ // Preload for getCombinedVersion() and for batch makeModuleResponse()
+ $this->preloadModuleInfo( array_keys( $modules ), $context );
+ } catch ( Exception $e ) {
+ $this->outputErrorAndLog( $e, 'Preloading module info failed: {exception}' );
+ }
+
+ // Combine versions to propagate cache invalidation
+ $versionHash = '';
+ try {
+ $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
+ } catch ( Exception $e ) {
+ $this->outputErrorAndLog( $e, 'Calculating version hash failed: {exception}' );
+ }
+
+ // See RFC 2616 § 3.11 Entity Tags
+ // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
+ $etag = 'W/"' . $versionHash . '"';
+
+ // Try the client-side cache first
+ if ( $this->tryRespondNotModified( $context, $etag ) ) {
+ return; // output handled (buffers cleared)
+ }
+
+ // Use file cache if enabled and available...
+ if ( $this->config->get( 'UseFileCache' ) ) {
+ $fileCache = ResourceFileCache::newFromContext( $context );
+ if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
+ return; // output handled
+ }
+ }
+
+ // Generate a response
+ $response = $this->makeModuleResponse( $context, $modules, $missing );
+
+ // Capture any PHP warnings from the output buffer and append them to the
+ // error list if we're in debug mode.
+ if ( $context->getDebug() ) {
+ $warnings = ob_get_contents();
+ if ( strlen( $warnings ) ) {
+ $this->errors[] = $warnings;
+ }
+ }
+
+ // Save response to file cache unless there are errors
+ if ( isset( $fileCache ) && !$this->errors && !count( $missing ) ) {
+ // Cache single modules and images...and other requests if there are enough hits
+ if ( ResourceFileCache::useFileCache( $context ) ) {
+ if ( $fileCache->isCacheWorthy() ) {
+ $fileCache->saveText( $response );
+ } else {
+ $fileCache->incrMissesRecent( $context->getRequest() );
+ }
+ }
+ }
+
+ $this->sendResponseHeaders( $context, $etag, (bool)$this->errors, $this->extraHeaders );
+
+ // Remove the output buffer and output the response
+ ob_end_clean();
+
+ if ( $context->getImageObj() && $this->errors ) {
+ // We can't show both the error messages and the response when it's an image.
+ $response = implode( "\n\n", $this->errors );
+ } elseif ( $this->errors ) {
+ $errorText = implode( "\n\n", $this->errors );
+ $errorResponse = self::makeComment( $errorText );
+ if ( $context->shouldIncludeScripts() ) {
+ $errorResponse .= 'if (window.console && console.error) {'
+ . Xml::encodeJsCall( 'console.error', [ $errorText ] )
+ . "}\n";
+ }
+
+ // Prepend error info to the response
+ $response = $errorResponse . $response;
+ }
+
+ $this->errors = [];
+ echo $response;
+ }
+
+ protected function measureResponseTime( Timing $timing ) {
+ DeferredUpdates::addCallableUpdate( function () use ( $timing ) {
+ $measure = $timing->measure( 'responseTime', 'requestStart', 'requestShutdown' );
+ if ( $measure !== false ) {
+ $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $stats->timing( 'resourceloader.responseTime', $measure['duration'] * 1000 );
+ }
+ } );
+ }
+
+ /**
+ * Send main response headers to the client.
+ *
+ * Deals with Content-Type, CORS (for stylesheets), and caching.
+ *
+ * @param ResourceLoaderContext $context
+ * @param string $etag ETag header value
+ * @param bool $errors Whether there are errors in the response
+ * @param string[] $extra Array of extra HTTP response headers
+ * @return void
+ */
+ protected function sendResponseHeaders(
+ ResourceLoaderContext $context, $etag, $errors, array $extra = []
+ ) {
+ \MediaWiki\HeaderCallback::warnIfHeadersSent();
+ $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
+ // Use a short cache expiry so that updates propagate to clients quickly, if:
+ // - No version specified (shared resources, e.g. stylesheets)
+ // - There were errors (recover quickly)
+ // - Version mismatch (T117587, T47877)
+ if ( is_null( $context->getVersion() )
+ || $errors
+ || $context->getVersion() !== $this->makeVersionQuery( $context )
+ ) {
+ $maxage = $rlMaxage['unversioned']['client'];
+ $smaxage = $rlMaxage['unversioned']['server'];
+ // If a version was specified we can use a longer expiry time since changing
+ // version numbers causes cache misses
+ } else {
+ $maxage = $rlMaxage['versioned']['client'];
+ $smaxage = $rlMaxage['versioned']['server'];
+ }
+ if ( $context->getImageObj() ) {
+ // Output different headers if we're outputting textual errors.
+ if ( $errors ) {
+ header( 'Content-Type: text/plain; charset=utf-8' );
+ } else {
+ $context->getImageObj()->sendResponseHeaders( $context );
+ }
+ } elseif ( $context->getOnly() === 'styles' ) {
+ header( 'Content-Type: text/css; charset=utf-8' );
+ header( 'Access-Control-Allow-Origin: *' );
+ } else {
+ header( 'Content-Type: text/javascript; charset=utf-8' );
+ }
+ // See RFC 2616 § 14.19 ETag
+ // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
+ header( 'ETag: ' . $etag );
+ if ( $context->getDebug() ) {
+ // Do not cache debug responses
+ header( 'Cache-Control: private, no-cache, must-revalidate' );
+ header( 'Pragma: no-cache' );
+ } else {
+ header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
+ $exp = min( $maxage, $smaxage );
+ header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) );
+ }
+ foreach ( $extra as $header ) {
+ header( $header );
+ }
+ }
+
+ /**
+ * Respond with HTTP 304 Not Modified if appropiate.
+ *
+ * If there's an If-None-Match header, respond with a 304 appropriately
+ * and clear out the output buffer. If the client cache is too old then do nothing.
+ *
+ * @param ResourceLoaderContext $context
+ * @param string $etag ETag header value
+ * @return bool True if HTTP 304 was sent and output handled
+ */
+ protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
+ // See RFC 2616 § 14.26 If-None-Match
+ // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
+ $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
+ // Never send 304s in debug mode
+ if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
+ // There's another bug in ob_gzhandler (see also the comment at
+ // the top of this function) that causes it to gzip even empty
+ // responses, meaning it's impossible to produce a truly empty
+ // response (because the gzip header is always there). This is
+ // a problem because 304 responses have to be completely empty
+ // per the HTTP spec, and Firefox behaves buggily when they're not.
+ // See also https://bugs.php.net/bug.php?id=51579
+ // To work around this, we tear down all output buffering before
+ // sending the 304.
+ wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
+
+ HttpStatus::header( 304 );
+
+ $this->sendResponseHeaders( $context, $etag, false );
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Send out code for a response from file cache if possible.
+ *
+ * @param ResourceFileCache $fileCache Cache object for this request URL
+ * @param ResourceLoaderContext $context Context in which to generate a response
+ * @param string $etag ETag header value
+ * @return bool If this found a cache file and handled the response
+ */
+ protected function tryRespondFromFileCache(
+ ResourceFileCache $fileCache,
+ ResourceLoaderContext $context,
+ $etag
+ ) {
+ $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
+ // Buffer output to catch warnings.
+ ob_start();
+ // Get the maximum age the cache can be
+ $maxage = is_null( $context->getVersion() )
+ ? $rlMaxage['unversioned']['server']
+ : $rlMaxage['versioned']['server'];
+ // Minimum timestamp the cache file must have
+ $good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) );
+ if ( !$good ) {
+ try { // RL always hits the DB on file cache miss...
+ wfGetDB( DB_REPLICA );
+ } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
+ $good = $fileCache->isCacheGood(); // cache existence check
+ }
+ }
+ if ( $good ) {
+ $ts = $fileCache->cacheTimestamp();
+ // Send content type and cache headers
+ $this->sendResponseHeaders( $context, $etag, false );
+ $response = $fileCache->fetchText();
+ // Capture any PHP warnings from the output buffer and append them to the
+ // response in a comment if we're in debug mode.
+ if ( $context->getDebug() ) {
+ $warnings = ob_get_contents();
+ if ( strlen( $warnings ) ) {
+ $response = self::makeComment( $warnings ) . $response;
+ }
+ }
+ // Remove the output buffer and output the response
+ ob_end_clean();
+ echo $response . "\n/* Cached {$ts} */";
+ return true; // cache hit
+ }
+ // Clear buffer
+ ob_end_clean();
+
+ return false; // cache miss
+ }
+
+ /**
+ * Generate a CSS or JS comment block.
+ *
+ * Only use this for public data, not error message details.
+ *
+ * @param string $text
+ * @return string
+ */
+ public static function makeComment( $text ) {
+ $encText = str_replace( '*/', '* /', $text );
+ return "/*\n$encText\n*/\n";
+ }
+
+ /**
+ * Handle exception display.
+ *
+ * @param Exception $e Exception to be shown to the user
+ * @return string Sanitized text in a CSS/JS comment that can be returned to the user
+ */
+ public static function formatException( $e ) {
+ return self::makeComment( self::formatExceptionNoComment( $e ) );
+ }
+
+ /**
+ * Handle exception display.
+ *
+ * @since 1.25
+ * @param Exception $e Exception to be shown to the user
+ * @return string Sanitized text that can be returned to the user
+ */
+ protected static function formatExceptionNoComment( $e ) {
+ global $wgShowExceptionDetails;
+
+ if ( !$wgShowExceptionDetails ) {
+ return MWExceptionHandler::getPublicLogMessage( $e );
+ }
+
+ return MWExceptionHandler::getLogMessage( $e ) .
+ "\nBacktrace:\n" .
+ MWExceptionHandler::getRedactedTraceAsString( $e );
+ }
+
+ /**
+ * Generate code for a response.
+ *
+ * Calling this method also populates the `errors` and `headers` members,
+ * later used by respond().
+ *
+ * @param ResourceLoaderContext $context Context in which to generate a response
+ * @param ResourceLoaderModule[] $modules List of module objects keyed by module name
+ * @param string[] $missing List of requested module names that are unregistered (optional)
+ * @return string Response data
+ */
+ public function makeModuleResponse( ResourceLoaderContext $context,
+ array $modules, array $missing = []
+ ) {
+ $out = '';
+ $states = [];
+
+ if ( !count( $modules ) && !count( $missing ) ) {
+ return <<<MESSAGE
+/* This file is the Web entry point for MediaWiki's ResourceLoader:
+ <https://www.mediawiki.org/wiki/ResourceLoader>. In this request,
+ no modules were requested. Max made me put this here. */
+MESSAGE;
+ }
+
+ $image = $context->getImageObj();
+ if ( $image ) {
+ $data = $image->getImageData( $context );
+ if ( $data === false ) {
+ $data = '';
+ $this->errors[] = 'Image generation failed';
+ }
+ return $data;
+ }
+
+ foreach ( $missing as $name ) {
+ $states[$name] = 'missing';
+ }
+
+ // Generate output
+ $isRaw = false;
+
+ $filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js';
+
+ foreach ( $modules as $name => $module ) {
+ try {
+ $content = $module->getModuleContent( $context );
+ $implementKey = $name . '@' . $module->getVersionHash( $context );
+ $strContent = '';
+
+ if ( isset( $content['headers'] ) ) {
+ $this->extraHeaders = array_merge( $this->extraHeaders, $content['headers'] );
+ }
+
+ // Append output
+ switch ( $context->getOnly() ) {
+ case 'scripts':
+ $scripts = $content['scripts'];
+ if ( is_string( $scripts ) ) {
+ // Load scripts raw...
+ $strContent = $scripts;
+ } elseif ( is_array( $scripts ) ) {
+ // ...except when $scripts is an array of URLs
+ $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
+ }
+ break;
+ case 'styles':
+ $styles = $content['styles'];
+ // We no longer seperate into media, they are all combined now with
+ // custom media type groups into @media .. {} sections as part of the css string.
+ // Module returns either an empty array or a numerical array with css strings.
+ $strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
+ break;
+ default:
+ $scripts = isset( $content['scripts'] ) ? $content['scripts'] : '';
+ if ( is_string( $scripts ) ) {
+ if ( $name === 'site' || $name === 'user' ) {
+ // Legacy scripts that run in the global scope without a closure.
+ // mw.loader.implement will use globalEval if scripts is a string.
+ // Minify manually here, because general response minification is
+ // not effective due it being a string literal, not a function.
+ if ( !self::inDebugMode() ) {
+ $scripts = self::filter( 'minify-js', $scripts ); // T107377
+ }
+ } else {
+ $scripts = new XmlJsCode( $scripts );
+ }
+ }
+ $strContent = self::makeLoaderImplementScript(
+ $implementKey,
+ $scripts,
+ isset( $content['styles'] ) ? $content['styles'] : [],
+ isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [],
+ isset( $content['templates'] ) ? $content['templates'] : []
+ );
+ break;
+ }
+
+ if ( !$context->getDebug() ) {
+ $strContent = self::filter( $filter, $strContent );
+ }
+
+ if ( $context->getOnly() === 'scripts' ) {
+ // Use a linebreak between module scripts (T162719)
+ $out .= $this->ensureNewline( $strContent );
+ } else {
+ $out .= $strContent;
+ }
+
+ } catch ( Exception $e ) {
+ $this->outputErrorAndLog( $e, 'Generating module package failed: {exception}' );
+
+ // Respond to client with error-state instead of module implementation
+ $states[$name] = 'error';
+ unset( $modules[$name] );
+ }
+ $isRaw |= $module->isRaw();
+ }
+
+ // Update module states
+ if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) {
+ if ( count( $modules ) && $context->getOnly() === 'scripts' ) {
+ // Set the state of modules loaded as only scripts to ready as
+ // they don't have an mw.loader.implement wrapper that sets the state
+ foreach ( $modules as $name => $module ) {
+ $states[$name] = 'ready';
+ }
+ }
+
+ // Set the state of modules we didn't respond to with mw.loader.implement
+ if ( count( $states ) ) {
+ $stateScript = self::makeLoaderStateScript( $states );
+ if ( !$context->getDebug() ) {
+ $stateScript = self::filter( 'minify-js', $stateScript );
+ }
+ // Use a linebreak between module script and state script (T162719)
+ $out = $this->ensureNewline( $out ) . $stateScript;
+ }
+ } else {
+ if ( count( $states ) ) {
+ $this->errors[] = 'Problematic modules: ' .
+ FormatJson::encode( $states, self::inDebugMode() );
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Ensure the string is either empty or ends in a line break
+ * @param string $str
+ * @return string
+ */
+ private function ensureNewline( $str ) {
+ $end = substr( $str, -1 );
+ if ( $end === false || $end === "\n" ) {
+ return $str;
+ }
+ return $str . "\n";
+ }
+
+ /**
+ * Get names of modules that use a certain message.
+ *
+ * @param string $messageKey
+ * @return array List of module names
+ */
+ public function getModulesByMessage( $messageKey ) {
+ $moduleNames = [];
+ foreach ( $this->getModuleNames() as $moduleName ) {
+ $module = $this->getModule( $moduleName );
+ if ( in_array( $messageKey, $module->getMessages() ) ) {
+ $moduleNames[] = $moduleName;
+ }
+ }
+ return $moduleNames;
+ }
+
+ /**
+ * Return JS code that calls mw.loader.implement with given module properties.
+ *
+ * @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, or a string of JavaScript for `$.globalEval`.
+ * @param mixed $styles Array of CSS strings keyed by media type, or an array of lists of URLs
+ * to CSS files keyed by media type
+ * @param mixed $messages List of messages associated with this module. May either be an
+ * associative array mapping message key to value, or a JSON-encoded message blob containing
+ * the same data, wrapped in an XmlJsCode object.
+ * @param array $templates Keys are name of templates and values are the source of
+ * the template.
+ * @throws MWException
+ * @return string JavaScript code
+ */
+ protected static function makeLoaderImplementScript(
+ $name, $scripts, $styles, $messages, $templates
+ ) {
+ if ( $scripts instanceof XmlJsCode ) {
+ if ( self::inDebugMode() ) {
+ $scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
+ } else {
+ $scripts = new XmlJsCode( 'function($,jQuery,require,module){'. $scripts->value . '}' );
+ }
+ } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
+ throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
+ }
+ // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not
+ // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead
+ // of "{}". Force them to objects.
+ $module = [
+ $name,
+ $scripts,
+ (object)$styles,
+ (object)$messages,
+ (object)$templates,
+ ];
+ self::trimArray( $module );
+
+ return Xml::encodeJsCall( 'mw.loader.implement', $module, self::inDebugMode() );
+ }
+
+ /**
+ * Returns JS code which, when called, will register a given list of messages.
+ *
+ * @param mixed $messages Either an associative array mapping message key to value, or a
+ * JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object.
+ * @return string JavaScript code
+ */
+ public static function makeMessageSetScript( $messages ) {
+ return Xml::encodeJsCall(
+ 'mw.messages.set',
+ [ (object)$messages ],
+ self::inDebugMode()
+ );
+ }
+
+ /**
+ * Combines an associative array mapping media type to CSS into a
+ * single stylesheet with "@media" blocks.
+ *
+ * @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings
+ * @return array
+ */
+ public static function makeCombinedStyles( array $stylePairs ) {
+ $out = [];
+ foreach ( $stylePairs as $media => $styles ) {
+ // ResourceLoaderFileModule::getStyle can return the styles
+ // as a string or an array of strings. This is to allow separation in
+ // the front-end.
+ $styles = (array)$styles;
+ foreach ( $styles as $style ) {
+ $style = trim( $style );
+ // Don't output an empty "@media print { }" block (T42498)
+ if ( $style !== '' ) {
+ // Transform the media type based on request params and config
+ // The way that this relies on $wgRequest to propagate request params is slightly evil
+ $media = OutputPage::transformCssMedia( $media );
+
+ if ( $media === '' || $media == 'all' ) {
+ $out[] = $style;
+ } elseif ( is_string( $media ) ) {
+ $out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
+ }
+ // else: skip
+ }
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Returns a JS call to mw.loader.state, which sets the state of a
+ * module or modules to a given value. Has two calling conventions:
+ *
+ * - ResourceLoader::makeLoaderStateScript( $name, $state ):
+ * Set the state of a single module called $name to $state
+ *
+ * - ResourceLoader::makeLoaderStateScript( [ $name => $state, ... ] ):
+ * Set the state of modules with the given names to the given states
+ *
+ * @param string $name
+ * @param string $state
+ * @return string JavaScript code
+ */
+ public static function makeLoaderStateScript( $name, $state = null ) {
+ if ( is_array( $name ) ) {
+ return Xml::encodeJsCall(
+ 'mw.loader.state',
+ [ $name ],
+ self::inDebugMode()
+ );
+ } else {
+ return Xml::encodeJsCall(
+ 'mw.loader.state',
+ [ $name, $state ],
+ self::inDebugMode()
+ );
+ }
+ }
+
+ /**
+ * Returns JS code which calls the script given by $script. The script will
+ * be called with local variables name, version, dependencies and group,
+ * which will have values corresponding to $name, $version, $dependencies
+ * and $group as supplied.
+ *
+ * @param string $name Module name
+ * @param string $version Module version hash
+ * @param array $dependencies List of module names on which this module depends
+ * @param string $group Group which the module is in.
+ * @param string $source Source of the module, or 'local' if not foreign.
+ * @param string $script JavaScript code
+ * @return string JavaScript code
+ */
+ public static function makeCustomLoaderScript( $name, $version, $dependencies,
+ $group, $source, $script
+ ) {
+ $script = str_replace( "\n", "\n\t", trim( $script ) );
+ return Xml::encodeJsCall(
+ "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
+ [ $name, $version, $dependencies, $group, $source ],
+ self::inDebugMode()
+ );
+ }
+
+ private static function isEmptyObject( stdClass $obj ) {
+ foreach ( $obj as $key => $value ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove empty values from the end of an array.
+ *
+ * Values considered empty:
+ *
+ * - null
+ * - []
+ * - new XmlJsCode( '{}' )
+ * - new stdClass() // (object) []
+ *
+ * @param Array $array
+ */
+ private static function trimArray( array &$array ) {
+ $i = count( $array );
+ while ( $i-- ) {
+ if ( $array[$i] === null
+ || $array[$i] === []
+ || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' )
+ || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
+ ) {
+ unset( $array[$i] );
+ } else {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Returns JS code which calls mw.loader.register with the given
+ * parameters. Has three calling conventions:
+ *
+ * - ResourceLoader::makeLoaderRegisterScript( $name, $version,
+ * $dependencies, $group, $source, $skip
+ * ):
+ * Register a single module.
+ *
+ * - ResourceLoader::makeLoaderRegisterScript( [ $name1, $name2 ] ):
+ * Register modules with the given names.
+ *
+ * - ResourceLoader::makeLoaderRegisterScript( [
+ * [ $name1, $version1, $dependencies1, $group1, $source1, $skip1 ],
+ * [ $name2, $version2, $dependencies1, $group2, $source2, $skip2 ],
+ * ...
+ * ] ):
+ * Registers modules with the given names and parameters.
+ *
+ * @param string $name Module name
+ * @param string $version Module version hash
+ * @param array $dependencies List of module names on which this module depends
+ * @param string $group Group which the module is in
+ * @param string $source Source of the module, or 'local' if not foreign
+ * @param string $skip Script body of the skip function
+ * @return string JavaScript code
+ */
+ public static function makeLoaderRegisterScript( $name, $version = null,
+ $dependencies = null, $group = null, $source = null, $skip = null
+ ) {
+ if ( is_array( $name ) ) {
+ // Build module name index
+ $index = [];
+ foreach ( $name as $i => &$module ) {
+ $index[$module[0]] = $i;
+ }
+
+ // Transform dependency names into indexes when possible, they will be resolved by
+ // mw.loader.register on the other end
+ foreach ( $name as &$module ) {
+ if ( isset( $module[2] ) ) {
+ foreach ( $module[2] as &$dependency ) {
+ if ( isset( $index[$dependency] ) ) {
+ $dependency = $index[$dependency];
+ }
+ }
+ }
+ }
+
+ array_walk( $name, [ 'self', 'trimArray' ] );
+
+ return Xml::encodeJsCall(
+ 'mw.loader.register',
+ [ $name ],
+ self::inDebugMode()
+ );
+ } else {
+ $registration = [ $name, $version, $dependencies, $group, $source, $skip ];
+ self::trimArray( $registration );
+ return Xml::encodeJsCall(
+ 'mw.loader.register',
+ $registration,
+ self::inDebugMode()
+ );
+ }
+ }
+
+ /**
+ * Returns JS code which calls mw.loader.addSource() with the given
+ * parameters. Has two calling conventions:
+ *
+ * - ResourceLoader::makeLoaderSourcesScript( $id, $properties ):
+ * Register a single source
+ *
+ * - ResourceLoader::makeLoaderSourcesScript( [ $id1 => $loadUrl, $id2 => $loadUrl, ... ] );
+ * Register sources with the given IDs and properties.
+ *
+ * @param string $id Source ID
+ * @param string $loadUrl load.php url
+ * @return string JavaScript code
+ */
+ public static function makeLoaderSourcesScript( $id, $loadUrl = null ) {
+ if ( is_array( $id ) ) {
+ return Xml::encodeJsCall(
+ 'mw.loader.addSource',
+ [ $id ],
+ self::inDebugMode()
+ );
+ } else {
+ return Xml::encodeJsCall(
+ 'mw.loader.addSource',
+ [ $id, $loadUrl ],
+ self::inDebugMode()
+ );
+ }
+ }
+
+ /**
+ * Wraps JavaScript code to run after startup and base modules.
+ *
+ * @param string $script JavaScript code
+ * @return string JavaScript code
+ */
+ public static function makeLoaderConditionalScript( $script ) {
+ return '(window.RLQ=window.RLQ||[]).push(function(){' .
+ trim( $script ) . '});';
+ }
+
+ /**
+ * Returns an HTML script tag that runs given JS code after startup and base modules.
+ *
+ * The code will be wrapped in a closure, and it will be executed by ResourceLoader's
+ * startup module if the client has adequate support for MediaWiki JavaScript code.
+ *
+ * @param string $script JavaScript code
+ * @return WrappedString HTML
+ */
+ public static function makeInlineScript( $script ) {
+ $js = self::makeLoaderConditionalScript( $script );
+ return new WrappedString(
+ Html::inlineScript( $js ),
+ '<script>(window.RLQ=window.RLQ||[]).push(function(){',
+ '});</script>'
+ );
+ }
+
+ /**
+ * Returns JS code which will set the MediaWiki configuration array to
+ * the given value.
+ *
+ * @param array $configuration List of configuration values keyed by variable name
+ * @return string JavaScript code
+ */
+ public static function makeConfigSetScript( array $configuration ) {
+ return Xml::encodeJsCall(
+ 'mw.config.set',
+ [ $configuration ],
+ self::inDebugMode()
+ );
+ }
+
+ /**
+ * Convert an array of module names to a packed query string.
+ *
+ * For example, `[ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ]`
+ * becomes `'foo.bar,baz|bar.baz,quux'`.
+ *
+ * This process is reversed by ResourceLoaderContext::expandModuleNames().
+ * See also mw.loader#buildModulesString() which is a port of this, used
+ * on the client-side.
+ *
+ * @param array $modules List of module names (strings)
+ * @return string Packed query string
+ */
+ public static function makePackedModulesString( $modules ) {
+ $moduleMap = []; // [ prefix => [ suffixes ] ]
+ foreach ( $modules as $module ) {
+ $pos = strrpos( $module, '.' );
+ $prefix = $pos === false ? '' : substr( $module, 0, $pos );
+ $suffix = $pos === false ? $module : substr( $module, $pos + 1 );
+ $moduleMap[$prefix][] = $suffix;
+ }
+
+ $arr = [];
+ foreach ( $moduleMap as $prefix => $suffixes ) {
+ $p = $prefix === '' ? '' : $prefix . '.';
+ $arr[] = $p . implode( ',', $suffixes );
+ }
+ return implode( '|', $arr );
+ }
+
+ /**
+ * Determine whether debug mode was requested
+ * Order of priority is 1) request param, 2) cookie, 3) $wg setting
+ * @return bool
+ */
+ public static function inDebugMode() {
+ if ( self::$debugMode === null ) {
+ global $wgRequest, $wgResourceLoaderDebug;
+ self::$debugMode = $wgRequest->getFuzzyBool( 'debug',
+ $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug )
+ );
+ }
+ return self::$debugMode;
+ }
+
+ /**
+ * Reset static members used for caching.
+ *
+ * Global state and $wgRequest are evil, but we're using it right
+ * now and sometimes we need to be able to force ResourceLoader to
+ * re-evaluate the context because it has changed (e.g. in the test suite).
+ */
+ public static function clearCache() {
+ self::$debugMode = null;
+ }
+
+ /**
+ * Build a load.php URL
+ *
+ * @since 1.24
+ * @param string $source Name of the ResourceLoader source
+ * @param ResourceLoaderContext $context
+ * @param array $extraQuery
+ * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too.
+ */
+ public function createLoaderURL( $source, ResourceLoaderContext $context,
+ $extraQuery = []
+ ) {
+ $query = self::createLoaderQuery( $context, $extraQuery );
+ $script = $this->getLoadScript( $source );
+
+ return wfAppendQuery( $script, $query );
+ }
+
+ /**
+ * Helper for createLoaderURL()
+ *
+ * @since 1.24
+ * @see makeLoaderQuery
+ * @param ResourceLoaderContext $context
+ * @param array $extraQuery
+ * @return array
+ */
+ protected static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = [] ) {
+ return self::makeLoaderQuery(
+ $context->getModules(),
+ $context->getLanguage(),
+ $context->getSkin(),
+ $context->getUser(),
+ $context->getVersion(),
+ $context->getDebug(),
+ $context->getOnly(),
+ $context->getRequest()->getBool( 'printable' ),
+ $context->getRequest()->getBool( 'handheld' ),
+ $extraQuery
+ );
+ }
+
+ /**
+ * Build a query array (array representation of query string) for load.php. Helper
+ * function for createLoaderURL().
+ *
+ * @param array $modules
+ * @param string $lang
+ * @param string $skin
+ * @param string $user
+ * @param string $version
+ * @param bool $debug
+ * @param string $only
+ * @param bool $printable
+ * @param bool $handheld
+ * @param array $extraQuery
+ *
+ * @return array
+ */
+ public static function makeLoaderQuery( $modules, $lang, $skin, $user = null,
+ $version = null, $debug = false, $only = null, $printable = false,
+ $handheld = false, $extraQuery = []
+ ) {
+ $query = [
+ 'modules' => self::makePackedModulesString( $modules ),
+ 'lang' => $lang,
+ 'skin' => $skin,
+ 'debug' => $debug ? 'true' : 'false',
+ ];
+ if ( $user !== null ) {
+ $query['user'] = $user;
+ }
+ if ( $version !== null ) {
+ $query['version'] = $version;
+ }
+ if ( $only !== null ) {
+ $query['only'] = $only;
+ }
+ if ( $printable ) {
+ $query['printable'] = 1;
+ }
+ if ( $handheld ) {
+ $query['handheld'] = 1;
+ }
+ $query += $extraQuery;
+
+ // Make queries uniform in order
+ ksort( $query );
+ return $query;
+ }
+
+ /**
+ * Check a module name for validity.
+ *
+ * Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be
+ * at most 255 bytes.
+ *
+ * @param string $moduleName Module name to check
+ * @return bool Whether $moduleName is a valid module name
+ */
+ public static function isValidModuleName( $moduleName ) {
+ return strcspn( $moduleName, '!,|', 0, 255 ) === strlen( $moduleName );
+ }
+
+ /**
+ * Returns LESS compiler set up for use with MediaWiki
+ *
+ * @since 1.27
+ * @param array $extraVars Associative array of extra (i.e., other than the
+ * globally-configured ones) that should be used for compilation.
+ * @throws MWException
+ * @return Less_Parser
+ */
+ public function getLessCompiler( $extraVars = [] ) {
+ // When called from the installer, it is possible that a required PHP extension
+ // is missing (at least for now; see T49564). If this is the case, throw an
+ // exception (caught by the installer) to prevent a fatal error later on.
+ if ( !class_exists( 'Less_Parser' ) ) {
+ throw new MWException( 'MediaWiki requires the less.php parser' );
+ }
+
+ $parser = new Less_Parser;
+ $parser->ModifyVars( array_merge( $this->getLessVars(), $extraVars ) );
+ $parser->SetImportDirs(
+ array_fill_keys( $this->config->get( 'ResourceLoaderLESSImportPaths' ), '' )
+ );
+ $parser->SetOption( 'relativeUrls', false );
+
+ return $parser;
+ }
+
+ /**
+ * Get global LESS variables.
+ *
+ * @since 1.27
+ * @return array Map of variable names to string CSS values.
+ */
+ public function getLessVars() {
+ if ( $this->lessVars === null ) {
+ $this->lessVars = $this->config->get( 'ResourceLoaderLESSVars' );
+ }
+ return $this->lessVars;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php b/www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php
new file mode 100644
index 00000000..545fd3bd
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderClientHtml.php
@@ -0,0 +1,472 @@
+<?php
+/**
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use Wikimedia\WrappedStringList;
+
+/**
+ * Bootstrap a ResourceLoader client on an HTML page.
+ *
+ * @since 1.28
+ */
+class ResourceLoaderClientHtml {
+
+ /** @var ResourceLoaderContext */
+ private $context;
+
+ /** @var ResourceLoader */
+ private $resourceLoader;
+
+ /** @var array */
+ private $options;
+
+ /** @var array */
+ private $config = [];
+
+ /** @var array */
+ private $modules = [];
+
+ /** @var array */
+ private $moduleStyles = [];
+
+ /** @var array */
+ private $moduleScripts = [];
+
+ /** @var array */
+ private $exemptStates = [];
+
+ /** @var array */
+ private $data;
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @param array $options [optional] Array of options
+ * - 'target': Custom parameter passed to StartupModule.
+ */
+ public function __construct( ResourceLoaderContext $context, array $options = [] ) {
+ $this->context = $context;
+ $this->resourceLoader = $context->getResourceLoader();
+ $this->options = $options;
+ }
+
+ /**
+ * Set mw.config variables.
+ *
+ * @param array $vars Array of key/value pairs
+ */
+ public function setConfig( array $vars ) {
+ foreach ( $vars as $key => $value ) {
+ $this->config[$key] = $value;
+ }
+ }
+
+ /**
+ * Ensure one or more modules are loaded.
+ *
+ * @param array $modules Array of module names
+ */
+ public function setModules( array $modules ) {
+ $this->modules = $modules;
+ }
+
+ /**
+ * Ensure the styles of one or more modules are loaded.
+ *
+ * @deprecated since 1.28
+ * @param array $modules Array of module names
+ */
+ public function setModuleStyles( array $modules ) {
+ $this->moduleStyles = $modules;
+ }
+
+ /**
+ * Ensure the scripts of one or more modules are loaded.
+ *
+ * @deprecated since 1.28
+ * @param array $modules Array of module names
+ */
+ public function setModuleScripts( array $modules ) {
+ $this->moduleScripts = $modules;
+ }
+
+ /**
+ * Set state of special modules that are handled by the caller manually.
+ *
+ * See OutputPage::buildExemptModules() for use cases.
+ *
+ * @param array $states Module state keyed by module name
+ */
+ public function setExemptStates( array $states ) {
+ $this->exemptStates = $states;
+ }
+
+ /**
+ * @return array
+ */
+ private function getData() {
+ if ( $this->data ) {
+ // @codeCoverageIgnoreStart
+ return $this->data;
+ // @codeCoverageIgnoreEnd
+ }
+
+ $rl = $this->resourceLoader;
+ $data = [
+ 'states' => [
+ // moduleName => state
+ ],
+ 'general' => [],
+ 'styles' => [],
+ 'scripts' => [],
+ // Embedding for private modules
+ 'embed' => [
+ 'styles' => [],
+ 'general' => [],
+ ],
+
+ ];
+
+ foreach ( $this->modules as $name ) {
+ $module = $rl->getModule( $name );
+ if ( !$module ) {
+ continue;
+ }
+
+ $context = $this->getContext( $module->getGroup(), ResourceLoaderModule::TYPE_COMBINED );
+ if ( $module->isKnownEmpty( $context ) ) {
+ // Avoid needless request or embed for empty module
+ $data['states'][$name] = 'ready';
+ continue;
+ }
+
+ if ( $module->shouldEmbedModule( $this->context ) ) {
+ // Embed via mw.loader.implement per T36907.
+ $data['embed']['general'][] = $name;
+ // Avoid duplicate request from mw.loader
+ $data['states'][$name] = 'loading';
+ } else {
+ // Load via mw.loader.load()
+ $data['general'][] = $name;
+ }
+ }
+
+ foreach ( $this->moduleStyles as $name ) {
+ $module = $rl->getModule( $name );
+ if ( !$module ) {
+ continue;
+ }
+
+ if ( $module->getType() !== ResourceLoaderModule::LOAD_STYLES ) {
+ $logger = $rl->getLogger();
+ $logger->error( 'Unexpected general module "{module}" in styles queue.', [
+ 'module' => $name,
+ ] );
+ continue;
+ }
+
+ // Stylesheet doesn't trigger mw.loader callback.
+ // Set "ready" state to allow script modules to depend on this module (T87871).
+ // And to avoid duplicate requests at run-time from mw.loader.
+ $data['states'][$name] = 'ready';
+
+ $group = $module->getGroup();
+ $context = $this->getContext( $group, ResourceLoaderModule::TYPE_STYLES );
+ // Avoid needless request for empty module
+ if ( !$module->isKnownEmpty( $context ) ) {
+ if ( $module->shouldEmbedModule( $this->context ) ) {
+ // Embed via style element
+ $data['embed']['styles'][] = $name;
+ } else {
+ // Load from load.php?only=styles via <link rel=stylesheet>
+ $data['styles'][] = $name;
+ }
+ }
+ }
+
+ foreach ( $this->moduleScripts as $name ) {
+ $module = $rl->getModule( $name );
+ if ( !$module ) {
+ continue;
+ }
+
+ $group = $module->getGroup();
+ $context = $this->getContext( $group, ResourceLoaderModule::TYPE_SCRIPTS );
+ if ( $module->isKnownEmpty( $context ) ) {
+ // Avoid needless request for empty module
+ $data['states'][$name] = 'ready';
+ } else {
+ // Load from load.php?only=scripts via <script src></script>
+ $data['scripts'][] = $name;
+
+ // Avoid duplicate request from mw.loader
+ $data['states'][$name] = 'loading';
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @return array Attribute key-value pairs for the HTML document element
+ */
+ public function getDocumentAttributes() {
+ return [ 'class' => 'client-nojs' ];
+ }
+
+ /**
+ * The order of elements in the head is as follows:
+ * - Inline scripts.
+ * - Stylesheets.
+ * - Async external script-src.
+ *
+ * Reasons:
+ * - Script execution may be blocked on preceeding stylesheets.
+ * - Async scripts are not blocked on stylesheets.
+ * - Inline scripts can't be asynchronous.
+ * - For styles, earlier is better.
+ *
+ * @return string|WrappedStringList HTML
+ */
+ public function getHeadHtml() {
+ $data = $this->getData();
+ $chunks = [];
+
+ // Change "client-nojs" class to client-js. This allows easy toggling of UI components.
+ // This happens synchronously on every page view to avoid flashes of wrong content.
+ // See also #getDocumentAttributes() and /resources/src/startup.js.
+ $chunks[] = Html::inlineScript(
+ 'document.documentElement.className = document.documentElement.className'
+ . '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );'
+ );
+
+ // Inline RLQ: Set page variables
+ if ( $this->config ) {
+ $chunks[] = ResourceLoader::makeInlineScript(
+ ResourceLoader::makeConfigSetScript( $this->config )
+ );
+ }
+
+ // Inline RLQ: Initial module states
+ $states = array_merge( $this->exemptStates, $data['states'] );
+ if ( $states ) {
+ $chunks[] = ResourceLoader::makeInlineScript(
+ ResourceLoader::makeLoaderStateScript( $states )
+ );
+ }
+
+ // Inline RLQ: Embedded modules
+ if ( $data['embed']['general'] ) {
+ $chunks[] = $this->getLoad(
+ $data['embed']['general'],
+ ResourceLoaderModule::TYPE_COMBINED
+ );
+ }
+
+ // Inline RLQ: Load general modules
+ if ( $data['general'] ) {
+ $chunks[] = ResourceLoader::makeInlineScript(
+ Xml::encodeJsCall( 'mw.loader.load', [ $data['general'] ] )
+ );
+ }
+
+ // Inline RLQ: Load only=scripts
+ if ( $data['scripts'] ) {
+ $chunks[] = $this->getLoad(
+ $data['scripts'],
+ ResourceLoaderModule::TYPE_SCRIPTS
+ );
+ }
+
+ // External stylesheets
+ if ( $data['styles'] ) {
+ $chunks[] = $this->getLoad(
+ $data['styles'],
+ ResourceLoaderModule::TYPE_STYLES
+ );
+ }
+
+ // Inline stylesheets (embedded only=styles)
+ if ( $data['embed']['styles'] ) {
+ $chunks[] = $this->getLoad(
+ $data['embed']['styles'],
+ ResourceLoaderModule::TYPE_STYLES
+ );
+ }
+
+ // Async scripts. Once the startup is loaded, inline RLQ scripts will run.
+ // Pass-through a custom 'target' from OutputPage (T143066).
+ $startupQuery = isset( $this->options['target'] )
+ ? [ 'target' => (string)$this->options['target'] ]
+ : [];
+ $chunks[] = $this->getLoad(
+ 'startup',
+ ResourceLoaderModule::TYPE_SCRIPTS,
+ $startupQuery
+ );
+
+ return WrappedStringList::join( "\n", $chunks );
+ }
+
+ /**
+ * @return string|WrappedStringList HTML
+ */
+ public function getBodyHtml() {
+ return '';
+ }
+
+ private function getContext( $group, $type ) {
+ return self::makeContext( $this->context, $group, $type );
+ }
+
+ private function getLoad( $modules, $only, array $extraQuery = [] ) {
+ return self::makeLoad( $this->context, (array)$modules, $only, $extraQuery );
+ }
+
+ private static function makeContext( ResourceLoaderContext $mainContext, $group, $type,
+ array $extraQuery = []
+ ) {
+ // Create new ResourceLoaderContext so that $extraQuery may trigger isRaw().
+ $req = new FauxRequest( array_merge( $mainContext->getRequest()->getValues(), $extraQuery ) );
+ // Set 'only' if not combined
+ $req->setVal( 'only', $type === ResourceLoaderModule::TYPE_COMBINED ? null : $type );
+ // Remove user parameter in most cases
+ if ( $group !== 'user' && $group !== 'private' ) {
+ $req->setVal( 'user', null );
+ }
+ $context = new ResourceLoaderContext( $mainContext->getResourceLoader(), $req );
+ // Allow caller to setVersion() and setModules()
+ return new DerivativeResourceLoaderContext( $context );
+ }
+
+ /**
+ * Explicily load or embed modules on a page.
+ *
+ * @param ResourceLoaderContext $mainContext
+ * @param array $modules One or more module names
+ * @param string $only ResourceLoaderModule TYPE_ class constant
+ * @param array $extraQuery [optional] Array with extra query parameters for the request
+ * @return string|WrappedStringList HTML
+ */
+ public static function makeLoad( ResourceLoaderContext $mainContext, array $modules, $only,
+ array $extraQuery = []
+ ) {
+ $rl = $mainContext->getResourceLoader();
+ $chunks = [];
+
+ // Sort module names so requests are more uniform
+ sort( $modules );
+
+ if ( $mainContext->getDebug() && count( $modules ) > 1 ) {
+ $chunks = [];
+ // Recursively call us for every item
+ foreach ( $modules as $name ) {
+ $chunks[] = self::makeLoad( $mainContext, [ $name ], $only, $extraQuery );
+ }
+ return new WrappedStringList( "\n", $chunks );
+ }
+
+ // Create keyed-by-source and then keyed-by-group list of module objects from modules list
+ $sortedModules = [];
+ foreach ( $modules as $name ) {
+ $module = $rl->getModule( $name );
+ if ( !$module ) {
+ $rl->getLogger()->warning( 'Unknown module "{module}"', [ 'module' => $name ] );
+ continue;
+ }
+ $sortedModules[$module->getSource()][$module->getGroup()][$name] = $module;
+ }
+
+ foreach ( $sortedModules as $source => $groups ) {
+ foreach ( $groups as $group => $grpModules ) {
+ $context = self::makeContext( $mainContext, $group, $only, $extraQuery );
+
+ // Separate sets of linked and embedded modules while preserving order
+ $moduleSets = [];
+ $idx = -1;
+ foreach ( $grpModules as $name => $module ) {
+ $shouldEmbed = $module->shouldEmbedModule( $context );
+ if ( !$moduleSets || $moduleSets[$idx][0] !== $shouldEmbed ) {
+ $moduleSets[++$idx] = [ $shouldEmbed, [] ];
+ }
+ $moduleSets[$idx][1][$name] = $module;
+ }
+
+ // Link/embed each set
+ foreach ( $moduleSets as list( $embed, $moduleSet ) ) {
+ $context->setModules( array_keys( $moduleSet ) );
+ if ( $embed ) {
+ // Decide whether to use style or script element
+ if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
+ $chunks[] = Html::inlineStyle(
+ $rl->makeModuleResponse( $context, $moduleSet )
+ );
+ } else {
+ $chunks[] = ResourceLoader::makeInlineScript(
+ $rl->makeModuleResponse( $context, $moduleSet )
+ );
+ }
+ } else {
+ // See if we have one or more raw modules
+ $isRaw = false;
+ foreach ( $moduleSet as $key => $module ) {
+ $isRaw |= $module->isRaw();
+ }
+
+ // Special handling for the user group; because users might change their stuff
+ // on-wiki like user pages, or user preferences; we need to find the highest
+ // timestamp of these user-changeable modules so we can ensure cache misses on change
+ // This should NOT be done for the site group (T29564) because anons get that too
+ // and we shouldn't be putting timestamps in CDN-cached HTML
+ if ( $group === 'user' ) {
+ // Must setModules() before makeVersionQuery()
+ $context->setVersion( $rl->makeVersionQuery( $context ) );
+ }
+
+ $url = $rl->createLoaderURL( $source, $context, $extraQuery );
+
+ // Decide whether to use 'style' or 'script' element
+ if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
+ $chunk = Html::linkedStyle( $url );
+ } else {
+ if ( $context->getRaw() || $isRaw ) {
+ $chunk = Html::element( 'script', [
+ // In SpecialJavaScriptTest, QUnit must load synchronous
+ 'async' => !isset( $extraQuery['sync'] ),
+ 'src' => $url
+ ] );
+ } else {
+ $chunk = ResourceLoader::makeInlineScript(
+ Xml::encodeJsCall( 'mw.loader.load', [ $url ] )
+ );
+ }
+ }
+
+ if ( $group == 'noscript' ) {
+ $chunks[] = Html::rawElement( 'noscript', [], $chunk );
+ } else {
+ $chunks[] = $chunk;
+ }
+ }
+ }
+ }
+ }
+
+ return new WrappedStringList( "\n", $chunks );
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderContext.php b/www/wiki/includes/resourceloader/ResourceLoaderContext.php
new file mode 100644
index 00000000..c4e9884a
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderContext.php
@@ -0,0 +1,395 @@
+<?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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Object passed around to modules which contains information about the state
+ * of a specific loader request.
+ */
+class ResourceLoaderContext implements MessageLocalizer {
+ protected $resourceLoader;
+ protected $request;
+ protected $logger;
+
+ // Module content vary
+ protected $skin;
+ protected $language;
+ protected $debug;
+ protected $user;
+
+ // Request vary (in addition to cache vary)
+ protected $modules;
+ protected $only;
+ protected $version;
+ protected $raw;
+ protected $image;
+ protected $variant;
+ protected $format;
+
+ protected $direction;
+ protected $hash;
+ protected $userObj;
+ protected $imageObj;
+
+ /**
+ * @param ResourceLoader $resourceLoader
+ * @param WebRequest $request
+ */
+ public function __construct( ResourceLoader $resourceLoader, WebRequest $request ) {
+ $this->resourceLoader = $resourceLoader;
+ $this->request = $request;
+ $this->logger = $resourceLoader->getLogger();
+
+ // Future developers: Use WebRequest::getRawVal() instead getVal().
+ // The getVal() method performs slow Language+UTF logic. (f303bb9360)
+
+ // List of modules
+ $modules = $request->getRawVal( 'modules' );
+ $this->modules = $modules ? self::expandModuleNames( $modules ) : [];
+
+ // Various parameters
+ $this->user = $request->getRawVal( 'user' );
+ $this->debug = $request->getFuzzyBool(
+ 'debug',
+ $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' )
+ );
+ $this->only = $request->getRawVal( 'only', null );
+ $this->version = $request->getRawVal( 'version', null );
+ $this->raw = $request->getFuzzyBool( 'raw' );
+
+ // Image requests
+ $this->image = $request->getRawVal( 'image' );
+ $this->variant = $request->getRawVal( 'variant' );
+ $this->format = $request->getRawVal( 'format' );
+
+ $this->skin = $request->getRawVal( 'skin' );
+ $skinnames = Skin::getSkinNames();
+ // If no skin is specified, or we don't recognize the skin, use the default skin
+ if ( !$this->skin || !isset( $skinnames[$this->skin] ) ) {
+ $this->skin = $resourceLoader->getConfig()->get( 'DefaultSkin' );
+ }
+ }
+
+ /**
+ * Expand a string of the form `jquery.foo,bar|jquery.ui.baz,quux` to
+ * an array of module names like `[ 'jquery.foo', 'jquery.bar',
+ * 'jquery.ui.baz', 'jquery.ui.quux' ]`.
+ *
+ * This process is reversed by ResourceLoader::makePackedModulesString().
+ *
+ * @param string $modules Packed module name list
+ * @return array Array of module names
+ */
+ public static function expandModuleNames( $modules ) {
+ $retval = [];
+ $exploded = explode( '|', $modules );
+ foreach ( $exploded as $group ) {
+ if ( strpos( $group, ',' ) === false ) {
+ // This is not a set of modules in foo.bar,baz notation
+ // but a single module
+ $retval[] = $group;
+ } else {
+ // This is a set of modules in foo.bar,baz notation
+ $pos = strrpos( $group, '.' );
+ if ( $pos === false ) {
+ // Prefixless modules, i.e. without dots
+ $retval = array_merge( $retval, explode( ',', $group ) );
+ } else {
+ // We have a prefix and a bunch of suffixes
+ $prefix = substr( $group, 0, $pos ); // 'foo'
+ $suffixes = explode( ',', substr( $group, $pos + 1 ) ); // [ 'bar', 'baz' ]
+ foreach ( $suffixes as $suffix ) {
+ $retval[] = "$prefix.$suffix";
+ }
+ }
+ }
+ }
+ return $retval;
+ }
+
+ /**
+ * Return a dummy ResourceLoaderContext object suitable for passing into
+ * things that don't "really" need a context.
+ * @return ResourceLoaderContext
+ */
+ public static function newDummyContext() {
+ return new self( new ResourceLoader(
+ MediaWikiServices::getInstance()->getMainConfig(),
+ LoggerFactory::getInstance( 'resourceloader' )
+ ), new FauxRequest( [] ) );
+ }
+
+ /**
+ * @return ResourceLoader
+ */
+ public function getResourceLoader() {
+ return $this->resourceLoader;
+ }
+
+ /**
+ * @return WebRequest
+ */
+ public function getRequest() {
+ return $this->request;
+ }
+
+ /**
+ * @since 1.27
+ * @return \Psr\Log\LoggerInterface
+ */
+ public function getLogger() {
+ return $this->logger;
+ }
+
+ /**
+ * @return array
+ */
+ public function getModules() {
+ return $this->modules;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLanguage() {
+ if ( $this->language === null ) {
+ // Must be a valid language code after this point (T64849)
+ // Only support uselang values that follow built-in conventions (T102058)
+ $lang = $this->getRequest()->getRawVal( 'lang', '' );
+ // Stricter version of RequestContext::sanitizeLangCode()
+ if ( !Language::isValidBuiltInCode( $lang ) ) {
+ $lang = $this->getResourceLoader()->getConfig()->get( 'LanguageCode' );
+ }
+ $this->language = $lang;
+ }
+ return $this->language;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDirection() {
+ if ( $this->direction === null ) {
+ $this->direction = $this->getRequest()->getRawVal( 'dir' );
+ if ( !$this->direction ) {
+ // Determine directionality based on user language (T8100)
+ $this->direction = Language::factory( $this->getLanguage() )->getDir();
+ }
+ }
+ return $this->direction;
+ }
+
+ /**
+ * @return string
+ */
+ public function getSkin() {
+ return $this->skin;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getUser() {
+ return $this->user;
+ }
+
+ /**
+ * Get a Message object with context set. See wfMessage for parameters.
+ *
+ * @since 1.27
+ * @param string|string[]|MessageSpecifier $key Message key, or array of keys,
+ * or a MessageSpecifier.
+ * @param mixed $args,...
+ * @return Message
+ */
+ public function msg( $key ) {
+ return call_user_func_array( 'wfMessage', func_get_args() )
+ ->inLanguage( $this->getLanguage() )
+ // Use a dummy title because there is no real title
+ // for this endpoint, and the cache won't vary on it
+ // anyways.
+ ->title( Title::newFromText( 'Dwimmerlaik' ) );
+ }
+
+ /**
+ * Get the possibly-cached User object for the specified username
+ *
+ * @since 1.25
+ * @return User
+ */
+ public function getUserObj() {
+ if ( $this->userObj === null ) {
+ $username = $this->getUser();
+ if ( $username ) {
+ // Use provided username if valid, fallback to anonymous user
+ $this->userObj = User::newFromName( $username ) ?: new User;
+ } else {
+ // Anonymous user
+ $this->userObj = new User;
+ }
+ }
+
+ return $this->userObj;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getDebug() {
+ return $this->debug;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getOnly() {
+ return $this->only;
+ }
+
+ /**
+ * @see ResourceLoaderModule::getVersionHash
+ * @see ResourceLoaderClientHtml::makeLoad
+ * @return string|null
+ */
+ public function getVersion() {
+ return $this->version;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getRaw() {
+ return $this->raw;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getImage() {
+ return $this->image;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getVariant() {
+ return $this->variant;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getFormat() {
+ return $this->format;
+ }
+
+ /**
+ * If this is a request for an image, get the ResourceLoaderImage object.
+ *
+ * @since 1.25
+ * @return ResourceLoaderImage|bool false if a valid object cannot be created
+ */
+ public function getImageObj() {
+ if ( $this->imageObj === null ) {
+ $this->imageObj = false;
+
+ if ( !$this->image ) {
+ return $this->imageObj;
+ }
+
+ $modules = $this->getModules();
+ if ( count( $modules ) !== 1 ) {
+ return $this->imageObj;
+ }
+
+ $module = $this->getResourceLoader()->getModule( $modules[0] );
+ if ( !$module || !$module instanceof ResourceLoaderImageModule ) {
+ return $this->imageObj;
+ }
+
+ $image = $module->getImage( $this->image, $this );
+ if ( !$image ) {
+ return $this->imageObj;
+ }
+
+ $this->imageObj = $image;
+ }
+
+ return $this->imageObj;
+ }
+
+ /**
+ * @return bool
+ */
+ public function shouldIncludeScripts() {
+ return $this->getOnly() === null || $this->getOnly() === 'scripts';
+ }
+
+ /**
+ * @return bool
+ */
+ public function shouldIncludeStyles() {
+ return $this->getOnly() === null || $this->getOnly() === 'styles';
+ }
+
+ /**
+ * @return bool
+ */
+ public function shouldIncludeMessages() {
+ return $this->getOnly() === null;
+ }
+
+ /**
+ * All factors that uniquely identify this request, except 'modules'.
+ *
+ * The list of modules is excluded here for legacy reasons as most callers already
+ * split up handling of individual modules. Including it here would massively fragment
+ * the cache and decrease its usefulness.
+ *
+ * E.g. Used by RequestFileCache to form a cache key for storing the reponse output.
+ *
+ * @return string
+ */
+ public function getHash() {
+ if ( !isset( $this->hash ) ) {
+ $this->hash = implode( '|', [
+ // Module content vary
+ $this->getLanguage(),
+ $this->getSkin(),
+ $this->getDebug(),
+ $this->getUser(),
+ // Request vary
+ $this->getOnly(),
+ $this->getVersion(),
+ $this->getRaw(),
+ $this->getImage(),
+ $this->getVariant(),
+ $this->getFormat(),
+ ] );
+ }
+ return $this->hash;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderEditToolbarModule.php b/www/wiki/includes/resourceloader/ResourceLoaderEditToolbarModule.php
new file mode 100644
index 00000000..2a6af715
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderEditToolbarModule.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * ResourceLoader module for the edit toolbar.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for the edit toolbar.
+ *
+ * @since 1.24
+ */
+class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule {
+ /**
+ * Get language-specific LESS variables for this module.
+ *
+ * @since 1.27
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ protected function getLessVars( ResourceLoaderContext $context ) {
+ $vars = parent::getLessVars( $context );
+ $language = Language::factory( $context->getLanguage() );
+ foreach ( $language->getImageFiles() as $key => $value ) {
+ $vars[$key] = CSSMin::serializeStringValue( $value );
+ }
+ return $vars;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderFileModule.php b/www/wiki/includes/resourceloader/ResourceLoaderFileModule.php
new file mode 100644
index 00000000..f2f3383f
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderFileModule.php
@@ -0,0 +1,1038 @@
+<?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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+/**
+ * ResourceLoader module based on local JavaScript/CSS files.
+ */
+class ResourceLoaderFileModule extends ResourceLoaderModule {
+
+ /** @var string Local base path, see __construct() */
+ protected $localBasePath = '';
+
+ /** @var string Remote base path, see __construct() */
+ protected $remoteBasePath = '';
+
+ /** @var array Saves a list of the templates named by the modules. */
+ protected $templates = [];
+
+ /**
+ * @var array List of paths to JavaScript files to always include
+ * @par Usage:
+ * @code
+ * [ [file-path], [file-path], ... ]
+ * @endcode
+ */
+ protected $scripts = [];
+
+ /**
+ * @var array List of JavaScript files to include when using a specific language
+ * @par Usage:
+ * @code
+ * [ [language-code] => [ [file-path], [file-path], ... ], ... ]
+ * @endcode
+ */
+ protected $languageScripts = [];
+
+ /**
+ * @var array List of JavaScript files to include when using a specific skin
+ * @par Usage:
+ * @code
+ * [ [skin-name] => [ [file-path], [file-path], ... ], ... ]
+ * @endcode
+ */
+ protected $skinScripts = [];
+
+ /**
+ * @var array List of paths to JavaScript files to include in debug mode
+ * @par Usage:
+ * @code
+ * [ [skin-name] => [ [file-path], [file-path], ... ], ... ]
+ * @endcode
+ */
+ protected $debugScripts = [];
+
+ /**
+ * @var array List of paths to CSS files to always include
+ * @par Usage:
+ * @code
+ * [ [file-path], [file-path], ... ]
+ * @endcode
+ */
+ protected $styles = [];
+
+ /**
+ * @var array List of paths to CSS files to include when using specific skins
+ * @par Usage:
+ * @code
+ * [ [file-path], [file-path], ... ]
+ * @endcode
+ */
+ protected $skinStyles = [];
+
+ /**
+ * @var array List of modules this module depends on
+ * @par Usage:
+ * @code
+ * [ [file-path], [file-path], ... ]
+ * @endcode
+ */
+ protected $dependencies = [];
+
+ /**
+ * @var string File name containing the body of the skip function
+ */
+ protected $skipFunction = null;
+
+ /**
+ * @var array List of message keys used by this module
+ * @par Usage:
+ * @code
+ * [ [message-key], [message-key], ... ]
+ * @endcode
+ */
+ protected $messages = [];
+
+ /** @var string Name of group to load this module in */
+ protected $group;
+
+ /** @var bool Link to raw files in debug mode */
+ protected $debugRaw = true;
+
+ /** @var bool Whether mw.loader.state() call should be omitted */
+ protected $raw = false;
+
+ protected $targets = [ 'desktop' ];
+
+ /** @var bool Whether CSSJanus flipping should be skipped for this module */
+ protected $noflip = false;
+
+ /**
+ * @var bool Whether getStyleURLsForDebug should return raw file paths,
+ * or return load.php urls
+ */
+ protected $hasGeneratedStyles = false;
+
+ /**
+ * @var array Place where readStyleFile() tracks file dependencies
+ * @par Usage:
+ * @code
+ * [ [file-path], [file-path], ... ]
+ * @endcode
+ */
+ protected $localFileRefs = [];
+
+ /**
+ * @var array Place where readStyleFile() tracks file dependencies for non-existent files.
+ * Used in tests to detect missing dependencies.
+ */
+ protected $missingLocalFileRefs = [];
+
+ /**
+ * Constructs a new module from an options array.
+ *
+ * @param array $options List of options; if not given or empty, an empty module will be
+ * constructed
+ * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
+ * to $IP
+ * @param string $remoteBasePath Base path to prepend to all remote paths in $options. Defaults
+ * to $wgResourceBasePath
+ *
+ * Below is a description for the $options array:
+ * @throws InvalidArgumentException
+ * @par Construction options:
+ * @code
+ * [
+ * // Base path to prepend to all local paths in $options. Defaults to $IP
+ * 'localBasePath' => [base path],
+ * // Base path to prepend to all remote paths in $options. Defaults to $wgResourceBasePath
+ * 'remoteBasePath' => [base path],
+ * // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
+ * 'remoteExtPath' => [base path],
+ * // Equivalent of remoteBasePath, but relative to $wgStylePath
+ * 'remoteSkinPath' => [base path],
+ * // Scripts to always include
+ * 'scripts' => [file path string or array of file path strings],
+ * // Scripts to include in specific language contexts
+ * 'languageScripts' => [
+ * [language code] => [file path string or array of file path strings],
+ * ],
+ * // Scripts to include in specific skin contexts
+ * 'skinScripts' => [
+ * [skin name] => [file path string or array of file path strings],
+ * ],
+ * // Scripts to include in debug contexts
+ * 'debugScripts' => [file path string or array of file path strings],
+ * // Modules which must be loaded before this module
+ * 'dependencies' => [module name string or array of module name strings],
+ * 'templates' => [
+ * [template alias with file.ext] => [file path to a template file],
+ * ],
+ * // Styles to always load
+ * 'styles' => [file path string or array of file path strings],
+ * // Styles to include in specific skin contexts
+ * 'skinStyles' => [
+ * [skin name] => [file path string or array of file path strings],
+ * ],
+ * // Messages to always load
+ * 'messages' => [array of message key strings],
+ * // Group which this module should be loaded together with
+ * 'group' => [group name string],
+ * // Function that, if it returns true, makes the loader skip this module.
+ * // The file must contain valid JavaScript for execution in a private function.
+ * // The file must not contain the "function () {" and "}" wrapper though.
+ * 'skipFunction' => [file path]
+ * ]
+ * @endcode
+ */
+ public function __construct(
+ $options = [],
+ $localBasePath = null,
+ $remoteBasePath = null
+ ) {
+ // Flag to decide whether to automagically add the mediawiki.template module
+ $hasTemplates = false;
+ // localBasePath and remoteBasePath both have unbelievably long fallback chains
+ // and need to be handled separately.
+ list( $this->localBasePath, $this->remoteBasePath ) =
+ self::extractBasePaths( $options, $localBasePath, $remoteBasePath );
+
+ // Extract, validate and normalise remaining options
+ foreach ( $options as $member => $option ) {
+ switch ( $member ) {
+ // Lists of file paths
+ case 'scripts':
+ case 'debugScripts':
+ case 'styles':
+ $this->{$member} = (array)$option;
+ break;
+ case 'templates':
+ $hasTemplates = true;
+ $this->{$member} = (array)$option;
+ break;
+ // Collated lists of file paths
+ case 'languageScripts':
+ case 'skinScripts':
+ case 'skinStyles':
+ if ( !is_array( $option ) ) {
+ throw new InvalidArgumentException(
+ "Invalid collated file path list error. " .
+ "'$option' given, array expected."
+ );
+ }
+ foreach ( $option as $key => $value ) {
+ if ( !is_string( $key ) ) {
+ throw new InvalidArgumentException(
+ "Invalid collated file path list key error. " .
+ "'$key' given, string expected."
+ );
+ }
+ $this->{$member}[$key] = (array)$value;
+ }
+ break;
+ case 'deprecated':
+ $this->deprecated = $option;
+ break;
+ // Lists of strings
+ case 'dependencies':
+ case 'messages':
+ case 'targets':
+ // Normalise
+ $option = array_values( array_unique( (array)$option ) );
+ sort( $option );
+
+ $this->{$member} = $option;
+ break;
+ // Single strings
+ case 'group':
+ case 'skipFunction':
+ $this->{$member} = (string)$option;
+ break;
+ // Single booleans
+ case 'debugRaw':
+ case 'raw':
+ case 'noflip':
+ $this->{$member} = (bool)$option;
+ break;
+ }
+ }
+ if ( $hasTemplates ) {
+ $this->dependencies[] = 'mediawiki.template';
+ // Ensure relevant template compiler module gets loaded
+ foreach ( $this->templates as $alias => $templatePath ) {
+ if ( is_int( $alias ) ) {
+ $alias = $templatePath;
+ }
+ $suffix = explode( '.', $alias );
+ $suffix = end( $suffix );
+ $compilerModule = 'mediawiki.template.' . $suffix;
+ if ( $suffix !== 'html' && !in_array( $compilerModule, $this->dependencies ) ) {
+ $this->dependencies[] = $compilerModule;
+ }
+ }
+ }
+ }
+
+ /**
+ * Extract a pair of local and remote base paths from module definition information.
+ * Implementation note: the amount of global state used in this function is staggering.
+ *
+ * @param array $options Module definition
+ * @param string $localBasePath Path to use if not provided in module definition. Defaults
+ * to $IP
+ * @param string $remoteBasePath Path to use if not provided in module definition. Defaults
+ * to $wgResourceBasePath
+ * @return array Array( localBasePath, remoteBasePath )
+ */
+ public static function extractBasePaths(
+ $options = [],
+ $localBasePath = null,
+ $remoteBasePath = null
+ ) {
+ global $IP, $wgResourceBasePath;
+
+ // The different ways these checks are done, and their ordering, look very silly,
+ // but were preserved for backwards-compatibility just in case. Tread lightly.
+
+ if ( $localBasePath === null ) {
+ $localBasePath = $IP;
+ }
+ if ( $remoteBasePath === null ) {
+ $remoteBasePath = $wgResourceBasePath;
+ }
+
+ if ( isset( $options['remoteExtPath'] ) ) {
+ global $wgExtensionAssetsPath;
+ $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
+ }
+
+ if ( isset( $options['remoteSkinPath'] ) ) {
+ global $wgStylePath;
+ $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
+ }
+
+ if ( array_key_exists( 'localBasePath', $options ) ) {
+ $localBasePath = (string)$options['localBasePath'];
+ }
+
+ if ( array_key_exists( 'remoteBasePath', $options ) ) {
+ $remoteBasePath = (string)$options['remoteBasePath'];
+ }
+
+ return [ $localBasePath, $remoteBasePath ];
+ }
+
+ /**
+ * Gets all scripts for a given context concatenated together.
+ *
+ * @param ResourceLoaderContext $context Context in which to generate script
+ * @return string JavaScript code for $context
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ $files = $this->getScriptFiles( $context );
+ return $this->getDeprecationInformation() . $this->readScriptFiles( $files );
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
+ $urls = [];
+ foreach ( $this->getScriptFiles( $context ) as $file ) {
+ $urls[] = OutputPage::transformResourcePath(
+ $this->getConfig(),
+ $this->getRemotePath( $file )
+ );
+ }
+ return $urls;
+ }
+
+ /**
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return $this->debugRaw;
+ }
+
+ /**
+ * Get all styles for a given context.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array CSS code for $context as an associative array mapping media type to CSS text.
+ */
+ public function getStyles( ResourceLoaderContext $context ) {
+ $styles = $this->readStyleFiles(
+ $this->getStyleFiles( $context ),
+ $this->getFlip( $context ),
+ $context
+ );
+ // Collect referenced files
+ $this->saveFileDependencies( $context, $this->localFileRefs );
+
+ return $styles;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
+ if ( $this->hasGeneratedStyles ) {
+ // Do the default behaviour of returning a url back to load.php
+ // but with only=styles.
+ return parent::getStyleURLsForDebug( $context );
+ }
+ // Our module consists entirely of real css files,
+ // in debug mode we can load those directly.
+ $urls = [];
+ foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
+ $urls[$mediaType] = [];
+ foreach ( $list as $file ) {
+ $urls[$mediaType][] = OutputPage::transformResourcePath(
+ $this->getConfig(),
+ $this->getRemotePath( $file )
+ );
+ }
+ }
+ return $urls;
+ }
+
+ /**
+ * Gets list of message keys used by this module.
+ *
+ * @return array List of message keys
+ */
+ public function getMessages() {
+ return $this->messages;
+ }
+
+ /**
+ * Gets the name of the group this module should be loaded in.
+ *
+ * @return string Group name
+ */
+ public function getGroup() {
+ return $this->group;
+ }
+
+ /**
+ * Gets list of names of modules this module depends on.
+ * @param ResourceLoaderContext|null $context
+ * @return array List of module names
+ */
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ return $this->dependencies;
+ }
+
+ /**
+ * Get the skip function.
+ * @return null|string
+ * @throws MWException
+ */
+ public function getSkipFunction() {
+ if ( !$this->skipFunction ) {
+ return null;
+ }
+
+ $localPath = $this->getLocalPath( $this->skipFunction );
+ if ( !file_exists( $localPath ) ) {
+ throw new MWException( __METHOD__ . ": skip function file not found: \"$localPath\"" );
+ }
+ $contents = $this->stripBom( file_get_contents( $localPath ) );
+ if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
+ $contents = $this->validateScriptFile( $localPath, $contents );
+ }
+ return $contents;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRaw() {
+ return $this->raw;
+ }
+
+ /**
+ * Disable module content versioning.
+ *
+ * This class uses getDefinitionSummary() instead, to avoid filesystem overhead
+ * involved with building the full module content inside a startup request.
+ *
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return false;
+ }
+
+ /**
+ * Helper method to gather file hashes for getDefinitionSummary.
+ *
+ * This function is context-sensitive, only computing hashes of files relevant to the
+ * given language, skin, etc.
+ *
+ * @see ResourceLoaderModule::getFileDependencies
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ protected function getFileHashes( ResourceLoaderContext $context ) {
+ $files = [];
+
+ // Flatten style files into $files
+ $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' );
+ foreach ( $styles as $styleFiles ) {
+ $files = array_merge( $files, $styleFiles );
+ }
+
+ $skinFiles = self::collateFilePathListByOption(
+ self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
+ 'media',
+ 'all'
+ );
+ foreach ( $skinFiles as $styleFiles ) {
+ $files = array_merge( $files, $styleFiles );
+ }
+
+ // Final merge, this should result in a master list of dependent files
+ $files = array_merge(
+ $files,
+ $this->scripts,
+ $this->templates,
+ $context->getDebug() ? $this->debugScripts : [],
+ $this->getLanguageScripts( $context->getLanguage() ),
+ self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
+ );
+ if ( $this->skipFunction ) {
+ $files[] = $this->skipFunction;
+ }
+ $files = array_map( [ $this, 'getLocalPath' ], $files );
+ // File deps need to be treated separately because they're already prefixed
+ $files = array_merge( $files, $this->getFileDependencies( $context ) );
+ // Filter out any duplicates from getFileDependencies() and others.
+ // Most commonly introduced by compileLessFile(), which always includes the
+ // entry point Less file we already know about.
+ $files = array_values( array_unique( $files ) );
+
+ // Don't include keys or file paths here, only the hashes. Including that would needlessly
+ // cause global cache invalidation when files move or if e.g. the MediaWiki path changes.
+ // Any significant ordering is already detected by the definition summary.
+ return array_map( [ __CLASS__, 'safeFileHash' ], $files );
+ }
+
+ /**
+ * Get the definition summary for this module.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getDefinitionSummary( ResourceLoaderContext $context ) {
+ $summary = parent::getDefinitionSummary( $context );
+
+ $options = [];
+ foreach ( [
+ // The following properties are omitted because they don't affect the module reponse:
+ // - localBasePath (Per T104950; Changes when absolute directory name changes. If
+ // this affects 'scripts' and other file paths, getFileHashes accounts for that.)
+ // - remoteBasePath (Per T104950)
+ // - dependencies (provided via startup module)
+ // - targets
+ // - group (provided via startup module)
+ 'scripts',
+ 'debugScripts',
+ 'styles',
+ 'languageScripts',
+ 'skinScripts',
+ 'skinStyles',
+ 'messages',
+ 'templates',
+ 'skipFunction',
+ 'debugRaw',
+ 'raw',
+ ] as $member ) {
+ $options[$member] = $this->{$member};
+ };
+
+ $summary[] = [
+ 'options' => $options,
+ 'fileHashes' => $this->getFileHashes( $context ),
+ 'messageBlob' => $this->getMessageBlob( $context ),
+ ];
+
+ $lessVars = $this->getLessVars( $context );
+ if ( $lessVars ) {
+ $summary[] = [ 'lessVars' => $lessVars ];
+ }
+
+ return $summary;
+ }
+
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getLocalPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getLocalPath();
+ }
+
+ return "{$this->localBasePath}/$path";
+ }
+
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getRemotePath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getRemotePath();
+ }
+
+ return "{$this->remoteBasePath}/$path";
+ }
+
+ /**
+ * Infer the stylesheet language from a stylesheet file path.
+ *
+ * @since 1.22
+ * @param string $path
+ * @return string The stylesheet language name
+ */
+ public function getStyleSheetLang( $path ) {
+ return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
+ }
+
+ /**
+ * Collates file paths by option (where provided).
+ *
+ * @param array $list List of file paths in any combination of index/path
+ * or path/options pairs
+ * @param string $option Option name
+ * @param mixed $default Default value if the option isn't set
+ * @return array List of file paths, collated by $option
+ */
+ protected static function collateFilePathListByOption( array $list, $option, $default ) {
+ $collatedFiles = [];
+ foreach ( (array)$list as $key => $value ) {
+ if ( is_int( $key ) ) {
+ // File name as the value
+ if ( !isset( $collatedFiles[$default] ) ) {
+ $collatedFiles[$default] = [];
+ }
+ $collatedFiles[$default][] = $value;
+ } elseif ( is_array( $value ) ) {
+ // File name as the key, options array as the value
+ $optionValue = isset( $value[$option] ) ? $value[$option] : $default;
+ if ( !isset( $collatedFiles[$optionValue] ) ) {
+ $collatedFiles[$optionValue] = [];
+ }
+ $collatedFiles[$optionValue][] = $key;
+ }
+ }
+ return $collatedFiles;
+ }
+
+ /**
+ * Get a list of element that match a key, optionally using a fallback key.
+ *
+ * @param array $list List of lists to select from
+ * @param string $key Key to look for in $map
+ * @param string $fallback Key to look for in $list if $key doesn't exist
+ * @return array List of elements from $map which matched $key or $fallback,
+ * or an empty list in case of no match
+ */
+ protected static function tryForKey( array $list, $key, $fallback = null ) {
+ if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
+ return $list[$key];
+ } elseif ( is_string( $fallback )
+ && isset( $list[$fallback] )
+ && is_array( $list[$fallback] )
+ ) {
+ return $list[$fallback];
+ }
+ return [];
+ }
+
+ /**
+ * Get a list of file paths for all scripts in this module, in order of proper execution.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array List of file paths
+ */
+ protected function getScriptFiles( ResourceLoaderContext $context ) {
+ $files = array_merge(
+ $this->scripts,
+ $this->getLanguageScripts( $context->getLanguage() ),
+ self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
+ );
+ if ( $context->getDebug() ) {
+ $files = array_merge( $files, $this->debugScripts );
+ }
+
+ return array_unique( $files, SORT_REGULAR );
+ }
+
+ /**
+ * Get the set of language scripts for the given language,
+ * possibly using a fallback language.
+ *
+ * @param string $lang
+ * @return array
+ */
+ private function getLanguageScripts( $lang ) {
+ $scripts = self::tryForKey( $this->languageScripts, $lang );
+ if ( $scripts ) {
+ return $scripts;
+ }
+ $fallbacks = Language::getFallbacksFor( $lang );
+ foreach ( $fallbacks as $lang ) {
+ $scripts = self::tryForKey( $this->languageScripts, $lang );
+ if ( $scripts ) {
+ return $scripts;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Get a list of file paths for all styles in this module, in order of proper inclusion.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array List of file paths
+ */
+ public function getStyleFiles( ResourceLoaderContext $context ) {
+ return array_merge_recursive(
+ self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
+ self::collateFilePathListByOption(
+ self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
+ 'media',
+ 'all'
+ )
+ );
+ }
+
+ /**
+ * Gets a list of file paths for all skin styles in the module used by
+ * the skin.
+ *
+ * @param string $skinName The name of the skin
+ * @return array A list of file paths collated by media type
+ */
+ protected function getSkinStyleFiles( $skinName ) {
+ return self::collateFilePathListByOption(
+ self::tryForKey( $this->skinStyles, $skinName ),
+ 'media',
+ 'all'
+ );
+ }
+
+ /**
+ * Gets a list of file paths for all skin style files in the module,
+ * for all available skins.
+ *
+ * @return array A list of file paths collated by media type
+ */
+ protected function getAllSkinStyleFiles() {
+ $styleFiles = [];
+ $internalSkinNames = array_keys( Skin::getSkinNames() );
+ $internalSkinNames[] = 'default';
+
+ foreach ( $internalSkinNames as $internalSkinName ) {
+ $styleFiles = array_merge_recursive(
+ $styleFiles,
+ $this->getSkinStyleFiles( $internalSkinName )
+ );
+ }
+
+ return $styleFiles;
+ }
+
+ /**
+ * Returns all style files and all skin style files used by this module.
+ *
+ * @return array
+ */
+ public function getAllStyleFiles() {
+ $collatedStyleFiles = array_merge_recursive(
+ self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
+ $this->getAllSkinStyleFiles()
+ );
+
+ $result = [];
+
+ foreach ( $collatedStyleFiles as $media => $styleFiles ) {
+ foreach ( $styleFiles as $styleFile ) {
+ $result[] = $this->getLocalPath( $styleFile );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Gets the contents of a list of JavaScript files.
+ *
+ * @param array $scripts List of file paths to scripts to read, remap and concetenate
+ * @throws MWException
+ * @return string Concatenated and remapped JavaScript data from $scripts
+ */
+ protected function readScriptFiles( array $scripts ) {
+ if ( empty( $scripts ) ) {
+ return '';
+ }
+ $js = '';
+ foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
+ $localPath = $this->getLocalPath( $fileName );
+ if ( !file_exists( $localPath ) ) {
+ throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" );
+ }
+ $contents = $this->stripBom( file_get_contents( $localPath ) );
+ if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
+ // Static files don't really need to be checked as often; unlike
+ // on-wiki module they shouldn't change unexpectedly without
+ // admin interference.
+ $contents = $this->validateScriptFile( $fileName, $contents );
+ }
+ $js .= $contents . "\n";
+ }
+ return $js;
+ }
+
+ /**
+ * Gets the contents of a list of CSS files.
+ *
+ * @param array $styles List of media type/list of file paths pairs, to read, remap and
+ * concetenate
+ * @param bool $flip
+ * @param ResourceLoaderContext $context
+ *
+ * @throws MWException
+ * @return array List of concatenated and remapped CSS data from $styles,
+ * keyed by media type
+ *
+ * @since 1.27 Calling this method without a ResourceLoaderContext instance
+ * is deprecated.
+ */
+ public function readStyleFiles( array $styles, $flip, $context = null ) {
+ if ( $context === null ) {
+ wfDeprecated( __METHOD__ . ' without a ResourceLoader context', '1.27' );
+ $context = ResourceLoaderContext::newDummyContext();
+ }
+
+ if ( empty( $styles ) ) {
+ return [];
+ }
+ foreach ( $styles as $media => $files ) {
+ $uniqueFiles = array_unique( $files, SORT_REGULAR );
+ $styleFiles = [];
+ foreach ( $uniqueFiles as $file ) {
+ $styleFiles[] = $this->readStyleFile( $file, $flip, $context );
+ }
+ $styles[$media] = implode( "\n", $styleFiles );
+ }
+ return $styles;
+ }
+
+ /**
+ * Reads a style file.
+ *
+ * This method can be used as a callback for array_map()
+ *
+ * @param string $path File path of style file to read
+ * @param bool $flip
+ * @param ResourceLoaderContext $context
+ *
+ * @return string CSS data in script file
+ * @throws MWException If the file doesn't exist
+ */
+ protected function readStyleFile( $path, $flip, $context ) {
+ $localPath = $this->getLocalPath( $path );
+ $remotePath = $this->getRemotePath( $path );
+ if ( !file_exists( $localPath ) ) {
+ $msg = __METHOD__ . ": style file not found: \"$localPath\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+
+ if ( $this->getStyleSheetLang( $localPath ) === 'less' ) {
+ $style = $this->compileLessFile( $localPath, $context );
+ $this->hasGeneratedStyles = true;
+ } else {
+ $style = $this->stripBom( file_get_contents( $localPath ) );
+ }
+
+ if ( $flip ) {
+ $style = CSSJanus::transform( $style, true, false );
+ }
+ $localDir = dirname( $localPath );
+ $remoteDir = dirname( $remotePath );
+ // Get and register local file references
+ $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
+ foreach ( $localFileRefs as $file ) {
+ if ( file_exists( $file ) ) {
+ $this->localFileRefs[] = $file;
+ } else {
+ $this->missingLocalFileRefs[] = $file;
+ }
+ }
+ // Don't cache this call. remap() ensures data URIs embeds are up to date,
+ // and urls contain correct content hashes in their query string. (T128668)
+ return CSSMin::remap( $style, $localDir, $remoteDir, true );
+ }
+
+ /**
+ * Get whether CSS for this module should be flipped
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function getFlip( $context ) {
+ return $context->getDirection() === 'rtl' && !$this->noflip;
+ }
+
+ /**
+ * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
+ *
+ * @return array Array of strings
+ */
+ public function getTargets() {
+ return $this->targets;
+ }
+
+ /**
+ * Get the module's load type.
+ *
+ * @since 1.28
+ * @return string
+ */
+ public function getType() {
+ $canBeStylesOnly = !(
+ // All options except 'styles', 'skinStyles' and 'debugRaw'
+ $this->scripts
+ || $this->debugScripts
+ || $this->templates
+ || $this->languageScripts
+ || $this->skinScripts
+ || $this->dependencies
+ || $this->messages
+ || $this->skipFunction
+ || $this->raw
+ );
+ return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
+ }
+
+ /**
+ * Compile a LESS file into CSS.
+ *
+ * Keeps track of all used files and adds them to localFileRefs.
+ *
+ * @since 1.22
+ * @since 1.27 Added $context paramter.
+ * @throws Exception If less.php encounters a parse error
+ * @param string $fileName File path of LESS source
+ * @param ResourceLoaderContext $context Context in which to generate script
+ * @return string CSS source
+ */
+ protected function compileLessFile( $fileName, ResourceLoaderContext $context ) {
+ static $cache;
+
+ if ( !$cache ) {
+ $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
+ }
+
+ // Construct a cache key from the LESS file name and a hash digest
+ // of the LESS variables used for compilation.
+ $vars = $this->getLessVars( $context );
+ ksort( $vars );
+ $varsHash = hash( 'md4', serialize( $vars ) );
+ $cacheKey = $cache->makeGlobalKey( 'LESS', $fileName, $varsHash );
+ $cachedCompile = $cache->get( $cacheKey );
+
+ // If we got a cached value, we have to validate it by getting a
+ // checksum of all the files that were loaded by the parser and
+ // ensuring it matches the cached entry's.
+ if ( isset( $cachedCompile['hash'] ) ) {
+ $contentHash = FileContentsHasher::getFileContentsHash( $cachedCompile['files'] );
+ if ( $contentHash === $cachedCompile['hash'] ) {
+ $this->localFileRefs = array_merge( $this->localFileRefs, $cachedCompile['files'] );
+ return $cachedCompile['css'];
+ }
+ }
+
+ $compiler = $context->getResourceLoader()->getLessCompiler( $vars );
+ $css = $compiler->parseFile( $fileName )->getCss();
+ $files = $compiler->AllParsedFiles();
+ $this->localFileRefs = array_merge( $this->localFileRefs, $files );
+
+ // Cache for 24 hours (86400 seconds).
+ $cache->set( $cacheKey, [
+ 'css' => $css,
+ 'files' => $files,
+ 'hash' => FileContentsHasher::getFileContentsHash( $files ),
+ ], 3600 * 24 );
+
+ return $css;
+ }
+
+ /**
+ * Takes named templates by the module and returns an array mapping.
+ * @return array Templates mapping template alias to content
+ * @throws MWException
+ */
+ public function getTemplates() {
+ $templates = [];
+
+ foreach ( $this->templates as $alias => $templatePath ) {
+ // Alias is optional
+ if ( is_int( $alias ) ) {
+ $alias = $templatePath;
+ }
+ $localPath = $this->getLocalPath( $templatePath );
+ if ( file_exists( $localPath ) ) {
+ $content = file_get_contents( $localPath );
+ $templates[$alias] = $this->stripBom( $content );
+ } else {
+ $msg = __METHOD__ . ": template file not found: \"$localPath\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+ }
+ return $templates;
+ }
+
+ /**
+ * Takes an input string and removes the UTF-8 BOM character if present
+ *
+ * We need to remove these after reading a file, because we concatenate our files and
+ * the BOM character is not valid in the middle of a string.
+ * We already assume UTF-8 everywhere, so this should be safe.
+ *
+ * @param string $input
+ * @return string Input minus the intial BOM char
+ */
+ protected function stripBom( $input ) {
+ if ( substr_compare( "\xef\xbb\xbf", $input, 0, 3 ) === 0 ) {
+ return substr( $input, 3 );
+ }
+ return $input;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderFilePath.php b/www/wiki/includes/resourceloader/ResourceLoaderFilePath.php
new file mode 100644
index 00000000..3cf09d82
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderFilePath.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * An object to represent a path to a JavaScript/CSS file, along with a remote
+ * and local base path, for use with ResourceLoaderFileModule.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * An object to represent a path to a JavaScript/CSS file, along with a remote
+ * and local base path, for use with ResourceLoaderFileModule.
+ */
+class ResourceLoaderFilePath {
+
+ /** @var string Local base path */
+ protected $localBasePath;
+
+ /** @var string Remote base path */
+ protected $remoteBasePath;
+
+ /**
+ * @var string Path to the file */
+ protected $path;
+
+ /**
+ * @param string $path Path to the file.
+ * @param string $localBasePath Base path to prepend when generating a local path.
+ * @param string $remoteBasePath Base path to prepend when generating a remote path.
+ */
+ public function __construct( $path, $localBasePath, $remoteBasePath ) {
+ $this->path = $path;
+ $this->localBasePath = $localBasePath;
+ $this->remoteBasePath = $remoteBasePath;
+ }
+
+ /**
+ * @return string
+ */
+ public function getLocalPath() {
+ return "{$this->localBasePath}/{$this->path}";
+ }
+
+ /**
+ * @return string
+ */
+ public function getRemotePath() {
+ return "{$this->remoteBasePath}/{$this->path}";
+ }
+
+ /**
+ * @return string
+ */
+ public function getPath() {
+ return $this->path;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderForeignApiModule.php b/www/wiki/includes/resourceloader/ResourceLoaderForeignApiModule.php
new file mode 100644
index 00000000..4d215d6f
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderForeignApiModule.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * ResourceLoader module for mediawiki.ForeignApi that has dynamically
+ * generated dependencies, via a hook usable by extensions.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for mediawiki.ForeignApi and its generated data
+ */
+class ResourceLoaderForeignApiModule extends ResourceLoaderFileModule {
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ $dependencies = $this->dependencies;
+ Hooks::run( 'ResourceLoaderForeignApiModules', [ &$dependencies, $context ] );
+ return $dependencies;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderImage.php b/www/wiki/includes/resourceloader/ResourceLoaderImage.php
new file mode 100644
index 00000000..d38a1750
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderImage.php
@@ -0,0 +1,413 @@
+<?php
+/**
+ * Class encapsulating an image used in a ResourceLoaderImageModule.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Class encapsulating an image used in a ResourceLoaderImageModule.
+ *
+ * @since 1.25
+ */
+class ResourceLoaderImage {
+
+ /**
+ * Map of allowed file extensions to their MIME types.
+ * @var array
+ */
+ protected static $fileTypes = [
+ 'svg' => 'image/svg+xml',
+ 'png' => 'image/png',
+ 'gif' => 'image/gif',
+ 'jpg' => 'image/jpg',
+ ];
+
+ /**
+ * @param string $name Image name
+ * @param string $module Module name
+ * @param string|array $descriptor Path to image file, or array structure containing paths
+ * @param string $basePath Directory to which paths in descriptor refer
+ * @param array $variants
+ * @throws InvalidArgumentException
+ */
+ public function __construct( $name, $module, $descriptor, $basePath, $variants ) {
+ $this->name = $name;
+ $this->module = $module;
+ $this->descriptor = $descriptor;
+ $this->basePath = $basePath;
+ $this->variants = $variants;
+
+ // Expand shorthands:
+ // [ "en,de,fr" => "foo.svg" ]
+ // → [ "en" => "foo.svg", "de" => "foo.svg", "fr" => "foo.svg" ]
+ if ( is_array( $this->descriptor ) && isset( $this->descriptor['lang'] ) ) {
+ foreach ( array_keys( $this->descriptor['lang'] ) as $langList ) {
+ if ( strpos( $langList, ',' ) !== false ) {
+ $this->descriptor['lang'] += array_fill_keys(
+ explode( ',', $langList ),
+ $this->descriptor['lang'][$langList]
+ );
+ unset( $this->descriptor['lang'][$langList] );
+ }
+ }
+ }
+ // Remove 'deprecated' key
+ if ( is_array( $this->descriptor ) ) {
+ unset( $this->descriptor[ 'deprecated' ] );
+ }
+
+ // Ensure that all files have common extension.
+ $extensions = [];
+ $descriptor = (array)$this->descriptor;
+ array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) {
+ $extensions[] = pathinfo( $path, PATHINFO_EXTENSION );
+ } );
+ $extensions = array_unique( $extensions );
+ if ( count( $extensions ) !== 1 ) {
+ throw new InvalidArgumentException(
+ "File type for different image files of '$name' not the same in module '$module'"
+ );
+ }
+ $ext = $extensions[0];
+ if ( !isset( self::$fileTypes[$ext] ) ) {
+ throw new InvalidArgumentException(
+ "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg) in module '$module'"
+ );
+ }
+ $this->extension = $ext;
+ }
+
+ /**
+ * Get name of this image.
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Get name of the module this image belongs to.
+ *
+ * @return string
+ */
+ public function getModule() {
+ return $this->module;
+ }
+
+ /**
+ * Get the list of variants this image can be converted to.
+ *
+ * @return string[]
+ */
+ public function getVariants() {
+ return array_keys( $this->variants );
+ }
+
+ /**
+ * Get the path to image file for given context.
+ *
+ * @param ResourceLoaderContext $context Any context
+ * @return string
+ */
+ public function getPath( ResourceLoaderContext $context ) {
+ $desc = $this->descriptor;
+ if ( is_string( $desc ) ) {
+ return $this->basePath . '/' . $desc;
+ }
+ if ( isset( $desc['lang'] ) ) {
+ $contextLang = $context->getLanguage();
+ if ( isset( $desc['lang'][$contextLang] ) ) {
+ return $this->basePath . '/' . $desc['lang'][$contextLang];
+ }
+ $fallbacks = Language::getFallbacksFor( $contextLang );
+ foreach ( $fallbacks as $lang ) {
+ // Images will fallback to 'default' instead of 'en', except for 'en-*' variants
+ if (
+ ( $lang !== 'en' || substr( $contextLang, 0, 3 ) === 'en-' ) &&
+ isset( $desc['lang'][$lang] )
+ ) {
+ return $this->basePath . '/' . $desc['lang'][$lang];
+ }
+ }
+ }
+ if ( isset( $desc[$context->getDirection()] ) ) {
+ return $this->basePath . '/' . $desc[$context->getDirection()];
+ }
+ return $this->basePath . '/' . $desc['default'];
+ }
+
+ /**
+ * Get the extension of the image.
+ *
+ * @param string $format Format to get the extension for, 'original' or 'rasterized'
+ * @return string Extension without leading dot, e.g. 'png'
+ */
+ public function getExtension( $format = 'original' ) {
+ if ( $format === 'rasterized' && $this->extension === 'svg' ) {
+ return 'png';
+ }
+ return $this->extension;
+ }
+
+ /**
+ * Get the MIME type of the image.
+ *
+ * @param string $format Format to get the MIME type for, 'original' or 'rasterized'
+ * @return string
+ */
+ public function getMimeType( $format = 'original' ) {
+ $ext = $this->getExtension( $format );
+ return self::$fileTypes[$ext];
+ }
+
+ /**
+ * Get the load.php URL that will produce this image.
+ *
+ * @param ResourceLoaderContext $context Any context
+ * @param string $script URL to load.php
+ * @param string|null $variant Variant to get the URL for
+ * @param string $format Format to get the URL for, 'original' or 'rasterized'
+ * @return string
+ */
+ public function getUrl( ResourceLoaderContext $context, $script, $variant, $format ) {
+ $query = [
+ 'modules' => $this->getModule(),
+ 'image' => $this->getName(),
+ 'variant' => $variant,
+ 'format' => $format,
+ 'lang' => $context->getLanguage(),
+ 'skin' => $context->getSkin(),
+ 'version' => $context->getVersion(),
+ ];
+
+ return wfAppendQuery( $script, $query );
+ }
+
+ /**
+ * Get the data: URI that will produce this image.
+ *
+ * @param ResourceLoaderContext $context Any context
+ * @param string|null $variant Variant to get the URI for
+ * @param string $format Format to get the URI for, 'original' or 'rasterized'
+ * @return string
+ */
+ public function getDataUri( ResourceLoaderContext $context, $variant, $format ) {
+ $type = $this->getMimeType( $format );
+ $contents = $this->getImageData( $context, $variant, $format );
+ return CSSMin::encodeStringAsDataURI( $contents, $type );
+ }
+
+ /**
+ * Get actual image data for this image. This can be saved to a file or sent to the browser to
+ * produce the converted image.
+ *
+ * Call getExtension() or getMimeType() with the same $format argument to learn what file type the
+ * returned data uses.
+ *
+ * @param ResourceLoaderContext $context Image context, or any context if $variant and $format
+ * given.
+ * @param string|null $variant Variant to get the data for. Optional; if given, overrides the data
+ * from $context.
+ * @param string $format Format to get the data for, 'original' or 'rasterized'. Optional; if
+ * given, overrides the data from $context.
+ * @return string|false Possibly binary image data, or false on failure
+ * @throws MWException If the image file doesn't exist
+ */
+ public function getImageData( ResourceLoaderContext $context, $variant = false, $format = false ) {
+ if ( $variant === false ) {
+ $variant = $context->getVariant();
+ }
+ if ( $format === false ) {
+ $format = $context->getFormat();
+ }
+
+ $path = $this->getPath( $context );
+ if ( !file_exists( $path ) ) {
+ throw new MWException( "File '$path' does not exist" );
+ }
+
+ if ( $this->getExtension() !== 'svg' ) {
+ return file_get_contents( $path );
+ }
+
+ if ( $variant && isset( $this->variants[$variant] ) ) {
+ $data = $this->variantize( $this->variants[$variant], $context );
+ } else {
+ $data = file_get_contents( $path );
+ }
+
+ if ( $format === 'rasterized' ) {
+ $data = $this->rasterize( $data );
+ if ( !$data ) {
+ wfDebugLog( 'ResourceLoaderImage', __METHOD__ . " failed to rasterize for $path" );
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Send response headers (using the header() function) that are necessary to correctly serve the
+ * image data for this image, as returned by getImageData().
+ *
+ * Note that the headers are independent of the language or image variant.
+ *
+ * @param ResourceLoaderContext $context Image context
+ */
+ public function sendResponseHeaders( ResourceLoaderContext $context ) {
+ $format = $context->getFormat();
+ $mime = $this->getMimeType( $format );
+ $filename = $this->getName() . '.' . $this->getExtension( $format );
+
+ header( 'Content-Type: ' . $mime );
+ header( 'Content-Disposition: ' .
+ FileBackend::makeContentDisposition( 'inline', $filename ) );
+ }
+
+ /**
+ * Convert this image, which is assumed to be SVG, to given variant.
+ *
+ * @param array $variantConf Array with a 'color' key, its value will be used as fill color
+ * @param ResourceLoaderContext $context Image context
+ * @return string New SVG file data
+ */
+ protected function variantize( $variantConf, ResourceLoaderContext $context ) {
+ $dom = new DomDocument;
+ $dom->loadXML( file_get_contents( $this->getPath( $context ) ) );
+ $root = $dom->documentElement;
+ $wrapper = $dom->createElement( 'g' );
+ while ( $root->firstChild ) {
+ $wrapper->appendChild( $root->firstChild );
+ }
+ $root->appendChild( $wrapper );
+ $wrapper->setAttribute( 'fill', $variantConf['color'] );
+ return $dom->saveXML();
+ }
+
+ /**
+ * Massage the SVG image data for converters which don't understand some path data syntax.
+ *
+ * This is necessary for rsvg and ImageMagick when compiled with rsvg support.
+ * Upstream bug is https://bugzilla.gnome.org/show_bug.cgi?id=620923, fixed 2014-11-10, so
+ * this will be needed for a while. (T76852)
+ *
+ * @param string $svg SVG image data
+ * @return string Massaged SVG image data
+ */
+ protected function massageSvgPathdata( $svg ) {
+ $dom = new DomDocument;
+ $dom->loadXML( $svg );
+ foreach ( $dom->getElementsByTagName( 'path' ) as $node ) {
+ $pathData = $node->getAttribute( 'd' );
+ // Make sure there is at least one space between numbers, and that leading zero is not omitted.
+ // rsvg has issues with syntax like "M-1-2" and "M.445.483" and especially "M-.445-.483".
+ $pathData = preg_replace( '/(-?)(\d*\.\d+|\d+)/', ' ${1}0$2 ', $pathData );
+ // Strip unnecessary leading zeroes for prettiness, not strictly necessary
+ $pathData = preg_replace( '/([ -])0(\d)/', '$1$2', $pathData );
+ $node->setAttribute( 'd', $pathData );
+ }
+ return $dom->saveXML();
+ }
+
+ /**
+ * Convert passed image data, which is assumed to be SVG, to PNG.
+ *
+ * @param string $svg SVG image data
+ * @return string|bool PNG image data, or false on failure
+ */
+ protected function rasterize( $svg ) {
+ /**
+ * This code should be factored out to a separate method on SvgHandler, or perhaps a separate
+ * class, with a separate set of configuration settings.
+ *
+ * This is a distinct use case from regular SVG rasterization:
+ * * We can skip many sanity and security checks (as the images come from a trusted source,
+ * rather than from the user).
+ * * We need to provide extra options to some converters to achieve acceptable quality for very
+ * small images, which might cause performance issues in the general case.
+ * * We want to directly pass image data to the converter, rather than a file path.
+ *
+ * See https://phabricator.wikimedia.org/T76473#801446 for examples of what happens with the
+ * default settings.
+ *
+ * For now, we special-case rsvg (used in WMF production) and do a messy workaround for other
+ * converters.
+ */
+
+ global $wgSVGConverter, $wgSVGConverterPath;
+
+ $svg = $this->massageSvgPathdata( $svg );
+
+ // Sometimes this might be 'rsvg-secure'. Long as it's rsvg.
+ if ( strpos( $wgSVGConverter, 'rsvg' ) === 0 ) {
+ $command = 'rsvg-convert';
+ if ( $wgSVGConverterPath ) {
+ $command = wfEscapeShellArg( "$wgSVGConverterPath/" ) . $command;
+ }
+
+ $process = proc_open(
+ $command,
+ [ 0 => [ 'pipe', 'r' ], 1 => [ 'pipe', 'w' ] ],
+ $pipes
+ );
+
+ if ( is_resource( $process ) ) {
+ fwrite( $pipes[0], $svg );
+ fclose( $pipes[0] );
+ $png = stream_get_contents( $pipes[1] );
+ fclose( $pipes[1] );
+ proc_close( $process );
+
+ return $png ?: false;
+ }
+ return false;
+
+ } else {
+ // Write input to and read output from a temporary file
+ $tempFilenameSvg = tempnam( wfTempDir(), 'ResourceLoaderImage' );
+ $tempFilenamePng = tempnam( wfTempDir(), 'ResourceLoaderImage' );
+
+ file_put_contents( $tempFilenameSvg, $svg );
+
+ $metadata = SVGMetadataExtractor::getMetadata( $tempFilenameSvg );
+ if ( !isset( $metadata['width'] ) || !isset( $metadata['height'] ) ) {
+ unlink( $tempFilenameSvg );
+ return false;
+ }
+
+ $handler = new SvgHandler;
+ $res = $handler->rasterize(
+ $tempFilenameSvg,
+ $tempFilenamePng,
+ $metadata['width'],
+ $metadata['height']
+ );
+ unlink( $tempFilenameSvg );
+
+ $png = null;
+ if ( $res === true ) {
+ $png = file_get_contents( $tempFilenamePng );
+ unlink( $tempFilenamePng );
+ }
+
+ return $png ?: false;
+ }
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderImageModule.php b/www/wiki/includes/resourceloader/ResourceLoaderImageModule.php
new file mode 100644
index 00000000..5e329e84
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderImageModule.php
@@ -0,0 +1,471 @@
+<?php
+/**
+ * ResourceLoader module for generated and embedded images.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ */
+
+/**
+ * ResourceLoader module for generated and embedded images.
+ *
+ * @since 1.25
+ */
+class ResourceLoaderImageModule extends ResourceLoaderModule {
+
+ protected $definition = null;
+
+ /**
+ * Local base path, see __construct()
+ * @var string
+ */
+ protected $localBasePath = '';
+
+ protected $origin = self::ORIGIN_CORE_SITEWIDE;
+
+ protected $images = [];
+ protected $variants = [];
+ protected $prefix = null;
+ protected $selectorWithoutVariant = '.{prefix}-{name}';
+ protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * Constructs a new module from an options array.
+ *
+ * @param array $options List of options; if not given or empty, an empty module will be
+ * constructed
+ * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
+ * to $IP
+ *
+ * Below is a description for the $options array:
+ * @par Construction options:
+ * @code
+ * [
+ * // Base path to prepend to all local paths in $options. Defaults to $IP
+ * 'localBasePath' => [base path],
+ * // Path to JSON file that contains any of the settings below
+ * 'data' => [file path string]
+ * // CSS class prefix to use in all style rules
+ * 'prefix' => [CSS class prefix],
+ * // Alternatively: Format of CSS selector to use in all style rules
+ * 'selector' => [CSS selector template, variables: {prefix} {name} {variant}],
+ * // Alternatively: When using variants
+ * 'selectorWithoutVariant' => [CSS selector template, variables: {prefix} {name}],
+ * 'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}],
+ * // List of variants that may be used for the image files
+ * 'variants' => [
+ * // This level of nesting can be omitted if you use the same images for every skin
+ * [skin name (or 'default')] => [
+ * [variant name] => [
+ * 'color' => [color string, e.g. '#ffff00'],
+ * 'global' => [boolean, if true, this variant is available
+ * for all images of this type],
+ * ],
+ * ...
+ * ],
+ * ...
+ * ],
+ * // List of image files and their options
+ * 'images' => [
+ * // This level of nesting can be omitted if you use the same images for every skin
+ * [skin name (or 'default')] => [
+ * [icon name] => [
+ * 'file' => [file path string or array whose values are file path strings
+ * and whose keys are 'default', 'ltr', 'rtl', a single
+ * language code like 'en', or a list of language codes like
+ * 'en,de,ar'],
+ * 'variants' => [array of variant name strings, variants
+ * available for this image],
+ * ],
+ * ...
+ * ],
+ * ...
+ * ],
+ * ]
+ * @endcode
+ * @throws InvalidArgumentException
+ */
+ public function __construct( $options = [], $localBasePath = null ) {
+ $this->localBasePath = self::extractLocalBasePath( $options, $localBasePath );
+
+ $this->definition = $options;
+ }
+
+ /**
+ * Parse definition and external JSON data, if referenced.
+ */
+ protected function loadFromDefinition() {
+ if ( $this->definition === null ) {
+ return;
+ }
+
+ $options = $this->definition;
+ $this->definition = null;
+
+ if ( isset( $options['data'] ) ) {
+ $dataPath = $this->localBasePath . '/' . $options['data'];
+ $data = json_decode( file_get_contents( $dataPath ), true );
+ $options = array_merge( $data, $options );
+ }
+
+ // Accepted combinations:
+ // * prefix
+ // * selector
+ // * selectorWithoutVariant + selectorWithVariant
+ // * prefix + selector
+ // * prefix + selectorWithoutVariant + selectorWithVariant
+
+ $prefix = isset( $options['prefix'] ) && $options['prefix'];
+ $selector = isset( $options['selector'] ) && $options['selector'];
+ $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
+ && $options['selectorWithoutVariant'];
+ $selectorWithVariant = isset( $options['selectorWithVariant'] )
+ && $options['selectorWithVariant'];
+
+ if ( $selectorWithoutVariant && !$selectorWithVariant ) {
+ throw new InvalidArgumentException(
+ "Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
+ );
+ }
+ if ( $selectorWithVariant && !$selectorWithoutVariant ) {
+ throw new InvalidArgumentException(
+ "Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
+ );
+ }
+ if ( $selector && $selectorWithVariant ) {
+ throw new InvalidArgumentException(
+ "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
+ );
+ }
+ if ( !$prefix && !$selector && !$selectorWithVariant ) {
+ throw new InvalidArgumentException(
+ "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
+ );
+ }
+
+ foreach ( $options as $member => $option ) {
+ switch ( $member ) {
+ case 'images':
+ case 'variants':
+ if ( !is_array( $option ) ) {
+ throw new InvalidArgumentException(
+ "Invalid list error. '$option' given, array expected."
+ );
+ }
+ if ( !isset( $option['default'] ) ) {
+ // Backwards compatibility
+ $option = [ 'default' => $option ];
+ }
+ foreach ( $option as $skin => $data ) {
+ if ( !is_array( $option ) ) {
+ throw new InvalidArgumentException(
+ "Invalid list error. '$option' given, array expected."
+ );
+ }
+ }
+ $this->{$member} = $option;
+ break;
+
+ case 'prefix':
+ case 'selectorWithoutVariant':
+ case 'selectorWithVariant':
+ $this->{$member} = (string)$option;
+ break;
+
+ case 'selector':
+ $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
+ }
+ }
+ }
+
+ /**
+ * Get CSS class prefix used by this module.
+ * @return string
+ */
+ public function getPrefix() {
+ $this->loadFromDefinition();
+ return $this->prefix;
+ }
+
+ /**
+ * Get CSS selector templates used by this module.
+ * @return string
+ */
+ public function getSelectors() {
+ $this->loadFromDefinition();
+ return [
+ 'selectorWithoutVariant' => $this->selectorWithoutVariant,
+ 'selectorWithVariant' => $this->selectorWithVariant,
+ ];
+ }
+
+ /**
+ * Get a ResourceLoaderImage object for given image.
+ * @param string $name Image name
+ * @param ResourceLoaderContext $context
+ * @return ResourceLoaderImage|null
+ */
+ public function getImage( $name, ResourceLoaderContext $context ) {
+ $this->loadFromDefinition();
+ $images = $this->getImages( $context );
+ return isset( $images[$name] ) ? $images[$name] : null;
+ }
+
+ /**
+ * Get ResourceLoaderImage objects for all images.
+ * @param ResourceLoaderContext $context
+ * @return ResourceLoaderImage[] Array keyed by image name
+ */
+ public function getImages( ResourceLoaderContext $context ) {
+ $skin = $context->getSkin();
+ if ( !isset( $this->imageObjects ) ) {
+ $this->loadFromDefinition();
+ $this->imageObjects = [];
+ }
+ if ( !isset( $this->imageObjects[$skin] ) ) {
+ $this->imageObjects[$skin] = [];
+ if ( !isset( $this->images[$skin] ) ) {
+ $this->images[$skin] = isset( $this->images['default'] ) ?
+ $this->images['default'] :
+ [];
+ }
+ foreach ( $this->images[$skin] as $name => $options ) {
+ $fileDescriptor = is_string( $options ) ? $options : $options['file'];
+
+ $allowedVariants = array_merge(
+ ( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
+ $this->getGlobalVariants( $context )
+ );
+ if ( isset( $this->variants[$skin] ) ) {
+ $variantConfig = array_intersect_key(
+ $this->variants[$skin],
+ array_fill_keys( $allowedVariants, true )
+ );
+ } else {
+ $variantConfig = [];
+ }
+
+ $image = new ResourceLoaderImage(
+ $name,
+ $this->getName(),
+ $fileDescriptor,
+ $this->localBasePath,
+ $variantConfig
+ );
+ $this->imageObjects[$skin][$image->getName()] = $image;
+ }
+ }
+
+ return $this->imageObjects[$skin];
+ }
+
+ /**
+ * Get list of variants in this module that are 'global', i.e., available
+ * for every image regardless of image options.
+ * @param ResourceLoaderContext $context
+ * @return string[]
+ */
+ public function getGlobalVariants( ResourceLoaderContext $context ) {
+ $skin = $context->getSkin();
+ if ( !isset( $this->globalVariants ) ) {
+ $this->loadFromDefinition();
+ $this->globalVariants = [];
+ }
+ if ( !isset( $this->globalVariants[$skin] ) ) {
+ $this->globalVariants[$skin] = [];
+ if ( !isset( $this->variants[$skin] ) ) {
+ $this->variants[$skin] = isset( $this->variants['default'] ) ?
+ $this->variants['default'] :
+ [];
+ }
+ foreach ( $this->variants[$skin] as $name => $config ) {
+ if ( isset( $config['global'] ) && $config['global'] ) {
+ $this->globalVariants[$skin][] = $name;
+ }
+ }
+ }
+
+ return $this->globalVariants[$skin];
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getStyles( ResourceLoaderContext $context ) {
+ $this->loadFromDefinition();
+
+ // Build CSS rules
+ $rules = [];
+ $script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
+ $selectors = $this->getSelectors();
+
+ foreach ( $this->getImages( $context ) as $name => $image ) {
+ $declarations = $this->getStyleDeclarations( $context, $image, $script );
+ $selector = strtr(
+ $selectors['selectorWithoutVariant'],
+ [
+ '{prefix}' => $this->getPrefix(),
+ '{name}' => $name,
+ '{variant}' => '',
+ ]
+ );
+ $rules[] = "$selector {\n\t$declarations\n}";
+
+ foreach ( $image->getVariants() as $variant ) {
+ $declarations = $this->getStyleDeclarations( $context, $image, $script, $variant );
+ $selector = strtr(
+ $selectors['selectorWithVariant'],
+ [
+ '{prefix}' => $this->getPrefix(),
+ '{name}' => $name,
+ '{variant}' => $variant,
+ ]
+ );
+ $rules[] = "$selector {\n\t$declarations\n}";
+ }
+ }
+
+ $style = implode( "\n", $rules );
+ return [ 'all' => $style ];
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @param ResourceLoaderImage $image Image to get the style for
+ * @param string $script URL to load.php
+ * @param string|null $variant Variant to get the style for
+ * @return string
+ */
+ private function getStyleDeclarations(
+ ResourceLoaderContext $context,
+ ResourceLoaderImage $image,
+ $script,
+ $variant = null
+ ) {
+ $imageDataUri = $image->getDataUri( $context, $variant, 'original' );
+ $primaryUrl = $imageDataUri ?: $image->getUrl( $context, $script, $variant, 'original' );
+ $declarations = $this->getCssDeclarations(
+ $primaryUrl,
+ $image->getUrl( $context, $script, $variant, 'rasterized' )
+ );
+ return implode( "\n\t", $declarations );
+ }
+
+ /**
+ * SVG support using a transparent gradient to guarantee cross-browser
+ * compatibility (browsers able to understand gradient syntax support also SVG).
+ * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
+ *
+ * Keep synchronized with the .background-image-svg LESS mixin in
+ * /resources/src/mediawiki.less/mediawiki.mixins.less.
+ *
+ * @param string $primary Primary URI
+ * @param string $fallback Fallback URI
+ * @return string[] CSS declarations to use given URIs as background-image
+ */
+ protected function getCssDeclarations( $primary, $fallback ) {
+ $primaryUrl = CSSMin::buildUrlValue( $primary );
+ $fallbackUrl = CSSMin::buildUrlValue( $fallback );
+ return [
+ "background-image: $fallbackUrl;",
+ "background-image: linear-gradient(transparent, transparent), $primaryUrl;",
+ ];
+ }
+
+ /**
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return false;
+ }
+
+ /**
+ * Get the definition summary for this module.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getDefinitionSummary( ResourceLoaderContext $context ) {
+ $this->loadFromDefinition();
+ $summary = parent::getDefinitionSummary( $context );
+
+ $options = [];
+ foreach ( [
+ 'localBasePath',
+ 'images',
+ 'variants',
+ 'prefix',
+ 'selectorWithoutVariant',
+ 'selectorWithVariant',
+ ] as $member ) {
+ $options[$member] = $this->{$member};
+ };
+
+ $summary[] = [
+ 'options' => $options,
+ 'fileHashes' => $this->getFileHashes( $context ),
+ ];
+ return $summary;
+ }
+
+ /**
+ * Helper method for getDefinitionSummary.
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ protected function getFileHashes( ResourceLoaderContext $context ) {
+ $this->loadFromDefinition();
+ $files = [];
+ foreach ( $this->getImages( $context ) as $name => $image ) {
+ $files[] = $image->getPath( $context );
+ }
+ $files = array_values( array_unique( $files ) );
+ return array_map( [ __CLASS__, 'safeFileHash' ], $files );
+ }
+
+ /**
+ * Extract a local base path from module definition information.
+ *
+ * @param array $options Module definition
+ * @param string $localBasePath Path to use if not provided in module definition. Defaults
+ * to $IP
+ * @return string Local base path
+ */
+ public static function extractLocalBasePath( $options, $localBasePath = null ) {
+ global $IP;
+
+ if ( $localBasePath === null ) {
+ $localBasePath = $IP;
+ }
+
+ if ( array_key_exists( 'localBasePath', $options ) ) {
+ $localBasePath = (string)$options['localBasePath'];
+ }
+
+ return $localBasePath;
+ }
+
+ /**
+ * @return string
+ */
+ public function getType() {
+ return self::LOAD_STYLES;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderJqueryMsgModule.php b/www/wiki/includes/resourceloader/ResourceLoaderJqueryMsgModule.php
new file mode 100644
index 00000000..bef34f99
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderJqueryMsgModule.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * ResourceLoader module for mediawiki.jqueryMsg that provides generated data.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for mediawiki.jqueryMsg and its generated data
+ */
+class ResourceLoaderJqueryMsgModule extends ResourceLoaderFileModule {
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ $fileScript = parent::getScript( $context );
+
+ $tagData = Sanitizer::getRecognizedTagData();
+ $parserDefaults = [];
+ $parserDefaults['allowedHtmlElements'] = array_merge(
+ array_keys( $tagData['htmlpairs'] ),
+ array_diff(
+ array_keys( $tagData['htmlsingle'] ),
+ array_keys( $tagData['htmlsingleonly'] )
+ )
+ );
+
+ $mainDataScript = Xml::encodeJsCall( 'mw.jqueryMsg.setParserDefaults', [ $parserDefaults ] );
+
+ // Associative array mapping magic words (e.g. SITENAME)
+ // to their values.
+ $magicWords = [
+ 'SITENAME' => $this->getConfig()->get( 'Sitename' ),
+ ];
+
+ Hooks::run( 'ResourceLoaderJqueryMsgModuleMagicWords', [ $context, &$magicWords ] );
+
+ $magicWordExtendData = [
+ 'magic' => $magicWords,
+ ];
+
+ $magicWordDataScript = Xml::encodeJsCall( 'mw.jqueryMsg.setParserDefaults', [
+ $magicWordExtendData,
+ /* deep= */ true
+ ] );
+
+ return $fileScript . $mainDataScript . $magicWordDataScript;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
+ // Bypass file module urls
+ return ResourceLoaderModule::getScriptURLsForDebug( $context );
+ }
+
+ /**
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php
new file mode 100644
index 00000000..e78484a2
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderLanguageDataModule.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * ResourceLoader module for populating language specific data.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Santhosh Thottingal
+ * @author Timo Tijhof
+ */
+
+/**
+ * ResourceLoader module for populating language specific data.
+ */
+class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
+
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * Get all the dynamic data for the content language to an array.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ protected function getData( ResourceLoaderContext $context ) {
+ $language = Language::factory( $context->getLanguage() );
+ return [
+ 'digitTransformTable' => $language->digitTransformTable(),
+ 'separatorTransformTable' => $language->separatorTransformTable(),
+ 'minimumGroupingDigits' => $language->minimumGroupingDigits(),
+ 'grammarForms' => $language->getGrammarForms(),
+ 'grammarTransformations' => $language->getGrammarTransformations(),
+ 'pluralRules' => $language->getPluralRules(),
+ 'digitGroupingPattern' => $language->digitGroupingPattern(),
+ 'fallbackLanguages' => $language->getFallbackLanguages(),
+ ];
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ return Xml::encodeJsCall(
+ 'mw.language.setData',
+ [
+ $context->getLanguage(),
+ $this->getData( $context )
+ ],
+ ResourceLoader::inDebugMode()
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ return [ 'mediawiki.language.init' ];
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/www/wiki/includes/resourceloader/ResourceLoaderLanguageNamesModule.php
new file mode 100644
index 00000000..57260ba5
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderLanguageNamesModule.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * ResourceLoader module for providing language names.
+ *
+ * By default these names will be autonyms however other extensions may
+ * provided language names in the context language (e.g. cldr extension)
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Ed Sanders
+ * @author Trevor Parscal
+ */
+
+/**
+ * ResourceLoader module for populating language specific data.
+ */
+class ResourceLoaderLanguageNamesModule extends ResourceLoaderModule {
+
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ protected function getData( ResourceLoaderContext $context ) {
+ return Language::fetchLanguageNames(
+ $context->getLanguage(),
+ 'all'
+ );
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ return Xml::encodeJsCall(
+ 'mw.language.setData',
+ [
+ $context->getLanguage(),
+ 'languageNames',
+ $this->getData( $context )
+ ],
+ ResourceLoader::inDebugMode()
+ );
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ return [ 'mediawiki.language.init' ];
+ }
+
+ /**
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php b/www/wiki/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php
new file mode 100644
index 00000000..d16a4ff7
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderMediaWikiUtilModule.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * ResourceLoader mediawiki.util module
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for mediawiki.util
+ *
+ * @since 1.30
+ */
+class ResourceLoaderMediaWikiUtilModule extends ResourceLoaderFileModule {
+ /**
+ * @inheritDoc
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ return ResourceLoader::makeConfigSetScript(
+ [ 'wgFragmentMode' => $this->getConfig()->get( 'FragmentMode' ) ]
+ )
+ . "\n"
+ . parent::getScript( $context );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function supportsURLLoading() {
+ return false;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderModule.php b/www/wiki/includes/resourceloader/ResourceLoaderModule.php
new file mode 100644
index 00000000..2abc17c2
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderModule.php
@@ -0,0 +1,1044 @@
+<?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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+use MediaWiki\MediaWikiServices;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Wikimedia\RelPath;
+use Wikimedia\ScopedCallback;
+
+/**
+ * Abstraction for ResourceLoader modules, with name registration and maxage functionality.
+ */
+abstract class ResourceLoaderModule implements LoggerAwareInterface {
+ # Type of resource
+ const TYPE_SCRIPTS = 'scripts';
+ const TYPE_STYLES = 'styles';
+ const TYPE_COMBINED = 'combined';
+
+ # Desired load type
+ // Module only has styles (loaded via <style> or <link rel=stylesheet>)
+ const LOAD_STYLES = 'styles';
+ // Module may have other resources (loaded via mw.loader from a script)
+ const LOAD_GENERAL = 'general';
+
+ # sitewide core module like a skin file or jQuery component
+ const ORIGIN_CORE_SITEWIDE = 1;
+
+ # per-user module generated by the software
+ const ORIGIN_CORE_INDIVIDUAL = 2;
+
+ # sitewide module generated from user-editable files, like MediaWiki:Common.js, or
+ # modules accessible to multiple users, such as those generated by the Gadgets extension.
+ const ORIGIN_USER_SITEWIDE = 3;
+
+ # per-user module generated from user-editable files, like User:Me/vector.js
+ const ORIGIN_USER_INDIVIDUAL = 4;
+
+ # an access constant; make sure this is kept as the largest number in this group
+ const ORIGIN_ALL = 10;
+
+ # script and style modules form a hierarchy of trustworthiness, with core modules like
+ # skins and jQuery as most trustworthy, and user scripts as least trustworthy. We can
+ # limit the types of scripts and styles we allow to load on, say, sensitive special
+ # pages like Special:UserLogin and Special:Preferences
+ protected $origin = self::ORIGIN_CORE_SITEWIDE;
+
+ protected $name = null;
+ protected $targets = [ 'desktop' ];
+
+ // In-object cache for file dependencies
+ protected $fileDeps = [];
+ // In-object cache for message blob (keyed by language)
+ protected $msgBlobs = [];
+ // In-object cache for version hash
+ protected $versionHash = [];
+ // In-object cache for module content
+ protected $contents = [];
+
+ /**
+ * @var Config
+ */
+ protected $config;
+
+ /**
+ * @var array|bool
+ */
+ protected $deprecated = false;
+
+ /**
+ * @var LoggerInterface
+ */
+ protected $logger;
+
+ /**
+ * Get this module's name. This is set when the module is registered
+ * with ResourceLoader::register()
+ *
+ * @return string|null Name (string) or null if no name was set
+ */
+ public function getName() {
+ return $this->name;
+ }
+
+ /**
+ * Set this module's name. This is called by ResourceLoader::register()
+ * when registering the module. Other code should not call this.
+ *
+ * @param string $name
+ */
+ public function setName( $name ) {
+ $this->name = $name;
+ }
+
+ /**
+ * Get this module's origin. This is set when the module is registered
+ * with ResourceLoader::register()
+ *
+ * @return int ResourceLoaderModule class constant, the subclass default
+ * if not set manually
+ */
+ public function getOrigin() {
+ return $this->origin;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function getFlip( $context ) {
+ global $wgContLang;
+
+ return $wgContLang->getDir() !== $context->getDirection();
+ }
+
+ /**
+ * Get JS representing deprecation information for the current module if available
+ *
+ * @return string JavaScript code
+ */
+ protected function getDeprecationInformation() {
+ $deprecationInfo = $this->deprecated;
+ if ( $deprecationInfo ) {
+ $name = $this->getName();
+ $warning = 'This page is using the deprecated ResourceLoader module "' . $name . '".';
+ if ( is_string( $deprecationInfo ) ) {
+ $warning .= "\n" . $deprecationInfo;
+ }
+ return Xml::encodeJsCall(
+ 'mw.log.warn',
+ [ $warning ]
+ );
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Get all JS for this module for a given language and skin.
+ * Includes all relevant JS except loader scripts.
+ *
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ // Stub, override expected
+ return '';
+ }
+
+ /**
+ * Takes named templates by the module and returns an array mapping.
+ *
+ * @return array of templates mapping template alias to content
+ */
+ public function getTemplates() {
+ // Stub, override expected.
+ return [];
+ }
+
+ /**
+ * @return Config
+ * @since 1.24
+ */
+ public function getConfig() {
+ if ( $this->config === null ) {
+ // Ugh, fall back to default
+ $this->config = MediaWikiServices::getInstance()->getMainConfig();
+ }
+
+ return $this->config;
+ }
+
+ /**
+ * @param Config $config
+ * @since 1.24
+ */
+ public function setConfig( Config $config ) {
+ $this->config = $config;
+ }
+
+ /**
+ * @since 1.27
+ * @param LoggerInterface $logger
+ * @return null
+ */
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @since 1.27
+ * @return LoggerInterface
+ */
+ protected function getLogger() {
+ if ( !$this->logger ) {
+ $this->logger = new NullLogger();
+ }
+ return $this->logger;
+ }
+
+ /**
+ * Get the URL or URLs to load for this module's JS in debug mode.
+ * The default behavior is to return a load.php?only=scripts URL for
+ * the module, but file-based modules will want to override this to
+ * load the files directly.
+ *
+ * This function is called only when 1) we're in debug mode, 2) there
+ * is no only= parameter and 3) supportsURLLoading() returns true.
+ * #2 is important to prevent an infinite loop, therefore this function
+ * MUST return either an only= URL or a non-load.php URL.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array Array of URLs
+ */
+ public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
+ $resourceLoader = $context->getResourceLoader();
+ $derivative = new DerivativeResourceLoaderContext( $context );
+ $derivative->setModules( [ $this->getName() ] );
+ $derivative->setOnly( 'scripts' );
+ $derivative->setDebug( true );
+
+ $url = $resourceLoader->createLoaderURL(
+ $this->getSource(),
+ $derivative
+ );
+
+ return [ $url ];
+ }
+
+ /**
+ * Whether this module supports URL loading. If this function returns false,
+ * getScript() will be used even in cases (debug mode, no only param) where
+ * getScriptURLsForDebug() would normally be used instead.
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return true;
+ }
+
+ /**
+ * Get all CSS for this module for a given skin.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array List of CSS strings or array of CSS strings keyed by media type.
+ * like [ 'screen' => '.foo { width: 0 }' ];
+ * or [ 'screen' => [ '.foo { width: 0 }' ] ];
+ */
+ public function getStyles( ResourceLoaderContext $context ) {
+ // Stub, override expected
+ return [];
+ }
+
+ /**
+ * Get the URL or URLs to load for this module's CSS in debug mode.
+ * The default behavior is to return a load.php?only=styles URL for
+ * the module, but file-based modules will want to override this to
+ * load the files directly. See also getScriptURLsForDebug()
+ *
+ * @param ResourceLoaderContext $context
+ * @return array [ mediaType => [ URL1, URL2, ... ], ... ]
+ */
+ public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
+ $resourceLoader = $context->getResourceLoader();
+ $derivative = new DerivativeResourceLoaderContext( $context );
+ $derivative->setModules( [ $this->getName() ] );
+ $derivative->setOnly( 'styles' );
+ $derivative->setDebug( true );
+
+ $url = $resourceLoader->createLoaderURL(
+ $this->getSource(),
+ $derivative
+ );
+
+ return [ 'all' => [ $url ] ];
+ }
+
+ /**
+ * Get the messages needed for this module.
+ *
+ * To get a JSON blob with messages, use MessageBlobStore::get()
+ *
+ * @return array List of message keys. Keys may occur more than once
+ */
+ public function getMessages() {
+ // Stub, override expected
+ return [];
+ }
+
+ /**
+ * Get the group this module is in.
+ *
+ * @return string Group name
+ */
+ public function getGroup() {
+ // Stub, override expected
+ return null;
+ }
+
+ /**
+ * Get the origin of this module. Should only be overridden for foreign modules.
+ *
+ * @return string Origin name, 'local' for local modules
+ */
+ public function getSource() {
+ // Stub, override expected
+ return 'local';
+ }
+
+ /**
+ * Whether this module's JS expects to work without the client-side ResourceLoader module.
+ * Returning true from this function will prevent mw.loader.state() call from being
+ * appended to the bottom of the script.
+ *
+ * @return bool
+ */
+ public function isRaw() {
+ return false;
+ }
+
+ /**
+ * Get a list of modules this module depends on.
+ *
+ * Dependency information is taken into account when loading a module
+ * on the client side.
+ *
+ * Note: It is expected that $context will be made non-optional in the near
+ * future.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array List of module names as strings
+ */
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ // Stub, override expected
+ return [];
+ }
+
+ /**
+ * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
+ *
+ * @return array Array of strings
+ */
+ public function getTargets() {
+ return $this->targets;
+ }
+
+ /**
+ * Get the module's load type.
+ *
+ * @since 1.28
+ * @return string ResourceLoaderModule LOAD_* constant
+ */
+ public function getType() {
+ return self::LOAD_GENERAL;
+ }
+
+ /**
+ * Get the skip function.
+ *
+ * Modules that provide fallback functionality can provide a "skip function". This
+ * function, if provided, will be passed along to the module registry on the client.
+ * When this module is loaded (either directly or as a dependency of another module),
+ * then this function is executed first. If the function returns true, the module will
+ * instantly be considered "ready" without requesting the associated module resources.
+ *
+ * The value returned here must be valid javascript for execution in a private function.
+ * It must not contain the "function () {" and "}" wrapper though.
+ *
+ * @return string|null A JavaScript function body returning a boolean value, or null
+ */
+ public function getSkipFunction() {
+ return null;
+ }
+
+ /**
+ * Get the files this module depends on indirectly for a given skin.
+ *
+ * These are only image files referenced by the module's stylesheet.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array List of files
+ */
+ protected function getFileDependencies( ResourceLoaderContext $context ) {
+ $vary = $context->getSkin() . '|' . $context->getLanguage();
+
+ // Try in-object cache first
+ if ( !isset( $this->fileDeps[$vary] ) ) {
+ $dbr = wfGetDB( DB_REPLICA );
+ $deps = $dbr->selectField( 'module_deps',
+ 'md_deps',
+ [
+ 'md_module' => $this->getName(),
+ 'md_skin' => $vary,
+ ],
+ __METHOD__
+ );
+
+ if ( !is_null( $deps ) ) {
+ $this->fileDeps[$vary] = self::expandRelativePaths(
+ (array)FormatJson::decode( $deps, true )
+ );
+ } else {
+ $this->fileDeps[$vary] = [];
+ }
+ }
+ return $this->fileDeps[$vary];
+ }
+
+ /**
+ * Set in-object cache for file dependencies.
+ *
+ * This is used to retrieve data in batches. See ResourceLoader::preloadModuleInfo().
+ * To save the data, use saveFileDependencies().
+ *
+ * @param ResourceLoaderContext $context
+ * @param string[] $files Array of file names
+ */
+ public function setFileDependencies( ResourceLoaderContext $context, $files ) {
+ $vary = $context->getSkin() . '|' . $context->getLanguage();
+ $this->fileDeps[$vary] = $files;
+ }
+
+ /**
+ * Set the files this module depends on indirectly for a given skin.
+ *
+ * @since 1.27
+ * @param ResourceLoaderContext $context
+ * @param array $localFileRefs List of files
+ */
+ protected function saveFileDependencies( ResourceLoaderContext $context, $localFileRefs ) {
+ try {
+ // Related bugs and performance considerations:
+ // 1. Don't needlessly change the database value with the same list in a
+ // different order or with duplicates.
+ // 2. Use relative paths to avoid ghost entries when $IP changes. (T111481)
+ // 3. Don't needlessly replace the database with the same value
+ // just because $IP changed (e.g. when upgrading a wiki).
+ // 4. Don't create an endless replace loop on every request for this
+ // module when '../' is used anywhere. Even though both are expanded
+ // (one expanded by getFileDependencies from the DB, the other is
+ // still raw as originally read by RL), the latter has not
+ // been normalized yet.
+
+ // Normalise
+ $localFileRefs = array_values( array_unique( $localFileRefs ) );
+ sort( $localFileRefs );
+ $localPaths = self::getRelativePaths( $localFileRefs );
+
+ $storedPaths = self::getRelativePaths( $this->getFileDependencies( $context ) );
+ // If the list has been modified since last time we cached it, update the cache
+ if ( $localPaths !== $storedPaths ) {
+ $vary = $context->getSkin() . '|' . $context->getLanguage();
+ $cache = ObjectCache::getLocalClusterInstance();
+ $key = $cache->makeKey( __METHOD__, $this->getName(), $vary );
+ $scopeLock = $cache->getScopedLock( $key, 0 );
+ if ( !$scopeLock ) {
+ return; // T124649; avoid write slams
+ }
+
+ $deps = FormatJson::encode( $localPaths );
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->upsert( 'module_deps',
+ [
+ 'md_module' => $this->getName(),
+ 'md_skin' => $vary,
+ 'md_deps' => $deps,
+ ],
+ [ [ 'md_module', 'md_skin' ] ],
+ [
+ 'md_deps' => $deps,
+ ]
+ );
+
+ if ( $dbw->trxLevel() ) {
+ $dbw->onTransactionResolution(
+ function () use ( &$scopeLock ) {
+ ScopedCallback::consume( $scopeLock ); // release after commit
+ },
+ __METHOD__
+ );
+ }
+ }
+ } catch ( Exception $e ) {
+ wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" );
+ }
+ }
+
+ /**
+ * Make file paths relative to MediaWiki directory.
+ *
+ * This is used to make file paths safe for storing in a database without the paths
+ * becoming stale or incorrect when MediaWiki is moved or upgraded (T111481).
+ *
+ * @since 1.27
+ * @param array $filePaths
+ * @return array
+ */
+ public static function getRelativePaths( array $filePaths ) {
+ global $IP;
+ return array_map( function ( $path ) use ( $IP ) {
+ return RelPath::getRelativePath( $path, $IP );
+ }, $filePaths );
+ }
+
+ /**
+ * Expand directories relative to $IP.
+ *
+ * @since 1.27
+ * @param array $filePaths
+ * @return array
+ */
+ public static function expandRelativePaths( array $filePaths ) {
+ global $IP;
+ return array_map( function ( $path ) use ( $IP ) {
+ return RelPath::joinPath( $IP, $path );
+ }, $filePaths );
+ }
+
+ /**
+ * Get the hash of the message blob.
+ *
+ * @since 1.27
+ * @param ResourceLoaderContext $context
+ * @return string|null JSON blob or null if module has no messages
+ */
+ protected function getMessageBlob( ResourceLoaderContext $context ) {
+ if ( !$this->getMessages() ) {
+ // Don't bother consulting MessageBlobStore
+ return null;
+ }
+ // Message blobs may only vary language, not by context keys
+ $lang = $context->getLanguage();
+ if ( !isset( $this->msgBlobs[$lang] ) ) {
+ $this->getLogger()->warning( 'Message blob for {module} should have been preloaded', [
+ 'module' => $this->getName(),
+ ] );
+ $store = $context->getResourceLoader()->getMessageBlobStore();
+ $this->msgBlobs[$lang] = $store->getBlob( $this, $lang );
+ }
+ return $this->msgBlobs[$lang];
+ }
+
+ /**
+ * Set in-object cache for message blobs.
+ *
+ * Used to allow fetching of message blobs in batches. See ResourceLoader::preloadModuleInfo().
+ *
+ * @since 1.27
+ * @param string|null $blob JSON blob or null
+ * @param string $lang Language code
+ */
+ public function setMessageBlob( $blob, $lang ) {
+ $this->msgBlobs[$lang] = $blob;
+ }
+
+ /**
+ * Get headers to send as part of a module web response.
+ *
+ * It is not supported to send headers through this method that are
+ * required to be unique or otherwise sent once in an HTTP response
+ * because clients may make batch requests for multiple modules (as
+ * is the default behaviour for ResourceLoader clients).
+ *
+ * For exclusive or aggregated headers, see ResourceLoader::sendResponseHeaders().
+ *
+ * @since 1.30
+ * @param ResourceLoaderContext $context
+ * @return string[] Array of HTTP response headers
+ */
+ final public function getHeaders( ResourceLoaderContext $context ) {
+ $headers = [];
+
+ $formattedLinks = [];
+ foreach ( $this->getPreloadLinks( $context ) as $url => $attribs ) {
+ $link = "<{$url}>;rel=preload";
+ foreach ( $attribs as $key => $val ) {
+ $link .= ";{$key}={$val}";
+ }
+ $formattedLinks[] = $link;
+ }
+ if ( $formattedLinks ) {
+ $headers[] = 'Link: ' . implode( ',', $formattedLinks );
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Get a list of resources that web browsers may preload.
+ *
+ * Behaviour of rel=preload link is specified at <https://www.w3.org/TR/preload/>.
+ *
+ * Use case for ResourceLoader originally part of T164299.
+ *
+ * @par Example
+ * @code
+ * protected function getPreloadLinks() {
+ * return [
+ * 'https://example.org/script.js' => [ 'as' => 'script' ],
+ * 'https://example.org/image.png' => [ 'as' => 'image' ],
+ * ];
+ * }
+ * @endcode
+ *
+ * @par Example using HiDPI image variants
+ * @code
+ * protected function getPreloadLinks() {
+ * return [
+ * 'https://example.org/logo.png' => [
+ * 'as' => 'image',
+ * 'media' => 'not all and (min-resolution: 2dppx)',
+ * ],
+ * 'https://example.org/logo@2x.png' => [
+ * 'as' => 'image',
+ * 'media' => '(min-resolution: 2dppx)',
+ * ],
+ * ];
+ * }
+ * @endcode
+ *
+ * @see ResourceLoaderModule::getHeaders
+ * @since 1.30
+ * @param ResourceLoaderContext $context
+ * @return array Keyed by url, values must be an array containing
+ * at least an 'as' key. Optionally a 'media' key as well.
+ */
+ protected function getPreloadLinks( ResourceLoaderContext $context ) {
+ return [];
+ }
+
+ /**
+ * Get module-specific LESS variables, if any.
+ *
+ * @since 1.27
+ * @param ResourceLoaderContext $context
+ * @return array Module-specific LESS variables.
+ */
+ protected function getLessVars( ResourceLoaderContext $context ) {
+ return [];
+ }
+
+ /**
+ * Get an array of this module's resources. Ready for serving to the web.
+ *
+ * @since 1.26
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getModuleContent( ResourceLoaderContext $context ) {
+ $contextHash = $context->getHash();
+ // Cache this expensive operation. This calls builds the scripts, styles, and messages
+ // content which typically involves filesystem and/or database access.
+ if ( !array_key_exists( $contextHash, $this->contents ) ) {
+ $this->contents[$contextHash] = $this->buildContent( $context );
+ }
+ return $this->contents[$contextHash];
+ }
+
+ /**
+ * Bundle all resources attached to this module into an array.
+ *
+ * @since 1.26
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ final protected function buildContent( ResourceLoaderContext $context ) {
+ $rl = $context->getResourceLoader();
+ $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $statStart = microtime( true );
+
+ // Only include properties that are relevant to this context (e.g. only=scripts)
+ // and that are non-empty (e.g. don't include "templates" for modules without
+ // templates). This helps prevent invalidating cache for all modules when new
+ // optional properties are introduced.
+ $content = [];
+
+ // Scripts
+ if ( $context->shouldIncludeScripts() ) {
+ // If we are in debug mode, we'll want to return an array of URLs if possible
+ // However, we can't do this if the module doesn't support it
+ // We also can't do this if there is an only= parameter, because we have to give
+ // the module a way to return a load.php URL without causing an infinite loop
+ if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
+ $scripts = $this->getScriptURLsForDebug( $context );
+ } else {
+ $scripts = $this->getScript( $context );
+ // Make the script safe to concatenate by making sure there is at least one
+ // trailing new line at the end of the content. Previously, this looked for
+ // a semi-colon instead, but that breaks concatenation if the semicolon
+ // is inside a comment like "// foo();". Instead, simply use a
+ // line break as separator which matches JavaScript native logic for implicitly
+ // ending statements even if a semi-colon is missing.
+ // Bugs: T29054, T162719.
+ if ( is_string( $scripts )
+ && strlen( $scripts )
+ && substr( $scripts, -1 ) !== "\n"
+ ) {
+ $scripts .= "\n";
+ }
+ }
+ $content['scripts'] = $scripts;
+ }
+
+ // Styles
+ if ( $context->shouldIncludeStyles() ) {
+ $styles = [];
+ // Don't create empty stylesheets like [ '' => '' ] for modules
+ // that don't *have* any stylesheets (T40024).
+ $stylePairs = $this->getStyles( $context );
+ if ( count( $stylePairs ) ) {
+ // If we are in debug mode without &only= set, we'll want to return an array of URLs
+ // See comment near shouldIncludeScripts() for more details
+ if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
+ $styles = [
+ 'url' => $this->getStyleURLsForDebug( $context )
+ ];
+ } else {
+ // Minify CSS before embedding in mw.loader.implement call
+ // (unless in debug mode)
+ if ( !$context->getDebug() ) {
+ foreach ( $stylePairs as $media => $style ) {
+ // Can be either a string or an array of strings.
+ if ( is_array( $style ) ) {
+ $stylePairs[$media] = [];
+ foreach ( $style as $cssText ) {
+ if ( is_string( $cssText ) ) {
+ $stylePairs[$media][] =
+ ResourceLoader::filter( 'minify-css', $cssText );
+ }
+ }
+ } elseif ( is_string( $style ) ) {
+ $stylePairs[$media] = ResourceLoader::filter( 'minify-css', $style );
+ }
+ }
+ }
+ // Wrap styles into @media groups as needed and flatten into a numerical array
+ $styles = [
+ 'css' => $rl->makeCombinedStyles( $stylePairs )
+ ];
+ }
+ }
+ $content['styles'] = $styles;
+ }
+
+ // Messages
+ $blob = $this->getMessageBlob( $context );
+ if ( $blob ) {
+ $content['messagesBlob'] = $blob;
+ }
+
+ $templates = $this->getTemplates();
+ if ( $templates ) {
+ $content['templates'] = $templates;
+ }
+
+ $headers = $this->getHeaders( $context );
+ if ( $headers ) {
+ $content['headers'] = $headers;
+ }
+
+ $statTiming = microtime( true ) - $statStart;
+ $statName = strtr( $this->getName(), '.', '_' );
+ $stats->timing( "resourceloader_build.all", 1000 * $statTiming );
+ $stats->timing( "resourceloader_build.$statName", 1000 * $statTiming );
+
+ return $content;
+ }
+
+ /**
+ * Get a string identifying the current version of this module in a given context.
+ *
+ * Whenever anything happens that changes the module's response (e.g. scripts, styles, and
+ * messages) this value must change. This value is used to store module responses in cache.
+ * (Both client-side and server-side.)
+ *
+ * It is not recommended to override this directly. Use getDefinitionSummary() instead.
+ * If overridden, one must call the parent getVersionHash(), append data and re-hash.
+ *
+ * This method should be quick because it is frequently run by ResourceLoaderStartUpModule to
+ * propagate changes to the client and effectively invalidate cache.
+ *
+ * For backward-compatibility, the following optional data providers are automatically included:
+ *
+ * - getModifiedTime()
+ * - getModifiedHash()
+ *
+ * @since 1.26
+ * @param ResourceLoaderContext $context
+ * @return string Hash (should use ResourceLoader::makeHash)
+ */
+ public function getVersionHash( ResourceLoaderContext $context ) {
+ // The startup module produces a manifest with versions representing the entire module.
+ // Typically, the request for the startup module itself has only=scripts. That must apply
+ // only to the startup module content, and not to the module version computed here.
+ $context = new DerivativeResourceLoaderContext( $context );
+ $context->setModules( [] );
+ // Version hash must cover all resources, regardless of startup request itself.
+ $context->setOnly( null );
+ // Compute version hash based on content, not debug urls.
+ $context->setDebug( false );
+
+ // Cache this somewhat expensive operation. Especially because some classes
+ // (e.g. startup module) iterate more than once over all modules to get versions.
+ $contextHash = $context->getHash();
+ if ( !array_key_exists( $contextHash, $this->versionHash ) ) {
+ if ( $this->enableModuleContentVersion() ) {
+ // Detect changes directly
+ $str = json_encode( $this->getModuleContent( $context ) );
+ } else {
+ // Infer changes based on definition and other metrics
+ $summary = $this->getDefinitionSummary( $context );
+ if ( !isset( $summary['_cacheEpoch'] ) ) {
+ throw new LogicException( 'getDefinitionSummary must call parent method' );
+ }
+ $str = json_encode( $summary );
+
+ $mtime = $this->getModifiedTime( $context );
+ if ( $mtime !== null ) {
+ // Support: MediaWiki 1.25 and earlier
+ $str .= strval( $mtime );
+ }
+
+ $mhash = $this->getModifiedHash( $context );
+ if ( $mhash !== null ) {
+ // Support: MediaWiki 1.25 and earlier
+ $str .= strval( $mhash );
+ }
+ }
+
+ $this->versionHash[$contextHash] = ResourceLoader::makeHash( $str );
+ }
+ return $this->versionHash[$contextHash];
+ }
+
+ /**
+ * Whether to generate version hash based on module content.
+ *
+ * If a module requires database or file system access to build the module
+ * content, consider disabling this in favour of manually tracking relevant
+ * aspects in getDefinitionSummary(). See getVersionHash() for how this is used.
+ *
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return false;
+ }
+
+ /**
+ * Get the definition summary for this module.
+ *
+ * This is the method subclasses are recommended to use to track values in their
+ * version hash. Call this in getVersionHash() and pass it to e.g. json_encode.
+ *
+ * Subclasses must call the parent getDefinitionSummary() and build on that.
+ * It is recommended that each subclass appends its own new array. This prevents
+ * clashes or accidental overwrites of existing keys and gives each subclass
+ * its own scope for simple array keys.
+ *
+ * @code
+ * $summary = parent::getDefinitionSummary( $context );
+ * $summary[] = [
+ * 'foo' => 123,
+ * 'bar' => 'quux',
+ * ];
+ * return $summary;
+ * @endcode
+ *
+ * Return an array containing values from all significant properties of this
+ * module's definition.
+ *
+ * Be careful not to normalise too much. Especially preserve the order of things
+ * that carry significance in getScript and getStyles (T39812).
+ *
+ * Avoid including things that are insiginificant (e.g. order of message keys is
+ * insignificant and should be sorted to avoid unnecessary cache invalidation).
+ *
+ * This data structure must exclusively contain arrays and scalars as values (avoid
+ * object instances) to allow simple serialisation using json_encode.
+ *
+ * If modules have a hash or timestamp from another source, that may be incuded as-is.
+ *
+ * A number of utility methods are available to help you gather data. These are not
+ * called by default and must be included by the subclass' getDefinitionSummary().
+ *
+ * - getMessageBlob()
+ *
+ * @since 1.23
+ * @param ResourceLoaderContext $context
+ * @return array|null
+ */
+ public function getDefinitionSummary( ResourceLoaderContext $context ) {
+ return [
+ '_class' => static::class,
+ '_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ),
+ ];
+ }
+
+ /**
+ * Get this module's last modification timestamp for a given context.
+ *
+ * @deprecated since 1.26 Use getDefinitionSummary() instead
+ * @param ResourceLoaderContext $context
+ * @return int|null UNIX timestamp
+ */
+ public function getModifiedTime( ResourceLoaderContext $context ) {
+ return null;
+ }
+
+ /**
+ * Helper method for providing a version hash to getVersionHash().
+ *
+ * @deprecated since 1.26 Use getDefinitionSummary() instead
+ * @param ResourceLoaderContext $context
+ * @return string|null Hash
+ */
+ public function getModifiedHash( ResourceLoaderContext $context ) {
+ return null;
+ }
+
+ /**
+ * Check whether this module is known to be empty. If a child class
+ * has an easy and cheap way to determine that this module is
+ * definitely going to be empty, it should override this method to
+ * return true in that case. Callers may optimize the request for this
+ * module away if this function returns true.
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function isKnownEmpty( ResourceLoaderContext $context ) {
+ return false;
+ }
+
+ /**
+ * Check whether this module should be embeded rather than linked
+ *
+ * Modules returning true here will be embedded rather than loaded by
+ * ResourceLoaderClientHtml.
+ *
+ * @since 1.30
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function shouldEmbedModule( ResourceLoaderContext $context ) {
+ return $this->getGroup() === 'private';
+ }
+
+ /** @var JSParser Lazy-initialized; use self::javaScriptParser() */
+ private static $jsParser;
+ private static $parseCacheVersion = 1;
+
+ /**
+ * Validate a given script file; if valid returns the original source.
+ * If invalid, returns replacement JS source that throws an exception.
+ *
+ * @param string $fileName
+ * @param string $contents
+ * @return string JS with the original, or a replacement error
+ */
+ protected function validateScriptFile( $fileName, $contents ) {
+ if ( !$this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) {
+ return $contents;
+ }
+ $cache = ObjectCache::getMainWANInstance();
+ return $cache->getWithSetCallback(
+ $cache->makeGlobalKey(
+ 'resourceloader',
+ 'jsparse',
+ self::$parseCacheVersion,
+ md5( $contents ),
+ $fileName
+ ),
+ $cache::TTL_WEEK,
+ function () use ( $contents, $fileName ) {
+ $parser = self::javaScriptParser();
+ try {
+ $parser->parse( $contents, $fileName, 1 );
+ $result = $contents;
+ } catch ( Exception $e ) {
+ // We'll save this to cache to avoid having to re-validate broken JS
+ $err = $e->getMessage();
+ $result = "mw.log.error(" .
+ Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
+ }
+ return $result;
+ }
+ );
+ }
+
+ /**
+ * @return JSParser
+ */
+ protected static function javaScriptParser() {
+ if ( !self::$jsParser ) {
+ self::$jsParser = new JSParser();
+ }
+ return self::$jsParser;
+ }
+
+ /**
+ * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist.
+ * Defaults to 1.
+ *
+ * @param string $filePath File path
+ * @return int UNIX timestamp
+ */
+ protected static function safeFilemtime( $filePath ) {
+ Wikimedia\suppressWarnings();
+ $mtime = filemtime( $filePath ) ?: 1;
+ Wikimedia\restoreWarnings();
+ return $mtime;
+ }
+
+ /**
+ * Compute a non-cryptographic string hash of a file's contents.
+ * If the file does not exist or cannot be read, returns an empty string.
+ *
+ * @since 1.26 Uses MD4 instead of SHA1.
+ * @param string $filePath File path
+ * @return string Hash
+ */
+ protected static function safeFileHash( $filePath ) {
+ return FileContentsHasher::getFileContentsHash( $filePath );
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderOOUIFileModule.php b/www/wiki/includes/resourceloader/ResourceLoaderOOUIFileModule.php
new file mode 100644
index 00000000..e97e0742
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderOOUIFileModule.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoaderFileModule which magically loads the right skinScripts and skinStyles for every
+ * skin, using the specified OOUI theme for each.
+ *
+ * @since 1.30
+ */
+class ResourceLoaderOOUIFileModule extends ResourceLoaderFileModule {
+ use ResourceLoaderOOUIModule;
+
+ public function __construct( $options = [] ) {
+ if ( isset( $options[ 'themeScripts' ] ) ) {
+ $skinScripts = $this->getSkinSpecific( $options[ 'themeScripts' ], 'scripts' );
+ if ( !isset( $options['skinScripts'] ) ) {
+ $options['skinScripts'] = [];
+ }
+ $this->extendSkinSpecific( $options['skinScripts'], $skinScripts );
+ }
+ if ( isset( $options[ 'themeStyles' ] ) ) {
+ $skinStyles = $this->getSkinSpecific( $options[ 'themeStyles' ], 'styles' );
+ if ( !isset( $options['skinStyles'] ) ) {
+ $options['skinStyles'] = [];
+ }
+ $this->extendSkinSpecific( $options['skinStyles'], $skinStyles );
+ }
+
+ parent::__construct( $options );
+ }
+
+ /**
+ * Helper function to generate values for 'skinStyles' and 'skinScripts'.
+ *
+ * @param string $module Module to generate skinStyles/skinScripts for:
+ * 'core', 'widgets', 'toolbars', 'windows'
+ * @param string $which 'scripts' or 'styles'
+ * @return array
+ */
+ private function getSkinSpecific( $module, $which ) {
+ $themes = self::getSkinThemeMap();
+
+ return array_combine(
+ array_keys( $themes ),
+ array_map( function ( $theme ) use ( $module, $which ) {
+ if ( $which === 'scripts' ) {
+ return $this->getThemeScriptsPath( $theme, $module );
+ } else {
+ return $this->getThemeStylesPath( $theme, $module );
+ }
+ }, array_values( $themes ) )
+ );
+ }
+
+ /**
+ * Prepend the $extraSkinSpecific assoc. array to the $skinSpecific assoc. array.
+ * Both of them represent a 'skinScripts' or 'skinStyles' definition.
+ *
+ * @param array &$skinSpecific
+ * @param array $extraSkinSpecific
+ */
+ private function extendSkinSpecific( &$skinSpecific, $extraSkinSpecific ) {
+ // For each skin where skinStyles/skinScripts are defined, add our ones at the beginning
+ foreach ( $skinSpecific as $skin => $files ) {
+ if ( !is_array( $files ) ) {
+ $files = [ $files ];
+ }
+ if ( isset( $extraSkinSpecific[$skin] ) ) {
+ $skinSpecific[$skin] = array_merge( [ $extraSkinSpecific[$skin] ], $files );
+ } elseif ( isset( $extraSkinSpecific['default'] ) ) {
+ $skinSpecific[$skin] = array_merge( [ $extraSkinSpecific['default'] ], $files );
+ }
+ }
+ // Add our remaining skinStyles/skinScripts for skins that did not have them defined
+ foreach ( $extraSkinSpecific as $skin => $file ) {
+ if ( !isset( $skinSpecific[$skin] ) ) {
+ $skinSpecific[$skin] = $file;
+ }
+ }
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php b/www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php
new file mode 100644
index 00000000..5c9e1d94
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderOOUIImageModule.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Secret special sauce.
+ *
+ * @since 1.26
+ */
+class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
+ use ResourceLoaderOOUIModule;
+
+ protected function loadFromDefinition() {
+ if ( $this->definition === null ) {
+ // Do nothing if definition was already processed
+ return;
+ }
+
+ $themes = self::getSkinThemeMap();
+
+ // For backwards-compatibility, allow missing 'themeImages'
+ $module = isset( $this->definition['themeImages'] ) ? $this->definition['themeImages'] : '';
+
+ $definition = [];
+ foreach ( $themes as $skin => $theme ) {
+ // Find the path to the JSON file which contains the actual image definitions for this theme
+ if ( $module ) {
+ $dataPath = $this->getThemeImagesPath( $theme, $module );
+ } else {
+ // Backwards-compatibility for things that probably shouldn't have used this class...
+ $dataPath =
+ $this->definition['rootPath'] . '/' .
+ strtolower( $theme ) . '/' .
+ $this->definition['name'] . '.json';
+ }
+ $localDataPath = $this->localBasePath . '/' . $dataPath;
+
+ // If there's no file for this module of this theme, that's okay, it will just use the defaults
+ if ( !file_exists( $localDataPath ) ) {
+ continue;
+ }
+ $data = json_decode( file_get_contents( $localDataPath ), true );
+
+ // Expand the paths to images (since they are relative to the JSON file that defines them, not
+ // our base directory)
+ $fixPath = function ( &$path ) use ( $dataPath ) {
+ $path = dirname( $dataPath ) . '/' . $path;
+ };
+ array_walk( $data['images'], function ( &$value ) use ( $fixPath ) {
+ if ( is_string( $value['file'] ) ) {
+ $fixPath( $value['file'] );
+ } elseif ( is_array( $value['file'] ) ) {
+ array_walk_recursive( $value['file'], $fixPath );
+ }
+ } );
+
+ // Convert into a definition compatible with the parent vanilla ResourceLoaderImageModule
+ foreach ( $data as $key => $value ) {
+ switch ( $key ) {
+ // Images and color variants are defined per-theme, here converted to per-skin
+ case 'images':
+ case 'variants':
+ $definition[$key][$skin] = $data[$key];
+ break;
+
+ // Other options must be identical for each theme (or only defined in the default one)
+ default:
+ if ( !isset( $definition[$key] ) ) {
+ $definition[$key] = $data[$key];
+ } elseif ( $definition[$key] !== $data[$key] ) {
+ throw new Exception(
+ "Mismatched OOUI theme images definition: " .
+ "key '$key' of theme '$theme' for module '$module' " .
+ "does not match other themes"
+ );
+ }
+ break;
+ }
+ }
+ }
+
+ // Extra selectors to allow using the same icons for old-style MediaWiki UI code
+ if ( substr( $module, 0, 5 ) === 'icons' ) {
+ $definition['selectorWithoutVariant'] = '.oo-ui-icon-{name}, .mw-ui-icon-{name}:before';
+ $definition['selectorWithVariant'] = '.oo-ui-image-{variant}.oo-ui-icon-{name}, ' .
+ '.mw-ui-icon-{name}-{variant}:before';
+ }
+
+ // Fields from module definition silently override keys from JSON files
+ $this->definition += $definition;
+
+ parent::loadFromDefinition();
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderOOUIModule.php b/www/wiki/includes/resourceloader/ResourceLoaderOOUIModule.php
new file mode 100644
index 00000000..4228a45f
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderOOUIModule.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Convenience methods for dealing with OOUI themes and their relations to MW skins.
+ *
+ * @since 1.30
+ */
+trait ResourceLoaderOOUIModule {
+ protected static $knownScriptsModules = [ 'core' ];
+ protected static $knownStylesModules = [ 'core', 'widgets', 'toolbars', 'windows' ];
+ protected static $knownImagesModules = [
+ 'indicators', 'textures',
+ // Extra icons
+ 'icons-accessibility',
+ 'icons-alerts',
+ 'icons-content',
+ 'icons-editing-advanced',
+ 'icons-editing-core',
+ 'icons-editing-list',
+ 'icons-editing-styling',
+ 'icons-interactions',
+ 'icons-layout',
+ 'icons-location',
+ 'icons-media',
+ 'icons-moderation',
+ 'icons-movement',
+ 'icons-user',
+ 'icons-wikimedia',
+ ];
+
+ // Note that keys must be lowercase, values TitleCase.
+ protected static $builtinSkinThemeMap = [
+ 'default' => 'WikimediaUI',
+ ];
+
+ // Note that keys must be TitleCase.
+ protected static $builtinThemePaths = [
+ 'WikimediaUI' => [
+ 'scripts' => 'resources/lib/oojs-ui/oojs-ui-wikimediaui.js',
+ 'styles' => 'resources/lib/oojs-ui/oojs-ui-{module}-wikimediaui.css',
+ 'images' => 'resources/lib/oojs-ui/themes/wikimediaui/{module}.json',
+ ],
+ 'Apex' => [
+ 'scripts' => 'resources/lib/oojs-ui/oojs-ui-apex.js',
+ 'styles' => 'resources/lib/oojs-ui/oojs-ui-{module}-apex.css',
+ 'images' => 'resources/lib/oojs-ui/themes/apex/{module}.json',
+ ],
+ ];
+
+ /**
+ * Return a map of skin names (in lowercase) to OOUI theme names, defining which theme a given
+ * skin should use.
+ *
+ * @return array
+ */
+ public static function getSkinThemeMap() {
+ $themeMap = self::$builtinSkinThemeMap;
+ $themeMap += ExtensionRegistry::getInstance()->getAttribute( 'SkinOOUIThemes' );
+ return $themeMap;
+ }
+
+ /**
+ * Return a map of theme names to lists of paths from which a given theme should be loaded.
+ *
+ * Keys are theme names, values are associative arrays. Keys of the inner array are 'scripts',
+ * 'styles', or 'images', and values are string paths.
+ *
+ * Additionally, the string '{module}' in paths represents the name of the module to load.
+ *
+ * @return array
+ */
+ protected static function getThemePaths() {
+ $themePaths = self::$builtinThemePaths;
+ return $themePaths;
+ }
+
+ /**
+ * Return a path to load given module of given theme from.
+ *
+ * @param string $theme OOUI theme name, for example 'WikimediaUI' or 'Apex'
+ * @param string $kind Kind of the module: 'scripts', 'styles', or 'images'
+ * @param string $module Module name, for valid values see $knownScriptsModules,
+ * $knownStylesModules, $knownImagesModules
+ * @return string
+ */
+ protected function getThemePath( $theme, $kind, $module ) {
+ $paths = self::getThemePaths();
+ $path = $paths[ $theme ][ $kind ];
+ $path = str_replace( '{module}', $module, $path );
+ return $path;
+ }
+
+ /**
+ * @param string $theme See getThemePath()
+ * @param string $module See getThemePath()
+ * @return string
+ */
+ protected function getThemeScriptsPath( $theme, $module ) {
+ if ( !in_array( $module, self::$knownScriptsModules ) ) {
+ throw new InvalidArgumentException( "Invalid OOUI scripts module '$module'" );
+ }
+ return $this->getThemePath( $theme, 'scripts', $module );
+ }
+
+ /**
+ * @param string $theme See getThemePath()
+ * @param string $module See getThemePath()
+ * @return string
+ */
+ protected function getThemeStylesPath( $theme, $module ) {
+ if ( !in_array( $module, self::$knownStylesModules ) ) {
+ throw new InvalidArgumentException( "Invalid OOUI styles module '$module'" );
+ }
+ return $this->getThemePath( $theme, 'styles', $module );
+ }
+
+ /**
+ * @param string $theme See getThemePath()
+ * @param string $module See getThemePath()
+ * @return string
+ */
+ protected function getThemeImagesPath( $theme, $module ) {
+ if ( !in_array( $module, self::$knownImagesModules ) ) {
+ throw new InvalidArgumentException( "Invalid OOUI images module '$module'" );
+ }
+ return $this->getThemePath( $theme, 'images', $module );
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderRawFileModule.php b/www/wiki/includes/resourceloader/ResourceLoaderRawFileModule.php
new file mode 100644
index 00000000..beab53eb
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderRawFileModule.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Module containing files that are loaded without ResourceLoader.
+ *
+ * Primary usecase being "base" modules loaded by the startup module,
+ * such as jquery and the mw.loader client itself. These make use of
+ * ResourceLoaderModule and load.php for convenience but aren't actually
+ * registered in the startup module (as it would have to load itself).
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Timo Tijhof
+ */
+
+class ResourceLoaderRawFileModule extends ResourceLoaderFileModule {
+
+ /**
+ * Enable raw mode to omit mw.loader.state() call as mw.loader
+ * does not yet exist when these modules execute.
+ * @var bool
+ */
+ protected $raw = true;
+
+ /**
+ * Get all JavaScript code.
+ *
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ $script = parent::getScript( $context );
+ // Add closure explicitly because raw modules can't be wrapped mw.loader.implement.
+ // Unlike with mw.loader.implement, this closure is immediately invoked.
+ // @see ResourceLoader::makeModuleResponse
+ // @see ResourceLoader::makeLoaderImplementScript
+ return "(function () {\n{$script}\n}());";
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderSiteModule.php b/www/wiki/includes/resourceloader/ResourceLoaderSiteModule.php
new file mode 100644
index 00000000..236112ea
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderSiteModule.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * ResourceLoader module for site 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+/**
+ * Module for site customizations
+ */
+class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
+
+ /**
+ * Get list of pages used by this module
+ *
+ * @param ResourceLoaderContext $context
+ * @return array List of pages
+ */
+ protected function getPages( ResourceLoaderContext $context ) {
+ $pages = [];
+ if ( $this->getConfig()->get( 'UseSiteJs' ) ) {
+ $pages['MediaWiki:Common.js'] = [ 'type' => 'script' ];
+ $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.js'] = [ 'type' => 'script' ];
+ }
+ return $pages;
+ }
+
+ /**
+ * @param ResourceLoaderContext|null $context
+ * @return array
+ */
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ return [ 'site.styles' ];
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderSiteStylesModule.php b/www/wiki/includes/resourceloader/ResourceLoaderSiteStylesModule.php
new file mode 100644
index 00000000..79922bfe
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderSiteStylesModule.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * ResourceLoader module for site style 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+/**
+ * Module for site style customizations
+ */
+class ResourceLoaderSiteStylesModule extends ResourceLoaderWikiModule {
+
+ /**
+ * Get list of pages used by this module
+ *
+ * @param ResourceLoaderContext $context
+ * @return array List of pages
+ */
+ protected function getPages( ResourceLoaderContext $context ) {
+ $pages = [];
+ if ( $this->getConfig()->get( 'UseSiteCss' ) ) {
+ $pages['MediaWiki:Common.css'] = [ 'type' => 'style' ];
+ $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.css'] = [ 'type' => 'style' ];
+ $pages['MediaWiki:Print.css'] = [ 'type' => 'style', 'media' => 'print' ];
+
+ }
+ return $pages;
+ }
+
+ /**
+ * @return string
+ */
+ public function getType() {
+ return self::LOAD_STYLES;
+ }
+
+ /**
+ * @return string
+ */
+ public function getGroup() {
+ return 'site';
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php b/www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php
new file mode 100644
index 00000000..fbd0a24a
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderSkinModule.php
@@ -0,0 +1,167 @@
+<?php
+/**
+ * ResourceLoader module for skin stylesheets.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Timo Tijhof
+ */
+
+class ResourceLoaderSkinModule extends ResourceLoaderFileModule {
+ /**
+ * All skins are assumed to be compatible with mobile
+ */
+ public $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getStyles( ResourceLoaderContext $context ) {
+ $logo = $this->getLogoData( $this->getConfig() );
+ $styles = parent::getStyles( $context );
+ $this->normalizeStyles( $styles );
+
+ $default = !is_array( $logo ) ? $logo : $logo['1x'];
+ $styles['all'][] = '.mw-wiki-logo { background-image: ' .
+ CSSMin::buildUrlValue( $default ) .
+ '; }';
+
+ if ( is_array( $logo ) ) {
+ if ( isset( $logo['svg'] ) ) {
+ $styles['all'][] = '.mw-wiki-logo { ' .
+ 'background-image: -webkit-linear-gradient(transparent, transparent), ' .
+ CSSMin::buildUrlValue( $logo['svg'] ) . '; ' .
+ 'background-image: linear-gradient(transparent, transparent), ' .
+ CSSMin::buildUrlValue( $logo['svg'] ) . ';' .
+ 'background-size: 135px auto; }';
+ } else {
+ if ( isset( $logo['1.5x'] ) ) {
+ $styles[
+ '(-webkit-min-device-pixel-ratio: 1.5), ' .
+ '(min--moz-device-pixel-ratio: 1.5), ' .
+ '(min-resolution: 1.5dppx), ' .
+ '(min-resolution: 144dpi)'
+ ][] = '.mw-wiki-logo { background-image: ' .
+ CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' .
+ 'background-size: 135px auto; }';
+ }
+ if ( isset( $logo['2x'] ) ) {
+ $styles[
+ '(-webkit-min-device-pixel-ratio: 2), ' .
+ '(min--moz-device-pixel-ratio: 2), ' .
+ '(min-resolution: 2dppx), ' .
+ '(min-resolution: 192dpi)'
+ ][] = '.mw-wiki-logo { background-image: ' .
+ CSSMin::buildUrlValue( $logo['2x'] ) . ';' .
+ 'background-size: 135px auto; }';
+ }
+ }
+ }
+
+ return $styles;
+ }
+
+ /**
+ * Ensure all media keys use array values.
+ *
+ * Normalises arrays returned by the ResourceLoaderFileModule::getStyles() method.
+ *
+ * @param array &$styles Associative array, keys are strings (media queries),
+ * values are strings or arrays
+ */
+ private function normalizeStyles( &$styles ) {
+ foreach ( $styles as $key => $val ) {
+ if ( !is_array( $val ) ) {
+ $styles[$key] = [ $val ];
+ }
+ }
+ }
+
+ /**
+ * @since 1.31
+ * @param Config $conf
+ * @return string|array
+ */
+ protected function getLogoData( Config $conf ) {
+ return static::getLogo( $conf );
+ }
+
+ /**
+ * @param Config $conf
+ * @return string|array Single url if no variants are defined,
+ * or an array of logo urls keyed by dppx in form "<float>x".
+ * Key "1x" is always defined. Key "svg" may also be defined,
+ * in which case variants other than "1x" are omitted.
+ */
+ public static function getLogo( Config $conf ) {
+ $logo = $conf->get( 'Logo' );
+ $logoHD = $conf->get( 'LogoHD' );
+
+ $logo1Url = OutputPage::transformResourcePath( $conf, $logo );
+
+ if ( !$logoHD ) {
+ return $logo1Url;
+ }
+
+ $logoUrls = [
+ '1x' => $logo1Url,
+ ];
+
+ if ( isset( $logoHD['svg'] ) ) {
+ $logoUrls['svg'] = OutputPage::transformResourcePath(
+ $conf,
+ $logoHD['svg']
+ );
+ } else {
+ // Only 1.5x and 2x are supported
+ if ( isset( $logoHD['1.5x'] ) ) {
+ $logoUrls['1.5x'] = OutputPage::transformResourcePath(
+ $conf,
+ $logoHD['1.5x']
+ );
+ }
+ if ( isset( $logoHD['2x'] ) ) {
+ $logoUrls['2x'] = OutputPage::transformResourcePath(
+ $conf,
+ $logoHD['2x']
+ );
+ }
+ }
+
+ return $logoUrls;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function isKnownEmpty( ResourceLoaderContext $context ) {
+ // Regardless of whether the files are specified, we always
+ // provide mw-wiki-logo styles.
+ return false;
+ }
+
+ public function getDefinitionSummary( ResourceLoaderContext $context ) {
+ $summary = parent::getDefinitionSummary( $context );
+ $summary[] = [
+ 'logo' => $this->getConfig()->get( 'Logo' ),
+ 'logoHD' => $this->getConfig()->get( 'LogoHD' ),
+ ];
+ return $summary;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php b/www/wiki/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php
new file mode 100644
index 00000000..a0061e35
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * ResourceLoader module for populating special characters data for some
+ * editing extensions to use.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for populating special characters data for some
+ * editing extensions to use.
+ */
+class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule {
+ private $path = "resources/src/mediawiki.language/specialcharacters.json";
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * Get all the dynamic data.
+ *
+ * @return array
+ */
+ protected function getData() {
+ global $IP;
+ return json_decode( file_get_contents( "$IP/{$this->path}" ) );
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ return Xml::encodeJsCall(
+ 'mw.language.setSpecialCharacters',
+ [
+ $this->getData()
+ ],
+ ResourceLoader::inDebugMode()
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ return [ 'mediawiki.language' ];
+ }
+
+ /**
+ * @return array
+ */
+ public function getMessages() {
+ return [
+ 'special-characters-group-latin',
+ 'special-characters-group-latinextended',
+ 'special-characters-group-ipa',
+ 'special-characters-group-symbols',
+ 'special-characters-group-greek',
+ 'special-characters-group-greekextended',
+ 'special-characters-group-cyrillic',
+ 'special-characters-group-arabic',
+ 'special-characters-group-arabicextended',
+ 'special-characters-group-persian',
+ 'special-characters-group-hebrew',
+ 'special-characters-group-bangla',
+ 'special-characters-group-tamil',
+ 'special-characters-group-telugu',
+ 'special-characters-group-sinhala',
+ 'special-characters-group-devanagari',
+ 'special-characters-group-gujarati',
+ 'special-characters-group-thai',
+ 'special-characters-group-lao',
+ 'special-characters-group-khmer',
+ 'special-characters-group-canadianaboriginal',
+ 'special-characters-title-endash',
+ 'special-characters-title-emdash',
+ 'special-characters-title-minus'
+ ];
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php b/www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php
new file mode 100644
index 00000000..56d88358
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -0,0 +1,458 @@
+<?php
+/**
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+/**
+ * Module for ResourceLoader initialization.
+ *
+ * See also <https://www.mediawiki.org/wiki/ResourceLoader/Features#Startup_Module>
+ *
+ * The startup module, as being called only from ResourceLoaderClientHtml, has
+ * 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.
+ * Default: "desktop".
+ * See also: OutputPage::setTarget(), ResourceLoaderModule::getTargets().
+ */
+class ResourceLoaderStartUpModule extends ResourceLoaderModule {
+
+ // Cache for getConfigSettings() as it's called by multiple methods
+ protected $configVars = [];
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ protected function getConfigSettings( $context ) {
+ $hash = $context->getHash();
+ if ( isset( $this->configVars[$hash] ) ) {
+ return $this->configVars[$hash];
+ }
+
+ global $wgContLang;
+ $conf = $this->getConfig();
+
+ // We can't use Title::newMainPage() if 'mainpage' is in
+ // $wgForceUIMsgAsContentMsg because that will try to use the session
+ // user's language and we have no session user. This does the
+ // equivalent but falling back to our ResourceLoaderContext language
+ // instead.
+ $mainPage = Title::newFromText( $context->msg( 'mainpage' )->inContentLanguage()->text() );
+ if ( !$mainPage ) {
+ $mainPage = Title::newFromText( 'Main Page' );
+ }
+
+ /**
+ * Namespace related preparation
+ * - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
+ * - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
+ */
+ $namespaceIds = $wgContLang->getNamespaceIds();
+ $caseSensitiveNamespaces = [];
+ foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
+ $namespaceIds[$wgContLang->lc( $name )] = $index;
+ if ( !MWNamespace::isCapitalized( $index ) ) {
+ $caseSensitiveNamespaces[] = $index;
+ }
+ }
+
+ $illegalFileChars = $conf->get( 'IllegalFileChars' );
+ $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+
+ // Build list of variables
+ $vars = [
+ 'wgLoadScript' => wfScript( 'load' ),
+ 'debug' => $context->getDebug(),
+ 'skin' => $context->getSkin(),
+ 'stylepath' => $conf->get( 'StylePath' ),
+ 'wgUrlProtocols' => wfUrlProtocols(),
+ 'wgArticlePath' => $conf->get( 'ArticlePath' ),
+ 'wgScriptPath' => $conf->get( 'ScriptPath' ),
+ 'wgScript' => wfScript(),
+ 'wgSearchType' => $conf->get( 'SearchType' ),
+ 'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
+ // Force object to avoid "empty" associative array from
+ // becoming [] instead of {} in JS (T36604)
+ 'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
+ 'wgServer' => $conf->get( 'Server' ),
+ 'wgServerName' => $conf->get( 'ServerName' ),
+ 'wgUserLanguage' => $context->getLanguage(),
+ 'wgContentLanguage' => $wgContLang->getCode(),
+ 'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ),
+ 'wgVersion' => $conf->get( 'Version' ),
+ 'wgEnableAPI' => $conf->get( 'EnableAPI' ),
+ 'wgEnableWriteAPI' => $conf->get( 'EnableWriteAPI' ),
+ 'wgMainPageTitle' => $mainPage->getPrefixedText(),
+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
+ 'wgNamespaceIds' => $namespaceIds,
+ 'wgContentNamespaces' => MWNamespace::getContentNamespaces(),
+ 'wgSiteName' => $conf->get( 'Sitename' ),
+ 'wgDBname' => $conf->get( 'DBname' ),
+ 'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
+ 'wgAvailableSkins' => Skin::getSkinNames(),
+ 'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
+ // MediaWiki sets cookies to have this prefix by default
+ 'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
+ 'wgCookieDomain' => $conf->get( 'CookieDomain' ),
+ 'wgCookiePath' => $conf->get( 'CookiePath' ),
+ 'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
+ 'wgResourceLoaderMaxQueryLength' => $conf->get( 'ResourceLoaderMaxQueryLength' ),
+ 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
+ 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
+ 'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
+ 'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ),
+ 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
+ 'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
+ 'wgEnableUploads' => $conf->get( 'EnableUploads' ),
+ 'wgCommentByteLimit' => $oldCommentSchema ? 255 : null,
+ 'wgCommentCodePointLimit' => $oldCommentSchema ? null : CommentStore::COMMENT_CHARACTER_LIMIT,
+ ];
+
+ Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars ] );
+
+ $this->configVars[$hash] = $vars;
+ return $this->configVars[$hash];
+ }
+
+ /**
+ * Recursively get all explicit and implicit dependencies for to the given module.
+ *
+ * @param array $registryData
+ * @param string $moduleName
+ * @return array
+ */
+ protected static function getImplicitDependencies( array $registryData, $moduleName ) {
+ static $dependencyCache = [];
+
+ // The list of implicit dependencies won't be altered, so we can
+ // cache them without having to worry.
+ if ( !isset( $dependencyCache[$moduleName] ) ) {
+ if ( !isset( $registryData[$moduleName] ) ) {
+ // Dependencies may not exist
+ $dependencyCache[$moduleName] = [];
+ } else {
+ $data = $registryData[$moduleName];
+ $dependencyCache[$moduleName] = $data['dependencies'];
+
+ foreach ( $data['dependencies'] as $dependency ) {
+ // Recursively get the dependencies of the dependencies
+ $dependencyCache[$moduleName] = array_merge(
+ $dependencyCache[$moduleName],
+ self::getImplicitDependencies( $registryData, $dependency )
+ );
+ }
+ }
+ }
+
+ return $dependencyCache[$moduleName];
+ }
+
+ /**
+ * Optimize the dependency tree in $this->modules.
+ *
+ * The optimization basically works like this:
+ * Given we have module A with the dependencies B and C
+ * and module B with the dependency C.
+ * Now we don't have to tell the client to explicitly fetch module
+ * C as that's already included in module B.
+ *
+ * This way we can reasonably reduce the amount of module registration
+ * data send to the client.
+ *
+ * @param array &$registryData Modules keyed by name with properties:
+ * - string 'version'
+ * - array 'dependencies'
+ * - string|null 'group'
+ * - string 'source'
+ */
+ public static function compileUnresolvedDependencies( array &$registryData ) {
+ foreach ( $registryData as $name => &$data ) {
+ $dependencies = $data['dependencies'];
+ foreach ( $data['dependencies'] as $dependency ) {
+ $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
+ $dependencies = array_diff( $dependencies, $implicitDependencies );
+ }
+ // Rebuild keys
+ $data['dependencies'] = array_values( $dependencies );
+ }
+ }
+
+ /**
+ * Get registration code for all modules.
+ *
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code for registering all modules with the client loader
+ */
+ public function getModuleRegistrations( ResourceLoaderContext $context ) {
+ $resourceLoader = $context->getResourceLoader();
+ // Future developers: Use WebRequest::getRawVal() instead getVal().
+ // The getVal() method performs slow Language+UTF logic. (f303bb9360)
+ $target = $context->getRequest()->getRawVal( 'target', 'desktop' );
+ // 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';
+
+ $out = '';
+ $states = [];
+ $registryData = [];
+
+ // Get registry data
+ foreach ( $resourceLoader->getModuleNames() as $name ) {
+ $module = $resourceLoader->getModule( $name );
+ $moduleTargets = $module->getTargets();
+ if ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) ) {
+ continue;
+ }
+
+ if ( $module->isRaw() ) {
+ // Don't register "raw" modules (like 'jquery' and 'mediawiki') client-side because
+ // depending on them is illegal anyway and would only lead to them being reloaded
+ // causing any state to be lost (like jQuery plugins, mw.config etc.)
+ continue;
+ }
+
+ try {
+ $versionHash = $module->getVersionHash( $context );
+ } catch ( Exception $e ) {
+ // See also T152266 and ResourceLoader::getCombinedVersion()
+ MWExceptionHandler::logException( $e );
+ $context->getLogger()->warning(
+ 'Calculating version for "{module}" failed: {exception}',
+ [
+ 'module' => $name,
+ 'exception' => $e,
+ ]
+ );
+ $versionHash = '';
+ $states[$name] = 'error';
+ }
+
+ if ( $versionHash !== '' && strlen( $versionHash ) !== 7 ) {
+ $context->getLogger()->warning(
+ "Module '{module}' produced an invalid version hash: '{version}'.",
+ [
+ 'module' => $name,
+ 'version' => $versionHash,
+ ]
+ );
+ // Module implementation either broken or deviated from ResourceLoader::makeHash
+ // Asserted by tests/phpunit/structure/ResourcesTest.
+ $versionHash = ResourceLoader::makeHash( $versionHash );
+ }
+
+ $skipFunction = $module->getSkipFunction();
+ if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) {
+ $skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
+ }
+
+ $registryData[$name] = [
+ 'version' => $versionHash,
+ 'dependencies' => $module->getDependencies( $context ),
+ 'group' => $module->getGroup(),
+ 'source' => $module->getSource(),
+ 'skip' => $skipFunction,
+ ];
+ }
+
+ self::compileUnresolvedDependencies( $registryData );
+
+ // Register sources
+ $out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
+
+ // Figure out the different call signatures for mw.loader.register
+ $registrations = [];
+ foreach ( $registryData as $name => $data ) {
+ // Call mw.loader.register(name, version, dependencies, group, source, skip)
+ $registrations[] = [
+ $name,
+ $data['version'],
+ $data['dependencies'],
+ $data['group'],
+ // Swap default (local) for null
+ $data['source'] === 'local' ? null : $data['source'],
+ $data['skip']
+ ];
+ }
+
+ // Register modules
+ $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations );
+
+ if ( $states ) {
+ $out .= "\n" . ResourceLoader::makeLoaderStateScript( $states );
+ }
+
+ return $out;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRaw() {
+ return true;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getPreloadLinks( ResourceLoaderContext $context ) {
+ $url = self::getStartupModulesUrl( $context );
+ return [
+ $url => [ 'as' => 'script' ]
+ ];
+ }
+
+ /**
+ * Base modules required for the base environment of ResourceLoader
+ *
+ * @return array
+ */
+ public static function getStartupModules() {
+ return [ 'jquery', 'mediawiki' ];
+ }
+
+ public static function getLegacyModules() {
+ global $wgIncludeLegacyJavaScript;
+
+ $legacyModules = [];
+ if ( $wgIncludeLegacyJavaScript ) {
+ $legacyModules[] = 'mediawiki.legacy.wikibits';
+ }
+
+ return $legacyModules;
+ }
+
+ /**
+ * Get the load URL of the startup modules.
+ *
+ * This is a helper for getScript(), but can also be called standalone, such
+ * as when generating an AppCache manifest.
+ *
+ * @param ResourceLoaderContext $context
+ * @return string
+ */
+ public static function getStartupModulesUrl( ResourceLoaderContext $context ) {
+ $rl = $context->getResourceLoader();
+ $derivative = new DerivativeResourceLoaderContext( $context );
+ $derivative->setModules( array_merge(
+ self::getStartupModules(),
+ self::getLegacyModules()
+ ) );
+ $derivative->setOnly( 'scripts' );
+ // Must setModules() before makeVersionQuery()
+ $derivative->setVersion( $rl->makeVersionQuery( $derivative ) );
+
+ return $rl->createLoaderURL( 'local', $derivative );
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ global $IP;
+ if ( $context->getOnly() !== 'scripts' ) {
+ return '/* Requires only=script */';
+ }
+
+ $out = file_get_contents( "$IP/resources/src/startup.js" );
+
+ $pairs = array_map( function ( $value ) {
+ $value = FormatJson::encode( $value, ResourceLoader::inDebugMode(), FormatJson::ALL_OK );
+ // Fix indentation
+ $value = str_replace( "\n", "\n\t", $value );
+ return $value;
+ }, [
+ '$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
+ '$VARS.configuration' => $this->getConfigSettings( $context ),
+ // This url may be preloaded. See getPreloadLinks().
+ '$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ),
+ ] );
+ $pairs['$CODE.registrations()'] = str_replace(
+ "\n",
+ "\n\t",
+ trim( $this->getModuleRegistrations( $context ) )
+ );
+
+ return strtr( $out, $pairs );
+ }
+
+ /**
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return false;
+ }
+
+ /**
+ * Get the definition summary for this module.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getDefinitionSummary( ResourceLoaderContext $context ) {
+ global $IP;
+ $summary = parent::getDefinitionSummary( $context );
+ $summary[] = [
+ // Detect changes to variables exposed in mw.config (T30899).
+ 'vars' => $this->getConfigSettings( $context ),
+ // Changes how getScript() creates mw.Map for mw.config
+ 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
+ // Detect changes to the module registrations
+ 'moduleHashes' => $this->getAllModuleHashes( $context ),
+
+ 'fileMtimes' => [
+ filemtime( "$IP/resources/src/startup.js" ),
+ ],
+ ];
+ return $summary;
+ }
+
+ /**
+ * Helper method for getDefinitionSummary().
+ *
+ * @param ResourceLoaderContext $context
+ * @return string SHA-1
+ */
+ protected function getAllModuleHashes( ResourceLoaderContext $context ) {
+ $rl = $context->getResourceLoader();
+ // Preload for getCombinedVersion()
+ $rl->preloadModuleInfo( $rl->getModuleNames(), $context );
+
+ // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
+ // Think carefully before making changes to this code!
+ // Pre-populate versionHash with something because the loop over all modules below includes
+ // the startup module (this module).
+ // See ResourceLoaderModule::getVersionHash() for usage of this cache.
+ $this->versionHash[$context->getHash()] = null;
+
+ return $rl->getCombinedVersion( $context, $rl->getModuleNames() );
+ }
+
+ /**
+ * @return string
+ */
+ public function getGroup() {
+ return 'startup';
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUploadDialogModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUploadDialogModule.php
new file mode 100644
index 00000000..1a390cf1
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderUploadDialogModule.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * ResourceLoader module for the upload dialog configuration data.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for the upload dialog configuration data.
+ *
+ * @since 1.27
+ */
+class ResourceLoaderUploadDialogModule extends ResourceLoaderModule {
+
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ $config = $context->getResourceLoader()->getConfig();
+ return ResourceLoader::makeConfigSetScript( [
+ 'wgUploadDialog' => $config->get( 'UploadDialog' ),
+ ] );
+ }
+
+ /**
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserDefaultsModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserDefaultsModule.php
new file mode 100644
index 00000000..b9dc0982
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderUserDefaultsModule.php
@@ -0,0 +1,49 @@
+<?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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Ori Livneh
+ */
+
+/**
+ * Module for default user preferences.
+ */
+class ResourceLoaderUserDefaultsModule extends ResourceLoaderModule {
+
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ return Xml::encodeJsCall(
+ 'mw.user.options.set',
+ [ User::getDefaultOptions() ],
+ ResourceLoader::inDebugMode()
+ );
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserModule.php
new file mode 100644
index 00000000..8e213819
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderUserModule.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+/**
+ * Module for user customizations scripts
+ */
+class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
+
+ protected $origin = self::ORIGIN_USER_INDIVIDUAL;
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array List of pages
+ */
+ protected function getPages( ResourceLoaderContext $context ) {
+ $config = $this->getConfig();
+ $user = $context->getUserObj();
+ if ( $user->isAnon() ) {
+ return [];
+ }
+
+ // Use localised/normalised variant to ensure $excludepage matches
+ $userPage = $user->getUserPage()->getPrefixedDBkey();
+ $pages = [];
+
+ if ( $config->get( 'AllowUserJs' ) ) {
+ $pages["$userPage/common.js"] = [ 'type' => 'script' ];
+ $pages["$userPage/" . $context->getSkin() . '.js'] = [ 'type' => 'script' ];
+ }
+
+ // User group pages are maintained site-wide and enabled with site JS/CSS.
+ if ( $config->get( 'UseSiteJs' ) ) {
+ foreach ( $user->getEffectiveGroups() as $group ) {
+ if ( $group == '*' ) {
+ continue;
+ }
+ $pages["MediaWiki:Group-$group.js"] = [ 'type' => 'script' ];
+ }
+ }
+
+ // Hack for T28283: Allow excluding pages for preview on a CSS/JS page.
+ // The excludepage parameter is set by OutputPage.
+ $excludepage = $context->getRequest()->getVal( 'excludepage' );
+ if ( isset( $pages[$excludepage] ) ) {
+ unset( $pages[$excludepage] );
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Get group name
+ *
+ * @return string
+ */
+ public function getGroup() {
+ return 'user';
+ }
+
+ /**
+ * @param ResourceLoaderContext|null $context
+ * @return array
+ */
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ return [ 'user.styles' ];
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php
new file mode 100644
index 00000000..ffa55c08
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -0,0 +1,83 @@
+<?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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+/**
+ * Module for user preference customizations
+ */
+class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
+
+ protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
+
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array List of module names as strings
+ */
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ return [ 'user.defaults' ];
+ }
+
+ /**
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ // Use FILTER_NOMIN annotation to prevent needless minification and caching (T84960).
+ return ResourceLoader::FILTER_NOMIN . Xml::encodeJsCall(
+ 'mw.user.options.set',
+ [ $context->getUserObj()->getOptions( User::GETOPTIONS_EXCLUDE_DEFAULTS ) ],
+ ResourceLoader::inDebugMode()
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return false;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function isKnownEmpty( ResourceLoaderContext $context ) {
+ return !$context->getUserObj()->getOptions( User::GETOPTIONS_EXCLUDE_DEFAULTS );
+ }
+
+ /**
+ * @return string
+ */
+ public function getGroup() {
+ return 'private';
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserStylesModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserStylesModule.php
new file mode 100644
index 00000000..8d8e0085
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderUserStylesModule.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+/**
+ * Module for user customizations styles
+ */
+class ResourceLoaderUserStylesModule extends ResourceLoaderWikiModule {
+
+ protected $origin = self::ORIGIN_USER_INDIVIDUAL;
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array List of pages
+ */
+ protected function getPages( ResourceLoaderContext $context ) {
+ $config = $this->getConfig();
+ $user = $context->getUserObj();
+ if ( $user->isAnon() ) {
+ return [];
+ }
+
+ // Use localised/normalised variant to ensure $excludepage matches
+ $userPage = $user->getUserPage()->getPrefixedDBkey();
+ $pages = [];
+
+ if ( $config->get( 'AllowUserCss' ) ) {
+ $pages["$userPage/common.css"] = [ 'type' => 'style' ];
+ $pages["$userPage/" . $context->getSkin() . '.css'] = [ 'type' => 'style' ];
+ }
+
+ // User group pages are maintained site-wide and enabled with site JS/CSS.
+ if ( $config->get( 'UseSiteCss' ) ) {
+ foreach ( $user->getEffectiveGroups() as $group ) {
+ if ( $group == '*' ) {
+ continue;
+ }
+ $pages["MediaWiki:Group-$group.css"] = [ 'type' => 'style' ];
+ }
+ }
+
+ // Hack for T28283: Allow excluding pages for preview on a CSS/JS page.
+ // The excludepage parameter is set by OutputPage.
+ $excludepage = $context->getRequest()->getVal( 'excludepage' );
+ if ( isset( $pages[$excludepage] ) ) {
+ unset( $pages[$excludepage] );
+ }
+
+ return $pages;
+ }
+
+ /**
+ * @return string
+ */
+ public function getType() {
+ return self::LOAD_STYLES;
+ }
+
+ /**
+ * Get group name
+ *
+ * @return string
+ */
+ public function getGroup() {
+ return 'user';
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php b/www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php
new file mode 100644
index 00000000..ae4fb67b
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderUserTokensModule.php
@@ -0,0 +1,76 @@
+<?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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Krinkle
+ */
+
+/**
+ * Module for user tokens
+ */
+class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
+
+ protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
+
+ protected $targets = [ 'desktop', 'mobile' ];
+
+ /**
+ * Fetch the tokens for the current user.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array List of tokens keyed by token type
+ */
+ protected function contextUserTokens( ResourceLoaderContext $context ) {
+ $user = $context->getUserObj();
+
+ return [
+ 'editToken' => $user->getEditToken(),
+ 'patrolToken' => $user->getEditToken( 'patrol' ),
+ 'watchToken' => $user->getEditToken( 'watch' ),
+ 'csrfToken' => $user->getEditToken(),
+ ];
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ // Use FILTER_NOMIN annotation to prevent needless minification and caching (T84960).
+ return ResourceLoader::FILTER_NOMIN . Xml::encodeJsCall(
+ 'mw.user.tokens.set',
+ [ $this->contextUserTokens( $context ) ],
+ ResourceLoader::inDebugMode()
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ public function supportsURLLoading() {
+ return false;
+ }
+
+ /**
+ * @return string
+ */
+ public function getGroup() {
+ return 'private';
+ }
+}
diff --git a/www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php b/www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php
new file mode 100644
index 00000000..5b512af7
--- /dev/null
+++ b/www/wiki/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -0,0 +1,473 @@
+<?php
+/**
+ * Abstraction for ResourceLoader modules that pull from wiki pages.
+ *
+ * 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
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Trevor Parscal
+ * @author Roan Kattouw
+ */
+
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Abstraction for ResourceLoader modules which pull from wiki pages
+ *
+ * This can only be used for wiki pages in the MediaWiki and User namespaces,
+ * because of its dependence on the functionality of Title::isUserConfigPage()
+ * and Title::isSiteConfigPage().
+ *
+ * This module supports being used as a placeholder for a module on a remote wiki.
+ * To do so, getDB() must be overloaded to return a foreign database object that
+ * allows local wikis to query page metadata.
+ *
+ * Safe for calls on local wikis are:
+ * - Option getters:
+ * - getGroup()
+ * - getPages()
+ * - Basic methods that strictly involve the foreign database
+ * - getDB()
+ * - isKnownEmpty()
+ * - getTitleInfo()
+ */
+class ResourceLoaderWikiModule extends ResourceLoaderModule {
+
+ // Origin defaults to users with sitewide authority
+ protected $origin = self::ORIGIN_USER_SITEWIDE;
+
+ // In-process cache for title info
+ protected $titleInfo = [];
+
+ // List of page names that contain CSS
+ protected $styles = [];
+
+ // List of page names that contain JavaScript
+ protected $scripts = [];
+
+ // Group of module
+ protected $group;
+
+ /**
+ * @param array $options For back-compat, this can be omitted in favour of overwriting getPages.
+ */
+ public function __construct( array $options = null ) {
+ if ( is_null( $options ) ) {
+ return;
+ }
+
+ foreach ( $options as $member => $option ) {
+ switch ( $member ) {
+ case 'styles':
+ case 'scripts':
+ case 'group':
+ case 'targets':
+ $this->{$member} = $option;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Subclasses should return an associative array of resources in the module.
+ * Keys should be the title of a page in the MediaWiki or User namespace.
+ *
+ * Values should be a nested array of options. The supported keys are 'type' and
+ * (CSS only) 'media'.
+ *
+ * For scripts, 'type' should be 'script'.
+ *
+ * For stylesheets, 'type' should be 'style'.
+ * There is an optional media key, the value of which can be the
+ * medium ('screen', 'print', etc.) of the stylesheet.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ protected function getPages( ResourceLoaderContext $context ) {
+ $config = $this->getConfig();
+ $pages = [];
+
+ // Filter out pages from origins not allowed by the current wiki configuration.
+ if ( $config->get( 'UseSiteJs' ) ) {
+ foreach ( $this->scripts as $script ) {
+ $pages[$script] = [ 'type' => 'script' ];
+ }
+ }
+
+ if ( $config->get( 'UseSiteCss' ) ) {
+ foreach ( $this->styles as $style ) {
+ $pages[$style] = [ 'type' => 'style' ];
+ }
+ }
+
+ return $pages;
+ }
+
+ /**
+ * Get group name
+ *
+ * @return string
+ */
+ public function getGroup() {
+ return $this->group;
+ }
+
+ /**
+ * Get the Database object used in getTitleInfo().
+ *
+ * Defaults to the local replica DB. Subclasses may want to override this to return a foreign
+ * database object, or null if getTitleInfo() shouldn't access the database.
+ *
+ * NOTE: This ONLY works for getTitleInfo() and isKnownEmpty(), NOT FOR ANYTHING ELSE.
+ * In particular, it doesn't work for getContent() or getScript() etc.
+ *
+ * @return IDatabase|null
+ */
+ protected function getDB() {
+ return wfGetDB( DB_REPLICA );
+ }
+
+ /**
+ * @param string $titleText
+ * @return null|string
+ */
+ protected function getContent( $titleText ) {
+ $title = Title::newFromText( $titleText );
+ if ( !$title ) {
+ return null; // Bad title
+ }
+
+ // If the page is a redirect, follow the redirect.
+ if ( $title->isRedirect() ) {
+ $content = $this->getContentObj( $title );
+ $title = $content ? $content->getUltimateRedirectTarget() : null;
+ if ( !$title ) {
+ return null; // Dead redirect
+ }
+ }
+
+ $handler = ContentHandler::getForTitle( $title );
+ if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) {
+ $format = CONTENT_FORMAT_CSS;
+ } elseif ( $handler->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) {
+ $format = CONTENT_FORMAT_JAVASCRIPT;
+ } else {
+ return null; // Bad content model
+ }
+
+ $content = $this->getContentObj( $title );
+ if ( !$content ) {
+ return null; // No content found
+ }
+
+ return $content->serialize( $format );
+ }
+
+ /**
+ * @param Title $title
+ * @return Content|null
+ */
+ protected function getContentObj( Title $title ) {
+ $revision = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
+ if ( !$revision ) {
+ return null;
+ }
+ $content = $revision->getContent( Revision::RAW );
+ if ( !$content ) {
+ wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' );
+ return null;
+ }
+ return $content;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ $scripts = '';
+ foreach ( $this->getPages( $context ) as $titleText => $options ) {
+ if ( $options['type'] !== 'script' ) {
+ continue;
+ }
+ $script = $this->getContent( $titleText );
+ if ( strval( $script ) !== '' ) {
+ $script = $this->validateScriptFile( $titleText, $script );
+ $scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n";
+ }
+ }
+ return $scripts;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getStyles( ResourceLoaderContext $context ) {
+ $styles = [];
+ foreach ( $this->getPages( $context ) as $titleText => $options ) {
+ if ( $options['type'] !== 'style' ) {
+ continue;
+ }
+ $media = isset( $options['media'] ) ? $options['media'] : 'all';
+ $style = $this->getContent( $titleText );
+ if ( strval( $style ) === '' ) {
+ continue;
+ }
+ if ( $this->getFlip( $context ) ) {
+ $style = CSSJanus::transform( $style, true, false );
+ }
+ $style = MemoizedCallable::call( 'CSSMin::remap',
+ [ $style, false, $this->getConfig()->get( 'ScriptPath' ), true ] );
+ if ( !isset( $styles[$media] ) ) {
+ $styles[$media] = [];
+ }
+ $style = ResourceLoader::makeComment( $titleText ) . $style;
+ $styles[$media][] = $style;
+ }
+ return $styles;
+ }
+
+ /**
+ * Disable module content versioning.
+ *
+ * This class does not support generating content outside of a module
+ * request due to foreign database support.
+ *
+ * See getDefinitionSummary() for meta-data versioning.
+ *
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return false;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getDefinitionSummary( ResourceLoaderContext $context ) {
+ $summary = parent::getDefinitionSummary( $context );
+ $summary[] = [
+ 'pages' => $this->getPages( $context ),
+ // Includes meta data of current revisions
+ 'titleInfo' => $this->getTitleInfo( $context ),
+ ];
+ return $summary;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return bool
+ */
+ public function isKnownEmpty( ResourceLoaderContext $context ) {
+ $revisions = $this->getTitleInfo( $context );
+
+ // For user modules, don't needlessly load if there are no non-empty pages
+ if ( $this->getGroup() === 'user' ) {
+ foreach ( $revisions as $revision ) {
+ if ( $revision['page_len'] > 0 ) {
+ // At least one non-empty page, module should be loaded
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // T70488: For other modules (i.e. ones that are called in cached html output) only check
+ // page existance. This ensures that, if some pages in a module are temporarily blanked,
+ // we don't end omit the module's script or link tag on some pages.
+ return count( $revisions ) === 0;
+ }
+
+ private function setTitleInfo( $key, array $titleInfo ) {
+ $this->titleInfo[$key] = $titleInfo;
+ }
+
+ /**
+ * Get the information about the wiki pages for a given context.
+ * @param ResourceLoaderContext $context
+ * @return array Keyed by page name
+ */
+ protected function getTitleInfo( ResourceLoaderContext $context ) {
+ $dbr = $this->getDB();
+ if ( !$dbr ) {
+ // We're dealing with a subclass that doesn't have a DB
+ return [];
+ }
+
+ $pageNames = array_keys( $this->getPages( $context ) );
+ sort( $pageNames );
+ $key = implode( '|', $pageNames );
+ if ( !isset( $this->titleInfo[$key] ) ) {
+ $this->titleInfo[$key] = static::fetchTitleInfo( $dbr, $pageNames, __METHOD__ );
+ }
+ return $this->titleInfo[$key];
+ }
+
+ protected static function fetchTitleInfo( IDatabase $db, array $pages, $fname = __METHOD__ ) {
+ $titleInfo = [];
+ $batch = new LinkBatch;
+ foreach ( $pages as $titleText ) {
+ $title = Title::newFromText( $titleText );
+ if ( $title ) {
+ // Page name may be invalid if user-provided (e.g. gadgets)
+ $batch->addObj( $title );
+ }
+ }
+ if ( !$batch->isEmpty() ) {
+ $res = $db->select( 'page',
+ // Include page_touched to allow purging if cache is poisoned (T117587, T113916)
+ [ 'page_namespace', 'page_title', 'page_touched', 'page_len', 'page_latest' ],
+ $batch->constructSet( 'page', $db ),
+ $fname
+ );
+ foreach ( $res as $row ) {
+ // Avoid including ids or timestamps of revision/page tables so
+ // that versions are not wasted
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $titleInfo[$title->getPrefixedText()] = [
+ 'page_len' => $row->page_len,
+ 'page_latest' => $row->page_latest,
+ 'page_touched' => $row->page_touched,
+ ];
+ }
+ }
+ return $titleInfo;
+ }
+
+ /**
+ * @since 1.28
+ * @param ResourceLoaderContext $context
+ * @param IDatabase $db
+ * @param string[] $moduleNames
+ */
+ public static function preloadTitleInfo(
+ ResourceLoaderContext $context, IDatabase $db, array $moduleNames
+ ) {
+ $rl = $context->getResourceLoader();
+ // getDB() can be overridden to point to a foreign database.
+ // For now, only preload local. In the future, we could preload by wikiID.
+ $allPages = [];
+ /** @var ResourceLoaderWikiModule[] $wikiModules */
+ $wikiModules = [];
+ foreach ( $moduleNames as $name ) {
+ $module = $rl->getModule( $name );
+ if ( $module instanceof self ) {
+ $mDB = $module->getDB();
+ // Subclasses may disable getDB and implement getTitleInfo differently
+ if ( $mDB && $mDB->getDomainID() === $db->getDomainID() ) {
+ $wikiModules[] = $module;
+ $allPages += $module->getPages( $context );
+ }
+ }
+ }
+
+ if ( !$wikiModules ) {
+ // Nothing to preload
+ return;
+ }
+
+ $pageNames = array_keys( $allPages );
+ sort( $pageNames );
+ $hash = sha1( implode( '|', $pageNames ) );
+
+ // Avoid Zend bug where "static::" does not apply LSB in the closure
+ $func = [ static::class, 'fetchTitleInfo' ];
+ $fname = __METHOD__;
+
+ $cache = ObjectCache::getMainWANInstance();
+ $allInfo = $cache->getWithSetCallback(
+ $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $db->getDomainID(), $hash ),
+ $cache::TTL_HOUR,
+ function ( $curVal, &$ttl, array &$setOpts ) use ( $func, $pageNames, $db, $fname ) {
+ $setOpts += Database::getCacheSetOptions( $db );
+
+ return call_user_func( $func, $db, $pageNames, $fname );
+ },
+ [
+ 'checkKeys' => [
+ $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $db->getDomainID() ) ]
+ ]
+ );
+
+ foreach ( $wikiModules as $wikiModule ) {
+ $pages = $wikiModule->getPages( $context );
+ // Before we intersect, map the names to canonical form (T145673).
+ $intersect = [];
+ foreach ( $pages as $page => $unused ) {
+ $title = Title::newFromText( $page );
+ if ( $title ) {
+ $intersect[ $title->getPrefixedText() ] = 1;
+ } else {
+ // Page name may be invalid if user-provided (e.g. gadgets)
+ $rl->getLogger()->info(
+ 'Invalid wiki page title "{title}" in ' . __METHOD__,
+ [ 'title' => $page ]
+ );
+ }
+ }
+ $info = array_intersect_key( $allInfo, $intersect );
+ $pageNames = array_keys( $pages );
+ sort( $pageNames );
+ $key = implode( '|', $pageNames );
+ $wikiModule->setTitleInfo( $key, $info );
+ }
+ }
+
+ /**
+ * Clear the preloadTitleInfo() cache for all wiki modules on this wiki on
+ * page change if it was a JS or CSS page
+ *
+ * @param Title $title
+ * @param Revision|null $old Prior page revision
+ * @param Revision|null $new New page revision
+ * @param string $wikiId
+ * @since 1.28
+ */
+ public static function invalidateModuleCache(
+ Title $title, Revision $old = null, Revision $new = null, $wikiId
+ ) {
+ static $formats = [ CONTENT_FORMAT_CSS, CONTENT_FORMAT_JAVASCRIPT ];
+
+ if ( $old && in_array( $old->getContentFormat(), $formats ) ) {
+ $purge = true;
+ } elseif ( $new && in_array( $new->getContentFormat(), $formats ) ) {
+ $purge = true;
+ } else {
+ $purge = ( $title->isSiteConfigPage() || $title->isUserConfigPage() );
+ }
+
+ if ( $purge ) {
+ $cache = ObjectCache::getMainWANInstance();
+ $key = $cache->makeGlobalKey( 'resourceloader', 'titleinfo', $wikiId );
+ $cache->touchCheckKey( $key );
+ }
+ }
+
+ /**
+ * @since 1.28
+ * @return string
+ */
+ public function getType() {
+ // Check both because subclasses don't always pass pages via the constructor,
+ // they may also override getPages() instead, in which case we should keep
+ // defaulting to LOAD_GENERAL and allow them to override getType() separately.
+ return ( $this->styles && !$this->scripts ) ? self::LOAD_STYLES : self::LOAD_GENERAL;
+ }
+}