summaryrefslogtreecommitdiff
path: root/includes/resourceloader
diff options
context:
space:
mode:
Diffstat (limited to 'includes/resourceloader')
-rw-r--r--includes/resourceloader/ResourceLoader.php282
-rw-r--r--includes/resourceloader/ResourceLoaderContext.php20
-rw-r--r--includes/resourceloader/ResourceLoaderFileModule.php315
-rw-r--r--includes/resourceloader/ResourceLoaderLESSFunctions.php67
-rw-r--r--includes/resourceloader/ResourceLoaderLanguageDataModule.php63
-rw-r--r--includes/resourceloader/ResourceLoaderModule.php156
-rw-r--r--includes/resourceloader/ResourceLoaderNoscriptModule.php2
-rw-r--r--includes/resourceloader/ResourceLoaderSiteModule.php25
-rw-r--r--includes/resourceloader/ResourceLoaderStartUpModule.php63
-rw-r--r--includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php69
-rw-r--r--includes/resourceloader/ResourceLoaderUserGroupsModule.php15
-rw-r--r--includes/resourceloader/ResourceLoaderUserModule.php21
-rw-r--r--includes/resourceloader/ResourceLoaderUserOptionsModule.php4
-rw-r--r--includes/resourceloader/ResourceLoaderUserTokensModule.php12
-rw-r--r--includes/resourceloader/ResourceLoaderWikiModule.php36
15 files changed, 774 insertions, 376 deletions
diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php
index 7b87f9d4..6380efcf 100644
--- a/includes/resourceloader/ResourceLoader.php
+++ b/includes/resourceloader/ResourceLoader.php
@@ -39,7 +39,7 @@ class ResourceLoader {
/** Associative array mapping module name to info associative array */
protected $moduleInfos = array();
-
+
/** Associative array mapping framework ids to a list of names of test suite modules */
/** like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) */
protected $testModuleNames = array();
@@ -47,6 +47,9 @@ class ResourceLoader {
/** array( 'source-id' => array( 'loadScript' => 'http://.../load.php' ) ) **/
protected $sources = array();
+ /** @var bool */
+ protected $hasErrors = false;
+
/* Protected Methods */
/**
@@ -60,7 +63,7 @@ class ResourceLoader {
* requests its own information. This sacrifice of modularity yields a substantial
* performance improvement.
*
- * @param $modules Array: List of module names to preload information for
+ * @param array $modules List of module names to preload information for
* @param $context ResourceLoaderContext: Context to load the information within
*/
public function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
@@ -127,8 +130,8 @@ class ResourceLoader {
* If $data is empty, only contains whitespace or the filter was unknown,
* $data is returned unmodified.
*
- * @param $filter String: Name of filter to run
- * @param $data String: Text to filter, such as JavaScript or CSS text
+ * @param string $filter Name of filter to run
+ * @param string $data Text to filter, such as JavaScript or CSS text
* @return String: Filtered data, or a comment containing an error message
*/
protected function filter( $filter, $data ) {
@@ -150,6 +153,7 @@ class ResourceLoader {
$cache = wfGetCache( CACHE_ANYTHING );
$cacheEntry = $cache->get( $key );
if ( is_string( $cacheEntry ) ) {
+ wfIncrStats( "rl-$filter-cache-hits" );
wfProfileOut( __METHOD__ );
return $cacheEntry;
}
@@ -157,6 +161,7 @@ class ResourceLoader {
$result = '';
// Run the filter - we've already verified one of these will work
try {
+ wfIncrStats( "rl-$filter-cache-misses" );
switch ( $filter ) {
case 'minify-js':
$result = JavaScriptMinifier::minify( $data,
@@ -173,10 +178,12 @@ class ResourceLoader {
// Save filtered text to Memcached
$cache->set( $key, $result );
- } catch ( Exception $exception ) {
- // Return exception as a comment
- $result = $this->makeComment( $exception->__toString() );
+ } catch ( Exception $e ) {
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" );
$this->hasErrors = true;
+ // Return exception as a comment
+ $result = self::formatException( $e );
}
wfProfileOut( __METHOD__ );
@@ -201,7 +208,7 @@ class ResourceLoader {
$this->addSource( $wgResourceLoaderSources );
// Register core modules
- $this->register( include( "$IP/resources/Resources.php" ) );
+ $this->register( include "$IP/resources/Resources.php" );
// Register extension modules
wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
$this->register( $wgResourceModules );
@@ -210,7 +217,6 @@ class ResourceLoader {
$this->registerTestModules();
}
-
wfProfileOut( __METHOD__ );
}
@@ -218,7 +224,7 @@ class ResourceLoader {
* Registers a module with the ResourceLoader system.
*
* @param $name Mixed: Name of module as a string or List of name/object pairs as an array
- * @param $info array Module info array. For backwards compatibility with 1.17alpha,
+ * @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
@@ -235,6 +241,7 @@ class ResourceLoader {
foreach ( $registrations as $name => $info ) {
// Disallow duplicate registrations
if ( isset( $this->moduleInfos[$name] ) ) {
+ wfProfileOut( __METHOD__ );
// A module has already been registered by this name
throw new MWException(
'ResourceLoader duplicate registration error. ' .
@@ -244,6 +251,7 @@ class ResourceLoader {
// Check $name for validity
if ( !self::isValidModuleName( $name ) ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( "ResourceLoader module name '$name' is invalid, see ResourceLoader::isValidModuleName()" );
}
@@ -252,6 +260,7 @@ class ResourceLoader {
// Old calling convention
// Validate the input
if ( !( $info instanceof ResourceLoaderModule ) ) {
+ wfProfileOut( __METHOD__ );
throw new MWException( 'ResourceLoader invalid module error. ' .
'Instances of ResourceLoaderModule expected.' );
}
@@ -274,24 +283,28 @@ class ResourceLoader {
global $IP, $wgEnableJavaScriptTest;
if ( $wgEnableJavaScriptTest !== true ) {
- throw new MWException( 'Attempt to register JavaScript test modules but <tt>$wgEnableJavaScriptTest</tt> is false. Edit your <tt>LocalSettings.php</tt> to enable it.' );
+ throw new MWException( 'Attempt to register JavaScript test modules but <code>$wgEnableJavaScriptTest</code> is false. Edit your <code>LocalSettings.php</code> to enable it.' );
}
wfProfileIn( __METHOD__ );
// Get core test suites
$testModules = array();
- $testModules['qunit'] = include( "$IP/tests/qunit/QUnitTestResources.php" );
+ $testModules['qunit'] = include "$IP/tests/qunit/QUnitTestResources.php";
// Get other test suites (e.g. from extensions)
wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) );
// 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 $moduleName => $moduleProps ) {
- $testModules['qunit'][$moduleName]['dependencies'][] = 'mediawiki.tests.qunit.testrunner';
+ 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'][] = 'mediawiki.tests.qunit.testrunner';
}
- foreach( $testModules as $id => $names ) {
+ foreach ( $testModules as $id => $names ) {
// Register test modules
$this->register( $testModules[$id] );
@@ -309,9 +322,10 @@ class ResourceLoader {
* 'loadScript': URL (either fully-qualified or protocol-relative) of load.php for this source
*
* @param $id Mixed: source ID (string), or array( id1 => props1, id2 => props2, ... )
- * @param $properties Array: source properties
+ * @param array $properties source properties
+ * @throws MWException
*/
- public function addSource( $id, $properties = null) {
+ public function addSource( $id, $properties = null ) {
// Allow multiple sources to be registered in one call
if ( is_array( $id ) ) {
foreach ( $id as $key => $value ) {
@@ -346,18 +360,18 @@ class ResourceLoader {
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 $framework String: Optional. Get only the test module names for one
+ * @param string $framework Optional. Get only the test module names for one
* particular framework.
* @return Array
*/
public function getTestModuleNames( $framework = 'all' ) {
- /// @TODO: api siteinfo prop testmodulenames modulenames
+ /// @todo api siteinfo prop testmodulenames modulenames
if ( $framework == 'all' ) {
return $this->testModuleNames;
} elseif ( isset( $this->testModuleNames[$framework] ) && is_array( $this->testModuleNames[$framework] ) ) {
@@ -370,7 +384,7 @@ class ResourceLoader {
/**
* Get the ResourceLoaderModule object for a given module name.
*
- * @param $name String: Module name
+ * @param string $name Module name
* @return ResourceLoaderModule if module has been registered, null otherwise
*/
public function getModule( $name ) {
@@ -381,6 +395,7 @@ class ResourceLoader {
}
// Construct the requested object
$info = $this->moduleInfos[$name];
+ /** @var ResourceLoaderModule $object */
if ( isset( $info['object'] ) ) {
// Object given in info array
$object = $info['object'];
@@ -435,7 +450,6 @@ class ResourceLoader {
wfProfileIn( __METHOD__ );
$errors = '';
- $this->hasErrors = false;
// Split requested modules into two groups, modules and missing
$modules = array();
@@ -446,11 +460,14 @@ class ResourceLoader {
// Do not allow private modules to be loaded from the web.
// This is a security issue, see bug 34907.
if ( $module->getGroup() === 'private' ) {
- $errors .= $this->makeComment( "Cannot show private module \"$name\"" );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $errors .= self::makeComment( "Cannot show private module \"$name\"" );
+
continue;
}
- $modules[$name] = $this->getModule( $name );
+ $modules[$name] = $module;
} else {
$missing[] = $name;
}
@@ -459,13 +476,15 @@ class ResourceLoader {
// Preload information needed to the mtime calculation below
try {
$this->preloadModuleInfo( array_keys( $modules ), $context );
- } catch( Exception $e ) {
- // Add exception to the output as a comment
- $errors .= $this->makeComment( $e->__toString() );
+ } catch ( Exception $e ) {
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $errors .= self::formatException( $e );
}
- wfProfileIn( __METHOD__.'-getModifiedTime' );
+ wfProfileIn( __METHOD__ . '-getModifiedTime' );
// To send Last-Modified and support If-Modified-Since, we need to detect
// the last modified time
@@ -478,13 +497,15 @@ class ResourceLoader {
// Calculate maximum modified time
$mtime = max( $mtime, $module->getModifiedTime( $context ) );
} catch ( Exception $e ) {
- // Add exception to the output as a comment
- $errors .= $this->makeComment( $e->__toString() );
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $errors .= self::formatException( $e );
}
}
- wfProfileOut( __METHOD__.'-getModifiedTime' );
+ wfProfileOut( __METHOD__ . '-getModifiedTime' );
// If there's an If-Modified-Since header, respond with a 304 appropriately
if ( $this->tryRespondLastModified( $context, $mtime ) ) {
@@ -501,7 +522,7 @@ class ResourceLoader {
// 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() && strlen( $warnings = ob_get_contents() ) ) {
- $response = $this->makeComment( $warnings ) . $response;
+ $response = self::makeComment( $warnings ) . $response;
$this->hasErrors = true;
}
@@ -530,8 +551,8 @@ class ResourceLoader {
/**
* Send content type and last modified headers to the client.
* @param $context ResourceLoaderContext
- * @param $mtime string TS_MW timestamp to use for last-modified
- * @param $error bool Whether there are commented-out errors in the response
+ * @param string $mtime TS_MW timestamp to use for last-modified
+ * @param bool $errors Whether there are commented-out errors in the response
* @return void
*/
protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) {
@@ -540,16 +561,17 @@ class ResourceLoader {
// to propagate to clients quickly
// If there were errors, we also need a shorter expiry time so we can recover quickly
if ( is_null( $context->getVersion() ) || $errors ) {
- $maxage = $wgResourceLoaderMaxage['unversioned']['client'];
+ $maxage = $wgResourceLoaderMaxage['unversioned']['client'];
$smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
// If a version was specified we can use a longer expiry time since changing
// version numbers causes cache misses
} else {
- $maxage = $wgResourceLoaderMaxage['versioned']['client'];
+ $maxage = $wgResourceLoaderMaxage['versioned']['client'];
$smaxage = $wgResourceLoaderMaxage['versioned']['server'];
}
if ( $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' );
}
@@ -569,8 +591,8 @@ class ResourceLoader {
* If there's an If-Modified-Since header, respond with a 304 appropriately
* and clear out the output buffer. If the client cache is too old then do nothing.
* @param $context ResourceLoaderContext
- * @param $mtime string The TS_MW timestamp to check the header against
- * @return bool True iff 304 header sent and output handled
+ * @param string $mtime The TS_MW timestamp to check the header against
+ * @return bool True if 304 header sent and output handled
*/
protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) {
// If there's an If-Modified-Since header, respond with a 304 appropriately
@@ -590,13 +612,7 @@ class ResourceLoader {
// See also http://bugs.php.net/bug.php?id=51579
// To work around this, we tear down all output buffering before
// sending the 304.
- // On some setups, ob_get_level() doesn't seem to go down to zero
- // no matter how often we call ob_get_clean(), so instead of doing
- // the more intuitive while ( ob_get_level() > 0 ) ob_get_clean();
- // we have to be safe here and avoid an infinite loop.
- for ( $i = 0; $i < ob_get_level(); $i++ ) {
- ob_end_clean();
- }
+ wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
header( 'HTTP/1.0 304 Not Modified' );
header( 'Status: 304 Not Modified' );
@@ -628,7 +644,7 @@ class ResourceLoader {
if ( !$good ) {
try { // RL always hits the DB on file cache miss...
wfGetDB( DB_SLAVE );
- } catch( DBConnectionError $e ) { // ...check if we need to fallback to cache
+ } catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache
$good = $fileCache->isCacheGood(); // cache existence check
}
}
@@ -657,22 +673,45 @@ class ResourceLoader {
return false; // cache miss
}
- protected function makeComment( $text ) {
+ /**
+ * Generate a CSS or JS comment block. Only use this for public data,
+ * not error message details.
+ *
+ * @param $text string
+ * @return string
+ */
+ public static function makeComment( $text ) {
$encText = str_replace( '*/', '* /', $text );
return "/*\n$encText\n*/\n";
}
/**
+ * Handle exception display
+ *
+ * @param Exception $e to be shown to the user
+ * @return string sanitized text that can be returned to the user
+ */
+ public static function formatException( $e ) {
+ global $wgShowExceptionDetails;
+
+ if ( $wgShowExceptionDetails ) {
+ return self::makeComment( $e->__toString() );
+ } else {
+ return self::makeComment( wfMessage( 'internalerror' )->text() );
+ }
+ }
+
+ /**
* Generates code for a response
*
* @param $context ResourceLoaderContext: Context in which to generate a response
- * @param $modules Array: List of module objects keyed by module name
- * @param $missing Array: List of unavailable modules (optional)
+ * @param array $modules List of module objects keyed by module name
+ * @param array $missing List of unavailable modules (optional)
* @return String: Response data
*/
public function makeModuleResponse( ResourceLoaderContext $context,
- array $modules, $missing = array() )
- {
+ array $modules, $missing = array()
+ ) {
$out = '';
$exceptions = '';
if ( $modules === array() && $missing === array() ) {
@@ -685,9 +724,11 @@ class ResourceLoader {
try {
$blobs = MessageBlobStore::get( $this, $modules, $context->getLanguage() );
} catch ( Exception $e ) {
- // Add exception to the output as a comment
- $exceptions .= $this->makeComment( $e->__toString() );
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $exceptions .= self::formatException( $e );
}
} else {
$blobs = array();
@@ -791,9 +832,11 @@ class ResourceLoader {
break;
}
} catch ( Exception $e ) {
- // Add exception to the output as a comment
- $exceptions .= $this->makeComment( $e->__toString() );
+ MWExceptionHandler::logException( $e );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" );
$this->hasErrors = true;
+ // Add exception to the output as a comment
+ $exceptions .= self::formatException( $e );
// Register module as missing
$missing[] = $name;
@@ -834,7 +877,7 @@ class ResourceLoader {
* Returns JS code to call to mw.loader.implement for a module with
* given properties.
*
- * @param $name string Module name
+ * @param string $name Module name
* @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code
* @param $styles Mixed: Array of CSS strings keyed by media type, or an array of lists of URLs to
* CSS files keyed by media type
@@ -842,6 +885,7 @@ class ResourceLoader {
* associative array mapping message key to value, or a JSON-encoded message blob containing
* the same data, wrapped in an XmlJsCode object.
*
+ * @throws MWException
* @return string
*/
public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
@@ -862,7 +906,9 @@ class ResourceLoader {
// output javascript "[]" instead of "{}". This fixes that.
(object)$styles,
(object)$messages
- ) );
+ ),
+ ResourceLoader::inDebugMode()
+ );
}
/**
@@ -881,7 +927,7 @@ class ResourceLoader {
* Combines an associative array mapping media type to CSS into a
* single stylesheet with "@media" blocks.
*
- * @param $styles Array: Array keyed by media type containing (arrays of) CSS strings.
+ * @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings.
*
* @return Array
*/
@@ -891,7 +937,7 @@ class ResourceLoader {
// 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;
+ $styles = (array)$styles;
foreach ( $styles as $style ) {
$style = trim( $style );
// Don't output an empty "@media print { }" block (bug 40498)
@@ -902,7 +948,7 @@ class ResourceLoader {
if ( $media === '' || $media == 'all' ) {
$out[] = $style;
- } else if ( is_string( $media ) ) {
+ } elseif ( is_string( $media ) ) {
$out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}";
}
// else: skip
@@ -941,12 +987,12 @@ class ResourceLoader {
* which will have values corresponding to $name, $version, $dependencies
* and $group as supplied.
*
- * @param $name String: Module name
+ * @param string $name Module name
* @param $version Integer: Module version number as a timestamp
- * @param $dependencies Array: List of module names on which this module depends
- * @param $group String: Group which the module is in.
- * @param $source String: Source of the module, or 'local' if not foreign.
- * @param $script String: JavaScript code
+ * @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
*/
@@ -974,21 +1020,21 @@ class ResourceLoader {
* ) ):
* Registers modules with the given names and parameters.
*
- * @param $name String: Module name
+ * @param string $name Module name
* @param $version Integer: Module version number as a timestamp
- * @param $dependencies Array: List of module names on which this module depends
- * @param $group String: group which the module is in.
- * @param $source String: source of the module, or 'local' if not foreign
+ * @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
*
* @return string
*/
public static function makeLoaderRegisterScript( $name, $version = null,
- $dependencies = null, $group = null, $source = null )
- {
+ $dependencies = null, $group = null, $source = null
+ ) {
if ( is_array( $name ) ) {
return Xml::encodeJsCall( 'mw.loader.register', array( $name ) );
} else {
- $version = (int) $version > 1 ? (int) $version : 1;
+ $version = (int)$version > 1 ? (int)$version : 1;
return Xml::encodeJsCall( 'mw.loader.register',
array( $name, $version, $dependencies, $group, $source ) );
}
@@ -1004,8 +1050,8 @@ class ResourceLoader {
* - ResourceLoader::makeLoaderSourcesScript( array( $id1 => $props1, $id2 => $props2, ... ) );
* Register sources with the given IDs and properties.
*
- * @param $id String: source ID
- * @param $properties Array: source properties (see addSource())
+ * @param string $id source ID
+ * @param array $properties source properties (see addSource())
*
* @return string
*/
@@ -1021,7 +1067,7 @@ class ResourceLoader {
* Returns JS code which runs given JS code if the client-side framework is
* present.
*
- * @param $script String: JavaScript code
+ * @param string $script JavaScript code
*
* @return string
*/
@@ -1033,12 +1079,12 @@ class ResourceLoader {
* Returns JS code which will set the MediaWiki configuration array to
* the given value.
*
- * @param $configuration Array: List of configuration values keyed by variable name
+ * @param array $configuration List of configuration values keyed by variable name
*
* @return string
*/
public static function makeConfigSetScript( array $configuration ) {
- return Xml::encodeJsCall( 'mw.config.set', array( $configuration ) );
+ return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), ResourceLoader::inDebugMode() );
}
/**
@@ -1046,7 +1092,7 @@ class ResourceLoader {
*
* For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' )
* becomes 'foo.bar,baz|bar.baz,quux'
- * @param $modules array of module names (strings)
+ * @param array $modules of module names (strings)
* @return string Packed query string
*/
public static function makePackedModulesString( $modules ) {
@@ -1084,16 +1130,16 @@ class ResourceLoader {
/**
* Build a load.php URL
- * @param $modules array of module names (strings)
- * @param $lang string Language code
- * @param $skin string Skin name
- * @param $user string|null User name. If null, the &user= parameter is omitted
- * @param $version string|null Versioning timestamp
- * @param $debug bool Whether the request should be in debug mode
- * @param $only string|null &only= parameter
- * @param $printable bool Printable mode
- * @param $handheld bool Handheld mode
- * @param $extraQuery array Extra query parameters to add
+ * @param array $modules of module names (strings)
+ * @param string $lang Language code
+ * @param string $skin Skin name
+ * @param string|null $user User name. If null, the &user= parameter is omitted
+ * @param string|null $version Versioning timestamp
+ * @param bool $debug Whether the request should be in debug mode
+ * @param string|null $only &only= parameter
+ * @param bool $printable Printable mode
+ * @param bool $handheld Handheld mode
+ * @param array $extraQuery Extra query parameters to add
* @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative)
*/
public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null,
@@ -1111,6 +1157,18 @@ class ResourceLoader {
/**
* Build a query array (array representation of query string) for load.php. Helper
* function for makeLoaderURL().
+ *
+ * @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,
@@ -1149,10 +1207,54 @@ class ResourceLoader {
* Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be
* at most 255 bytes.
*
- * @param $moduleName string Module name to check
+ * @param string $moduleName Module name to check
* @return bool Whether $moduleName is a valid module name
*/
public static function isValidModuleName( $moduleName ) {
return !preg_match( '/[|,!]/', $moduleName ) && strlen( $moduleName ) <= 255;
}
+
+ /**
+ * Returns LESS compiler set up for use with MediaWiki
+ *
+ * @since 1.22
+ * @return lessc
+ */
+ public static function getLessCompiler() {
+ global $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths;
+
+ // When called from the installer, it is possible that a required PHP extension
+ // is missing (at least for now; see bug 47564). If this is the case, throw an
+ // exception (caught by the installer) to prevent a fatal error later on.
+ if ( !function_exists( 'ctype_digit' ) ) {
+ throw new MWException( 'lessc requires the Ctype extension' );
+ }
+
+ $less = new lessc();
+ $less->setPreserveComments( true );
+ $less->setVariables( self::getLESSVars() );
+ $less->setImportDir( $wgResourceLoaderLESSImportPaths );
+ foreach ( $wgResourceLoaderLESSFunctions as $name => $func ) {
+ $less->registerFunction( $name, $func );
+ }
+ return $less;
+ }
+
+ /**
+ * Get global LESS variables.
+ *
+ * $since 1.22
+ * @return array: Map of variable names to string CSS values.
+ */
+ public static function getLESSVars() {
+ global $wgResourceLoaderLESSVars;
+
+ static $lessVars = null;
+ if ( $lessVars === null ) {
+ $lessVars = $wgResourceLoaderLESSVars;
+ // Sort by key to ensure consistent hashing for cache lookups.
+ ksort( $lessVars );
+ }
+ return $lessVars;
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php
index 0e96c6c8..22ff6a7e 100644
--- a/includes/resourceloader/ResourceLoaderContext.php
+++ b/includes/resourceloader/ResourceLoaderContext.php
@@ -58,14 +58,14 @@ class ResourceLoaderContext {
// Interpret request
// List of modules
$modules = $request->getVal( 'modules' );
- $this->modules = $modules ? self::expandModuleNames( $modules ) : array();
+ $this->modules = $modules ? self::expandModuleNames( $modules ) : array();
// Various parameters
- $this->skin = $request->getVal( 'skin' );
- $this->user = $request->getVal( 'user' );
- $this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug );
- $this->only = $request->getVal( 'only' );
- $this->version = $request->getVal( 'version' );
- $this->raw = $request->getFuzzyBool( 'raw' );
+ $this->skin = $request->getVal( 'skin' );
+ $this->user = $request->getVal( 'user' );
+ $this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug );
+ $this->only = $request->getVal( 'only' );
+ $this->version = $request->getVal( 'version' );
+ $this->raw = $request->getFuzzyBool( 'raw' );
$skinnames = Skin::getSkinNames();
// If no skin is specified, or we don't recognize the skin, use the default skin
@@ -78,7 +78,7 @@ class ResourceLoaderContext {
* Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to
* an array of module names like array( 'jquery.foo', 'jquery.bar',
* 'jquery.ui.baz', 'jquery.ui.quux' )
- * @param $modules String Packed module name list
+ * @param string $modules Packed module name list
* @return array of module names
*/
public static function expandModuleNames( $modules ) {
@@ -96,7 +96,7 @@ class ResourceLoaderContext {
$pos = strrpos( $group, '.' );
if ( $pos === false ) {
// Prefixless modules, i.e. without dots
- $retval = explode( ',', $group );
+ $retval = array_merge( $retval, explode( ',', $group ) );
} else {
// We have a prefix and a bunch of suffixes
$prefix = substr( $group, 0, $pos ); // 'foo'
@@ -145,7 +145,7 @@ class ResourceLoaderContext {
public function getLanguage() {
if ( $this->language === null ) {
global $wgLang;
- $this->language = $this->request->getVal( 'lang' );
+ $this->language = $this->request->getVal( 'lang' );
if ( !$this->language ) {
$this->language = $wgLang->getCode();
}
diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php
index 8b9b7277..9ed181ed 100644
--- a/includes/resourceloader/ResourceLoaderFileModule.php
+++ b/includes/resourceloader/ResourceLoaderFileModule.php
@@ -113,6 +113,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
protected $debugRaw = true;
/** Boolean: Whether mw.loader.state() call should be omitted */
protected $raw = false;
+ protected $targets = array( 'desktop' );
+
+ /**
+ * Boolean: Whether getStyleURLsForDebug should return raw file paths,
+ * or return load.php urls
+ */
+ protected $hasGeneratedStyles = false;
+
/**
* Array: Cache for mtime
* @par Usage:
@@ -135,57 +143,58 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Constructs a new module from an options array.
*
- * @param $options Array: List of options; if not given or empty, an empty module will be
+ * @param array $options List of options; if not given or empty, an empty module will be
* constructed
- * @param $localBasePath String: Base path to prepend to all local paths in $options. Defaults
+ * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
* to $IP
- * @param $remoteBasePath String: Base path to prepend to all remote paths in $options. Defaults
+ * @param string $remoteBasePath Base path to prepend to all remote paths in $options. Defaults
* to $wgScriptPath
*
* Below is a description for the $options array:
+ * @throws MWException
* @par Construction options:
* @code
- * array(
- * // 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 $wgScriptPath
- * 'remoteBasePath' => [base path],
- * // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
- * 'remoteExtPath' => [base path],
- * // Scripts to always include
- * 'scripts' => [file path string or array of file path strings],
- * // Scripts to include in specific language contexts
- * 'languageScripts' => array(
- * [language code] => [file path string or array of file path strings],
- * ),
- * // Scripts to include in specific skin contexts
- * 'skinScripts' => array(
- * [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],
- * // Scripts to include in the startup module
- * 'loaderScripts' => [file path string or array of file path strings],
- * // Modules which must be loaded before this module
- * 'dependencies' => [modile name string or array of module name strings],
- * // Styles to always load
- * 'styles' => [file path string or array of file path strings],
- * // Styles to include in specific skin contexts
- * 'skinStyles' => array(
- * [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],
- * // Position on the page to load this module at
- * 'position' => ['bottom' (default) or 'top']
- * )
+ * array(
+ * // 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 $wgScriptPath
+ * 'remoteBasePath' => [base path],
+ * // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
+ * 'remoteExtPath' => [base path],
+ * // Scripts to always include
+ * 'scripts' => [file path string or array of file path strings],
+ * // Scripts to include in specific language contexts
+ * 'languageScripts' => array(
+ * [language code] => [file path string or array of file path strings],
+ * ),
+ * // Scripts to include in specific skin contexts
+ * 'skinScripts' => array(
+ * [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],
+ * // Scripts to include in the startup module
+ * 'loaderScripts' => [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],
+ * // Styles to always load
+ * 'styles' => [file path string or array of file path strings],
+ * // Styles to include in specific skin contexts
+ * 'skinStyles' => array(
+ * [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],
+ * // Position on the page to load this module at
+ * 'position' => ['bottom' (default) or 'top']
+ * )
* @endcode
*/
public function __construct( $options = array(), $localBasePath = null,
- $remoteBasePath = null )
- {
+ $remoteBasePath = null
+ ) {
global $IP, $wgScriptPath, $wgResourceBasePath;
$this->localBasePath = $localBasePath === null ? $IP : $localBasePath;
if ( $remoteBasePath !== null ) {
@@ -206,7 +215,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
case 'debugScripts':
case 'loaderScripts':
case 'styles':
- $this->{$member} = (array) $option;
+ $this->{$member} = (array)$option;
break;
// Collated lists of file paths
case 'languageScripts':
@@ -225,25 +234,26 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
"'$key' given, string expected."
);
}
- $this->{$member}[$key] = (array) $value;
+ $this->{$member}[$key] = (array)$value;
}
break;
// Lists of strings
case 'dependencies':
case 'messages':
- $this->{$member} = (array) $option;
+ case 'targets':
+ $this->{$member} = (array)$option;
break;
// Single strings
case 'group':
case 'position':
case 'localBasePath':
case 'remoteBasePath':
- $this->{$member} = (string) $option;
+ $this->{$member} = (string)$option;
break;
// Single booleans
case 'debugRaw':
case 'raw':
- $this->{$member} = (bool) $option;
+ $this->{$member} = (bool)$option;
break;
}
}
@@ -255,8 +265,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets all scripts for a given context concatenated together.
*
- * @param $context ResourceLoaderContext: Context in which to generate script
- * @return String: JavaScript code for $context
+ * @param ResourceLoaderContext $context Context in which to generate script
+ * @return string: JavaScript code for $context
*/
public function getScript( ResourceLoaderContext $context ) {
$files = $this->getScriptFiles( $context );
@@ -264,7 +274,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return array
*/
public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
@@ -285,7 +295,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets loader script.
*
- * @return String: JavaScript code to be added to startup module
+ * @return string: JavaScript code to be added to startup module
*/
public function getLoaderScript() {
if ( count( $this->loaderScripts ) == 0 ) {
@@ -297,8 +307,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets all styles for a given context concatenated together.
*
- * @param $context ResourceLoaderContext: Context in which to generate styles
- * @return String: CSS code for $context
+ * @param ResourceLoaderContext $context Context in which to generate styles
+ * @return string: CSS code for $context
*/
public function getStyles( ResourceLoaderContext $context ) {
$styles = $this->readStyleFiles(
@@ -320,16 +330,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
);
}
} catch ( Exception $e ) {
- wfDebug( __METHOD__ . " failed to update DB: $e\n" );
+ wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" );
}
return $styles;
}
/**
- * @param $context ResourceLoaderContext
+ * @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 = array();
foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
$urls[$mediaType] = array();
@@ -343,7 +360,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets list of message keys used by this module.
*
- * @return Array: List of message keys
+ * @return array: List of message keys
*/
public function getMessages() {
return $this->messages;
@@ -352,7 +369,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets the name of the group this module should be loaded in.
*
- * @return String: Group name
+ * @return string: Group name
*/
public function getGroup() {
return $this->group;
@@ -368,7 +385,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets list of names of modules this module depends on.
*
- * @return Array: List of module names
+ * @return array: List of module names
*/
public function getDependencies() {
return $this->dependencies;
@@ -390,9 +407,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
* calculations on files relevant to the given language, skin and debug
* mode.
*
- * @param $context ResourceLoaderContext: Context in which to calculate
+ * @param ResourceLoaderContext $context Context in which to calculate
* the modified time
- * @return Integer: UNIX timestamp
+ * @return int: UNIX timestamp
* @see ResourceLoaderModule::getFileDependencies
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
@@ -437,9 +454,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
return $this->modifiedTime[$context->getHash()] = 1;
}
- wfProfileIn( __METHOD__.'-filemtime' );
+ wfProfileIn( __METHOD__ . '-filemtime' );
$filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) );
- wfProfileOut( __METHOD__.'-filemtime' );
+ wfProfileOut( __METHOD__ . '-filemtime' );
$this->modifiedTime[$context->getHash()] = max(
$filesMtime,
$this->getMsgBlobMtime( $context->getLanguage() ) );
@@ -451,7 +468,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/* Protected Methods */
/**
- * @param $path string
+ * @param string $path
* @return string
*/
protected function getLocalPath( $path ) {
@@ -459,7 +476,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
- * @param $path string
+ * @param string $path
* @return string
*/
protected function getRemotePath( $path ) {
@@ -467,17 +484,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
+ * 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 $list Array: List of file paths in any combination of index/path
+ * @param array $list List of file paths in any combination of index/path
* or path/options pairs
- * @param $option String: option name
- * @param $default Mixed: default value if the option isn't set
- * @return Array: List of file paths, collated by $option
+ * @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 = array();
- foreach ( (array) $list as $key => $value ) {
+ foreach ( (array)$list as $key => $value ) {
if ( is_int( $key ) ) {
// File name as the value
if ( !isset( $collatedFiles[$default] ) ) {
@@ -499,10 +527,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets a list of element that match a key, optionally using a fallback key.
*
- * @param $list Array: List of lists to select from
- * @param $key String: Key to look for in $map
- * @param $fallback String: Key to look for in $list if $key doesn't exist
- * @return Array: List of elements from $map which matched $key or $fallback,
+ * @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 ) {
@@ -520,8 +548,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets a list of file paths for all scripts in this module, in order of propper execution.
*
- * @param $context ResourceLoaderContext: Context
- * @return Array: List of file paths
+ * @param ResourceLoaderContext $context
+ * @return array: List of file paths
*/
protected function getScriptFiles( ResourceLoaderContext $context ) {
$files = array_merge(
@@ -532,14 +560,15 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
if ( $context->getDebug() ) {
$files = array_merge( $files, $this->debugScripts );
}
- return $files;
+
+ return array_unique( $files );
}
/**
* Gets a list of file paths for all styles in this module, in order of propper inclusion.
*
- * @param $context ResourceLoaderContext: Context
- * @return Array: List of file paths
+ * @param ResourceLoaderContext $context
+ * @return array: List of file paths
*/
protected function getStyleFiles( ResourceLoaderContext $context ) {
return array_merge_recursive(
@@ -551,10 +580,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
+ * Returns all style files used by this module
+ * @return array
+ */
+ public function getAllStyleFiles() {
+ $files = array();
+ foreach( (array)$this->styles as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $path = $key;
+ } else {
+ $path = $value;
+ }
+ $files[] = $this->getLocalPath( $path );
+ }
+ return $files;
+ }
+
+ /**
* Gets the contents of a list of JavaScript files.
*
- * @param $scripts Array: List of file paths to scripts to read, remap and concetenate
- * @return String: Concatenated and remapped JavaScript data from $scripts
+ * @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 ) {
global $wgResourceLoaderValidateStaticJS;
@@ -565,7 +612,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
foreach ( array_unique( $scripts ) as $fileName ) {
$localPath = $this->getLocalPath( $fileName );
if ( !file_exists( $localPath ) ) {
- throw new MWException( __METHOD__.": script file not found: \"$localPath\"" );
+ throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" );
}
$contents = file_get_contents( $localPath );
if ( $wgResourceLoaderValidateStaticJS ) {
@@ -582,12 +629,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets the contents of a list of CSS files.
*
- * @param $styles Array: List of media type/list of file paths pairs, to read, remap and
+ * @param array $styles List of media type/list of file paths pairs, to read, remap and
* concetenate
*
- * @param $flip bool
+ * @param bool $flip
*
- * @return Array: List of concatenated and remapped CSS data from $styles,
+ * @return array: List of concatenated and remapped CSS data from $styles,
* keyed by media type
*/
protected function readStyleFiles( array $styles, $flip ) {
@@ -613,18 +660,27 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
*
* This method can be used as a callback for array_map()
*
- * @param $path String: File path of style file to read
- * @param $flip bool
+ * @param string $path File path of style file to read
+ * @param bool $flip
*
- * @return String: CSS data in script file
+ * @return string: CSS data in script file
* @throws MWException if the file doesn't exist
*/
protected function readStyleFile( $path, $flip ) {
$localPath = $this->getLocalPath( $path );
if ( !file_exists( $localPath ) ) {
- throw new MWException( __METHOD__.": style file not found: \"$localPath\"" );
+ $msg = __METHOD__ . ": style file not found: \"$localPath\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+
+ if ( $this->getStyleSheetLang( $path ) === 'less' ) {
+ $style = $this->compileLESSFile( $localPath );
+ $this->hasGeneratedStyles = true;
+ } else {
+ $style = file_get_contents( $localPath );
}
- $style = file_get_contents( $localPath );
+
if ( $flip ) {
$style = CSSJanus::transform( $style, true, false );
}
@@ -646,28 +702,75 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
- * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist
- * but returns 1 instead.
- * @param $filename string File name
- * @return int UNIX timestamp, or 1 if the file doesn't exist
- */
- protected static function safeFilemtime( $filename ) {
- if ( file_exists( $filename ) ) {
- return filemtime( $filename );
- } else {
- // We only ever map this function on an array if we're gonna call max() after,
- // so return our standard minimum timestamps here. This is 1, not 0, because
- // wfTimestamp(0) == NOW
- return 1;
- }
- }
-
- /**
* Get whether CSS for this module should be flipped
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return bool
*/
public function getFlip( $context ) {
return $context->getDirection() === 'rtl';
}
+
+ /**
+ * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
+ *
+ * @return array of strings
+ */
+ public function getTargets() {
+ return $this->targets;
+ }
+
+ /**
+ * Generate a cache key for a LESS file.
+ *
+ * The cache key varies on the file name and the names and values of global
+ * LESS variables.
+ *
+ * @since 1.22
+ * @param string $fileName File name of root LESS file.
+ * @return string: Cache key
+ */
+ protected static function getLESSCacheKey( $fileName ) {
+ $vars = json_encode( ResourceLoader::getLESSVars() );
+ $hash = md5( $fileName . $vars );
+ return wfMemcKey( 'resourceloader', 'less', $hash );
+ }
+
+ /**
+ * Compile a LESS file into CSS.
+ *
+ * If invalid, returns replacement CSS source consisting of the compilation
+ * error message encoded as a comment. To save work, we cache a result object
+ * which comprises the compiled CSS and the names & mtimes of the files
+ * that were processed. lessphp compares the cached & current mtimes and
+ * recompiles as necessary.
+ *
+ * @since 1.22
+ * @param string $fileName File path of LESS source
+ * @return string: CSS source
+ */
+ protected function compileLESSFile( $fileName ) {
+ $key = self::getLESSCacheKey( $fileName );
+ $cache = wfGetCache( CACHE_ANYTHING );
+
+ // The input to lessc. Either an associative array representing the
+ // cached results of a previous compilation, or the string file name if
+ // no cache result exists.
+ $source = $cache->get( $key );
+ if ( !is_array( $source ) || !isset( $source['root'] ) ) {
+ $source = $fileName;
+ }
+
+ $compiler = ResourceLoader::getLessCompiler();
+ $result = null;
+
+ $result = $compiler->cachedCompile( $source );
+
+ if ( !is_array( $result ) ) {
+ throw new MWException( 'LESS compiler result has type ' . gettype( $result ) . '; array expected.' );
+ }
+
+ $this->localFileRefs += array_keys( $result['files'] );
+ $cache->set( $key, $result );
+ return $result['compiled'];
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderLESSFunctions.php b/includes/resourceloader/ResourceLoaderLESSFunctions.php
new file mode 100644
index 00000000..c7570f64
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderLESSFunctions.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * PHP-provided functions for LESS; see docs for $wgResourceLoaderLESSFunctions
+ *
+ * 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 ResourceLoaderLESSFunctions {
+ /**
+ * Check if an image file reference is suitable for embedding.
+ * An image is embeddable if it (a) exists, (b) has a suitable MIME-type,
+ * (c) does not exceed IE<9 size limit of 32kb. This is a LESS predicate
+ * function; it returns a LESS boolean value and can thus be used as a
+ * mixin guard.
+ *
+ * @par Example:
+ * @code
+ * .background-image(@url) when(embeddable(@url)) {
+ * background-image: url(@url) !ie;
+ * }
+ * @endcode
+ */
+ public static function embeddable( $frame, $less ) {
+ $base = pathinfo( $less->parser->sourceName, PATHINFO_DIRNAME );
+ $url = $frame[2][0];
+ $file = realpath( $base . '/' . $url );
+ return $less->toBool( $file
+ && strpos( $url, '//' ) === false
+ && filesize( $file ) < CSSMin::EMBED_SIZE_LIMIT
+ && CSSMin::getMimeType( $file ) !== false );
+ }
+
+ /**
+ * Convert an image URI to a base64-encoded data URI.
+ *
+ * @par Example:
+ * @code
+ * .fancy-button {
+ * background-image: embed('../images/button-bg.png');
+ * }
+ * @endcode
+ */
+ public static function embed( $frame, $less ) {
+ $base = pathinfo( $less->parser->sourceName, PATHINFO_DIRNAME );
+ $url = $frame[2][0];
+ $file = realpath( $base . '/' . $url );
+
+ $data = CSSMin::encodeImageAsDataURI( $file );
+ $less->addParsedFile( $file );
+ return 'url(' . $data . ')';
+ }
+}
diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
index c916c4a5..fa0fbf85 100644
--- a/includes/resourceloader/ResourceLoaderLanguageDataModule.php
+++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
@@ -28,6 +28,7 @@
class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
protected $language;
+ protected $targets = array( 'desktop', 'mobile' );
/**
* Get the grammar forms for the site content language.
*
@@ -47,33 +48,47 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
}
/**
+ * Get the digit groupin Pattern for the site content language.
+ *
+ * @return array
+ */
+ protected function getDigitGroupingPattern() {
+ return $this->language->digitGroupingPattern();
+ }
+
+ /**
* Get the digit transform table for the content language
- * Seperator transform table also required here to convert
- * the . and , sign to appropriate forms in content language.
*
* @return array
*/
protected function getDigitTransformTable() {
- $digitTransformTable = $this->language->digitTransformTable();
- $separatorTransformTable = $this->language->separatorTransformTable();
- if ( $digitTransformTable ) {
- array_merge( $digitTransformTable, (array)$separatorTransformTable );
- } else {
- return $separatorTransformTable;
- }
- return $digitTransformTable;
+ return $this->language->digitTransformTable();
}
/**
- * Get all the dynamic data for the content language to an array
+ * Get seperator transform table required for converting
+ * the . and , sign to appropriate forms in site content language.
+ *
+ * @return array
+ */
+ protected function getSeparatorTransformTable() {
+ return $this->language->separatorTransformTable();
+ }
+
+ /**
+ * Get all the dynamic data for the content language to an array.
+ *
+ * NOTE: Before calling this you HAVE to make sure $this->language is set.
*
* @return array
*/
protected function getData() {
return array(
'digitTransformTable' => $this->getDigitTransformTable(),
+ 'separatorTransformTable' => $this->getSeparatorTransformTable(),
'grammarForms' => $this->getSiteLangGrammarForms(),
'pluralRules' => $this->getPluralRules(),
+ 'digitGroupingPattern' => $this->getDigitGroupingPattern(),
);
}
@@ -91,26 +106,20 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
/**
* @param $context ResourceLoaderContext
- * @return array|int|Mixed
+ * @return int: UNIX timestamp
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
- $this->language = Language::factory( $context ->getLanguage() );
- $cache = wfGetCache( CACHE_ANYTHING );
- $key = wfMemcKey( 'resourceloader', 'langdatamodule', 'changeinfo' );
+ return max( 1, $this->getHashMtime( $context ) );
+ }
- $data = $this->getData();
- $hash = md5( serialize( $data ) );
+ /**
+ * @param $context ResourceLoaderContext
+ * @return string: Hash
+ */
+ public function getModifiedHash( ResourceLoaderContext $context ) {
+ $this->language = Language::factory( $context->getLanguage() );
- $result = $cache->get( $key );
- if ( is_array( $result ) && $result['hash'] === $hash ) {
- return $result['timestamp'];
- }
- $timestamp = wfTimestamp();
- $cache->set( $key, array(
- 'hash' => $hash,
- 'timestamp' => $timestamp,
- ) );
- return $timestamp;
+ return md5( serialize( $this->getData() ) );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php
index 9c49c45f..11264fc8 100644
--- a/includes/resourceloader/ResourceLoaderModule.php
+++ b/includes/resourceloader/ResourceLoaderModule.php
@@ -58,6 +58,7 @@ abstract class ResourceLoaderModule {
/* Protected Members */
protected $name = null;
+ protected $targets = array( 'desktop' );
// In-object cache for file dependencies
protected $fileDeps = array();
@@ -70,17 +71,17 @@ abstract class ResourceLoaderModule {
* Get this module's name. This is set when the module is registered
* with ResourceLoader::register()
*
- * @return Mixed: Name (string) or null if no name was set
+ * @return mixed: Name (string) or null if no name was set
*/
public function getName() {
return $this->name;
}
/**
- * Set this module's name. This is called by ResourceLodaer::register()
+ * Set this module's name. This is called by ResourceLoader::register()
* when registering the module. Other code should not call this.
*
- * @param $name String: Name
+ * @param string $name Name
*/
public function setName( $name ) {
$this->name = $name;
@@ -90,25 +91,25 @@ abstract class ResourceLoaderModule {
* 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 manuall
+ * @return int: ResourceLoaderModule class constant, the subclass default
+ * if not set manually
*/
public function getOrigin() {
return $this->origin;
}
/**
- * Set this module's origin. This is called by ResourceLodaer::register()
+ * Set this module's origin. This is called by ResourceLoader::register()
* when registering the module. Other code should not call this.
*
- * @param $origin Int origin
+ * @param int $origin origin
*/
public function setOrigin( $origin ) {
$this->origin = $origin;
}
/**
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return bool
*/
public function getFlip( $context ) {
@@ -121,8 +122,8 @@ abstract class ResourceLoaderModule {
* Get all JS for this module for a given language and skin.
* Includes all relevant JS except loader scripts.
*
- * @param $context ResourceLoaderContext: Context object
- * @return String: JavaScript code
+ * @param ResourceLoaderContext $context
+ * @return string: JavaScript code
*/
public function getScript( ResourceLoaderContext $context ) {
// Stub, override expected
@@ -140,8 +141,8 @@ abstract class ResourceLoaderModule {
* #2 is important to prevent an infinite loop, therefore this function
* MUST return either an only= URL or a non-load.php URL.
*
- * @param $context ResourceLoaderContext: Context object
- * @return Array of URLs
+ * @param ResourceLoaderContext $context
+ * @return array: Array of URLs
*/
public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
$url = ResourceLoader::makeLoaderURL(
@@ -171,8 +172,8 @@ abstract class ResourceLoaderModule {
/**
* Get all CSS for this module for a given skin.
*
- * @param $context ResourceLoaderContext: Context object
- * @return Array: List of CSS strings or array of CSS strings keyed by media type.
+ * @param ResourceLoaderContext $context
+ * @return array: List of CSS strings or array of CSS strings keyed by media type.
* like array( 'screen' => '.foo { width: 0 }' );
* or array( 'screen' => array( '.foo { width: 0 }' ) );
*/
@@ -187,8 +188,8 @@ abstract class ResourceLoaderModule {
* the module, but file-based modules will want to override this to
* load the files directly. See also getScriptURLsForDebug()
*
- * @param $context ResourceLoaderContext: Context object
- * @return Array: array( mediaType => array( URL1, URL2, ... ), ... )
+ * @param ResourceLoaderContext $context
+ * @return array: array( mediaType => array( URL1, URL2, ... ), ... )
*/
public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
$url = ResourceLoader::makeLoaderURL(
@@ -210,7 +211,7 @@ abstract class ResourceLoaderModule {
*
* To get a JSON blob with messages, use MessageBlobStore::get()
*
- * @return Array: List of message keys. Keys may occur more than once
+ * @return array: List of message keys. Keys may occur more than once
*/
public function getMessages() {
// Stub, override expected
@@ -220,7 +221,7 @@ abstract class ResourceLoaderModule {
/**
* Get the group this module is in.
*
- * @return String: Group name
+ * @return string: Group name
*/
public function getGroup() {
// Stub, override expected
@@ -230,7 +231,7 @@ abstract class ResourceLoaderModule {
/**
* Get the origin of this module. Should only be overridden for foreign modules.
*
- * @return String: Origin name, 'local' for local modules
+ * @return string: Origin name, 'local' for local modules
*/
public function getSource() {
// Stub, override expected
@@ -262,7 +263,7 @@ abstract class ResourceLoaderModule {
/**
* Get the loader JS for this module, if set.
*
- * @return Mixed: JavaScript loader code as a string or boolean false if no custom loader set
+ * @return mixed: JavaScript loader code as a string or boolean false if no custom loader set
*/
public function getLoaderScript() {
// Stub, override expected
@@ -273,16 +274,11 @@ abstract class ResourceLoaderModule {
* Get a list of modules this module depends on.
*
* Dependency information is taken into account when loading a module
- * on the client side. When adding a module on the server side,
- * dependency information is NOT taken into account and YOU are
- * responsible for adding dependent modules as well. If you don't do
- * this, the client side loader will send a second request back to the
- * server to fetch the missing modules, which kind of defeats the
- * purpose of the resource loader.
+ * on the client side.
*
* To add dependencies dynamically on the client side, use a custom
* loader script, see getLoaderScript()
- * @return Array: List of module names as strings
+ * @return array: List of module names as strings
*/
public function getDependencies() {
// Stub, override expected
@@ -290,11 +286,20 @@ abstract class ResourceLoaderModule {
}
/**
+ * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
+ *
+ * @return array: Array of strings
+ */
+ public function getTargets() {
+ return $this->targets;
+ }
+
+ /**
* Get the files this module depends on indirectly for a given skin.
* Currently these are only image files referenced by the module's CSS.
*
- * @param $skin String: Skin name
- * @return Array: List of files
+ * @param string $skin Skin name
+ * @return array: List of files
*/
public function getFileDependencies( $skin ) {
// Try in-object cache first
@@ -309,7 +314,7 @@ abstract class ResourceLoaderModule {
), __METHOD__
);
if ( !is_null( $deps ) ) {
- $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
+ $this->fileDeps[$skin] = (array)FormatJson::decode( $deps, true );
} else {
$this->fileDeps[$skin] = array();
}
@@ -319,8 +324,8 @@ abstract class ResourceLoaderModule {
/**
* Set preloaded file dependency information. Used so we can load this
* information for all modules at once.
- * @param $skin String: Skin name
- * @param $deps Array: Array of file names
+ * @param string $skin Skin name
+ * @param array $deps Array of file names
*/
public function setFileDependencies( $skin, $deps ) {
$this->fileDeps[$skin] = $deps;
@@ -329,13 +334,14 @@ abstract class ResourceLoaderModule {
/**
* Get the last modification timestamp of the message blob for this
* module in a given language.
- * @param $lang String: Language code
- * @return Integer: UNIX timestamp, or 0 if the module doesn't have messages
+ * @param string $lang Language code
+ * @return int: UNIX timestamp, or 0 if the module doesn't have messages
*/
public function getMsgBlobMtime( $lang ) {
if ( !isset( $this->msgBlobMtime[$lang] ) ) {
- if ( !count( $this->getMessages() ) )
+ if ( !count( $this->getMessages() ) ) {
return 0;
+ }
$dbr = wfGetDB( DB_SLAVE );
$msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
@@ -356,7 +362,7 @@ abstract class ResourceLoaderModule {
/**
* Set a preloaded message blob last modification timestamp. Used so we
* can load this information for all modules at once.
- * @param $lang String: Language code
+ * @param string $lang Language code
* @param $mtime Integer: UNIX timestamp or 0 if there is no such blob
*/
public function setMsgBlobMtime( $lang, $mtime ) {
@@ -375,9 +381,13 @@ abstract class ResourceLoaderModule {
* NOTE: The mtime of the module's messages is NOT automatically included.
* If you want this to happen, you'll need to call getMsgBlobMtime()
* yourself and take its result into consideration.
- *
- * @param $context ResourceLoaderContext: Context object
- * @return Integer: UNIX timestamp
+ *
+ * NOTE: The mtime of the module's hash is NOT automatically included.
+ * If your module provides a getModifiedHash() method, you'll need to call getHashMtime()
+ * yourself and take its result into consideration.
+ *
+ * @param ResourceLoaderContext $context Context object
+ * @return integer UNIX timestamp
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
// 0 would mean now
@@ -385,19 +395,59 @@ abstract class ResourceLoaderModule {
}
/**
+ * Helper method for calculating when the module's hash (if it has one) changed.
+ *
+ * @param ResourceLoaderContext $context
+ * @return integer: UNIX timestamp or 0 if there is no hash provided
+ */
+ public function getHashMtime( ResourceLoaderContext $context ) {
+ $hash = $this->getModifiedHash( $context );
+ if ( !is_string( $hash ) ) {
+ return 0;
+ }
+
+ $cache = wfGetCache( CACHE_ANYTHING );
+ $key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName() );
+
+ $data = $cache->get( $key );
+ if ( is_array( $data ) && $data['hash'] === $hash ) {
+ // Hash is still the same, re-use the timestamp of when we first saw this hash.
+ return $data['timestamp'];
+ }
+
+ $timestamp = wfTimestamp();
+ $cache->set( $key, array(
+ 'hash' => $hash,
+ 'timestamp' => $timestamp,
+ ) );
+
+ return $timestamp;
+ }
+
+ /**
+ * Get the last modification timestamp of the message blob for this
+ * module in a given language.
+ *
+ * @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 $context ResourceLoaderContext: Context object
- * @return Boolean
+ * @param ResourceLoaderContext $context
+ * @return bool
*/
public function isKnownEmpty( ResourceLoaderContext $context ) {
return false;
}
-
/** @var JSParser lazy-initialized; use self::javaScriptParser() */
private static $jsParser;
private static $parseCacheVersion = 1;
@@ -408,7 +458,7 @@ abstract class ResourceLoaderModule {
*
* @param string $fileName
* @param string $contents
- * @return string JS with the original, or a replacement error
+ * @return string: JS with the original, or a replacement error
*/
protected function validateScriptFile( $fileName, $contents ) {
global $wgResourceLoaderValidateJS;
@@ -426,10 +476,10 @@ abstract class ResourceLoaderModule {
try {
$parser->parse( $contents, $fileName, 1 );
$result = $contents;
- } catch (Exception $e) {
+ } catch ( Exception $e ) {
// We'll save this to cache to avoid having to validate broken JS over and over...
$err = $e->getMessage();
- $result = "throw new Error(" . Xml::encodeJsVar("JavaScript parse error: $err") . ");";
+ $result = "throw new Error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
}
$cache->set( $key, $result );
@@ -449,4 +499,20 @@ abstract class ResourceLoaderModule {
return self::$jsParser;
}
+ /**
+ * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist
+ * but returns 1 instead.
+ * @param string $filename File name
+ * @return int UNIX timestamp, or 1 if the file doesn't exist
+ */
+ protected static function safeFilemtime( $filename ) {
+ if ( file_exists( $filename ) ) {
+ return filemtime( $filename );
+ } else {
+ // We only ever map this function on an array if we're gonna call max() after,
+ // so return our standard minimum timestamps here. This is 1, not 0, because
+ // wfTimestamp(0) == NOW
+ return 1;
+ }
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderNoscriptModule.php b/includes/resourceloader/ResourceLoaderNoscriptModule.php
index 8e81c8d9..bd026f3f 100644
--- a/includes/resourceloader/ResourceLoaderNoscriptModule.php
+++ b/includes/resourceloader/ResourceLoaderNoscriptModule.php
@@ -45,7 +45,7 @@ class ResourceLoaderNoscriptModule extends ResourceLoaderWikiModule {
/**
* Gets group name
- *
+ *
* @return String: Name of group
*/
public function getGroup() {
diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php
index 03fe1fe5..05754d37 100644
--- a/includes/resourceloader/ResourceLoaderSiteModule.php
+++ b/includes/resourceloader/ResourceLoaderSiteModule.php
@@ -37,20 +37,19 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
* @return Array: List of pages
*/
protected function getPages( ResourceLoaderContext $context ) {
- global $wgHandheldStyle;
+ global $wgUseSiteJs, $wgUseSiteCss;
- $pages = array(
- 'MediaWiki:Common.js' => array( 'type' => 'script' ),
- 'MediaWiki:Common.css' => array( 'type' => 'style' ),
- 'MediaWiki:' . ucfirst( $context->getSkin() ) . '.js' => array( 'type' => 'script' ),
- 'MediaWiki:' . ucfirst( $context->getSkin() ) . '.css' => array( 'type' => 'style' ),
- 'MediaWiki:Print.css' => array( 'type' => 'style', 'media' => 'print' ),
- );
- if ( $wgHandheldStyle ) {
- $pages['MediaWiki:Handheld.css'] = array(
- 'type' => 'style',
- 'media' => 'handheld' );
+ $pages = array();
+ if ( $wgUseSiteJs ) {
+ $pages['MediaWiki:Common.js'] = array( 'type' => 'script' );
+ $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.js'] = array( 'type' => 'script' );
}
+ if ( $wgUseSiteCss ) {
+ $pages['MediaWiki:Common.css'] = array( 'type' => 'style' );
+ $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.css'] = array( 'type' => 'style' );
+
+ }
+ $pages['MediaWiki:Print.css'] = array( 'type' => 'style', 'media' => 'print' );
return $pages;
}
@@ -58,7 +57,7 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
/**
* Gets group name
- *
+ *
* @return String: Name of group
*/
public function getGroup() {
diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php
index 20ee83f9..20f6e0ba 100644
--- a/includes/resourceloader/ResourceLoaderStartUpModule.php
+++ b/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -27,6 +27,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
/* Protected Members */
protected $modifiedTime = array();
+ protected $targets = array( 'desktop', 'mobile' );
/* Protected Methods */
@@ -51,7 +52,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
*/
$namespaceIds = $wgContLang->getNamespaceIds();
$caseSensitiveNamespaces = array();
- foreach( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
+ foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
$namespaceIds[$wgContLang->lc( $name )] = $index;
if ( !MWNamespace::isCapitalized( $index ) ) {
$caseSensitiveNamespaces[] = $index;
@@ -83,7 +84,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
'wgNamespaceIds' => $namespaceIds,
'wgSiteName' => $wgSitename,
- 'wgFileExtensions' => array_values( $wgFileExtensions ),
+ 'wgFileExtensions' => array_values( array_unique( $wgFileExtensions ) ),
'wgDBname' => $wgDBname,
// This sucks, it is only needed on Special:Upload, but I could
// not find a way to add vars only for a certain module
@@ -94,6 +95,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgCookiePrefix' => $wgCookiePrefix,
'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength,
'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
+ 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
);
wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) );
@@ -114,6 +116,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
$out = '';
$registrations = array();
$resourceLoader = $context->getResourceLoader();
+ $target = $context->getRequest()->getVal( 'target', 'desktop' );
// Register sources
$out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
@@ -121,6 +124,10 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
// Register modules
foreach ( $resourceLoader->getModuleNames() as $name ) {
$module = $resourceLoader->getModule( $name );
+ $moduleTargets = $module->getTargets();
+ if ( !in_array( $target, $moduleTargets ) ) {
+ continue;
+ }
$deps = $module->getDependencies();
$group = $module->getGroup();
$source = $module->getSource();
@@ -130,33 +137,33 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
$version = wfTimestamp( TS_ISO_8601_BASIC,
$module->getModifiedTime( $context ) );
$out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $source, $loader );
+ continue;
}
+
// Automatically register module
+ // getModifiedTime() is supposed to return a UNIX timestamp, but it doesn't always
+ // seem to do that, and custom implementations might forget. Coerce it to TS_UNIX
+ $moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) );
+ $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
+ // Modules without dependencies, a group or a foreign source pass two arguments (name, timestamp) to
+ // mw.loader.register()
+ if ( !count( $deps ) && $group === null && $source === 'local' ) {
+ $registrations[] = array( $name, $mtime );
+ }
+ // Modules with dependencies but no group or foreign source pass three arguments
+ // (name, timestamp, dependencies) to mw.loader.register()
+ elseif ( $group === null && $source === 'local' ) {
+ $registrations[] = array( $name, $mtime, $deps );
+ }
+ // Modules with a group but no foreign source pass four arguments (name, timestamp, dependencies, group)
+ // to mw.loader.register()
+ elseif ( $source === 'local' ) {
+ $registrations[] = array( $name, $mtime, $deps, $group );
+ }
+ // Modules with a foreign source pass five arguments (name, timestamp, dependencies, group, source)
+ // to mw.loader.register()
else {
- // getModifiedTime() is supposed to return a UNIX timestamp, but it doesn't always
- // seem to do that, and custom implementations might forget. Coerce it to TS_UNIX
- $moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) );
- $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
- // Modules without dependencies, a group or a foreign source pass two arguments (name, timestamp) to
- // mw.loader.register()
- if ( !count( $deps ) && $group === null && $source === 'local' ) {
- $registrations[] = array( $name, $mtime );
- }
- // Modules with dependencies but no group or foreign source pass three arguments
- // (name, timestamp, dependencies) to mw.loader.register()
- elseif ( $group === null && $source === 'local' ) {
- $registrations[] = array( $name, $mtime, $deps );
- }
- // Modules with a group but no foreign source pass four arguments (name, timestamp, dependencies, group)
- // to mw.loader.register()
- elseif ( $source === 'local' ) {
- $registrations[] = array( $name, $mtime, $deps, $group );
- }
- // Modules with a foreign source pass five arguments (name, timestamp, dependencies, group, source)
- // to mw.loader.register()
- else {
- $registrations[] = array( $name, $mtime, $deps, $group, $source );
- }
+ $registrations[] = array( $name, $mtime, $deps, $group, $source );
}
}
$out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
@@ -179,7 +186,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
* @return string
*/
public function getScript( ResourceLoaderContext $context ) {
- global $IP, $wgLoadScript, $wgLegacyJavaScriptGlobals;
+ global $IP, $wgLegacyJavaScriptGlobals;
$out = file_get_contents( "$IP/resources/startup.js" );
if ( $context->getOnly() === 'scripts' ) {
@@ -219,7 +226,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
"};\n";
// Conditional script injection
- $scriptTag = Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) );
+ $scriptTag = Html::linkedScript( wfAppendQuery( wfScript( 'load' ), $query ) );
$out .= "if ( isCompatible() ) {\n" .
"\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) .
"}\n" .
diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
index 0e95d964..bda86539 100644
--- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
@@ -48,7 +48,7 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
global $wgUser;
return $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() );
}
-
+
/**
* @param $context ResourceLoaderContext
* @return array
@@ -56,43 +56,44 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
public function getStyles( ResourceLoaderContext $context ) {
global $wgAllowUserCssPrefs, $wgUser;
- if ( $wgAllowUserCssPrefs ) {
- $options = $wgUser->getOptions();
+ if ( !$wgAllowUserCssPrefs ) {
+ return array();
+ }
- // Build CSS rules
- $rules = array();
+ $options = $wgUser->getOptions();
- // Underline: 2 = browser default, 1 = always, 0 = never
- if ( $options['underline'] < 2 ) {
- $rules[] = "a { text-decoration: " .
- ( $options['underline'] ? 'underline' : 'none' ) . "; }";
- } else {
- # The scripts of these languages are very hard to read with underlines
- $rules[] = 'a:lang(ar), a:lang(ckb), a:lang(fa),a:lang(kk-arab), ' .
- 'a:lang(mzn), a:lang(ps), a:lang(ur) { text-decoration: none; }';
- }
- if ( $options['justify'] ) {
- $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
- }
- if ( !$options['showtoc'] ) {
- $rules[] = "#toc { display: none; }\n";
- }
- if ( !$options['editsection'] ) {
- $rules[] = ".editsection { display: none; }\n";
- }
- if ( $options['editfont'] !== 'default' ) {
- // Double-check that $options['editfont'] consists of safe characters only
- if ( preg_match( '/^[a-zA-Z0-9_, -]+$/', $options['editfont'] ) ) {
- $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
- }
- }
- $style = implode( "\n", $rules );
- if ( $this->getFlip( $context ) ) {
- $style = CSSJanus::transform( $style, true, false );
+ // Build CSS rules
+ $rules = array();
+
+ // Underline: 2 = browser default, 1 = always, 0 = never
+ if ( $options['underline'] < 2 ) {
+ $rules[] = "a { text-decoration: " .
+ ( $options['underline'] ? 'underline' : 'none' ) . "; }";
+ } else {
+ # The scripts of these languages are very hard to read with underlines
+ $rules[] = 'a:lang(ar), a:lang(ckb), a:lang(kk-arab), ' .
+ 'a:lang(mzn), a:lang(ps), a:lang(ur) { text-decoration: none; }';
+ }
+ if ( $options['justify'] ) {
+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
+ }
+ if ( !$options['showtoc'] ) {
+ $rules[] = "#toc { display: none; }\n";
+ }
+ if ( !$options['editsection'] ) {
+ $rules[] = ".mw-editsection { display: none; }\n";
+ }
+ if ( $options['editfont'] !== 'default' ) {
+ // Double-check that $options['editfont'] consists of safe characters only
+ if ( preg_match( '/^[a-zA-Z0-9_, -]+$/', $options['editfont'] ) ) {
+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
}
- return array( 'all' => $style );
}
- return array();
+ $style = implode( "\n", $rules );
+ if ( $this->getFlip( $context ) ) {
+ $style = CSSJanus::transform( $style, true, false );
+ }
+ return array( 'all' => $style );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/includes/resourceloader/ResourceLoaderUserGroupsModule.php
index 1316f423..9064263f 100644
--- a/includes/resourceloader/ResourceLoaderUserGroupsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserGroupsModule.php
@@ -33,12 +33,15 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
* @return array
*/
protected function getPages( ResourceLoaderContext $context ) {
- global $wgUser;
+ global $wgUser, $wgUseSiteJs, $wgUseSiteCss;
$userName = $context->getUser();
if ( $userName === null ) {
return array();
}
+ if ( !$wgUseSiteJs && !$wgUseSiteCss ) {
+ return array();
+ }
// Use $wgUser is possible; allows to skip a lot of code
if ( is_object( $wgUser ) && $wgUser->getName() == $userName ) {
@@ -51,12 +54,16 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule {
}
$pages = array();
- foreach( $user->getEffectiveGroups() as $group ) {
+ foreach ( $user->getEffectiveGroups() as $group ) {
if ( in_array( $group, array( '*', 'user' ) ) ) {
continue;
}
- $pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' );
- $pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' );
+ if ( $wgUseSiteJs ) {
+ $pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' );
+ }
+ if ( $wgUseSiteCss ) {
+ $pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' );
+ }
}
return $pages;
}
diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php
index 177302c5..7a04e473 100644
--- a/includes/resourceloader/ResourceLoaderUserModule.php
+++ b/includes/resourceloader/ResourceLoaderUserModule.php
@@ -35,11 +35,15 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
* @return array
*/
protected function getPages( ResourceLoaderContext $context ) {
+ global $wgAllowUserJs, $wgAllowUserCss;
$username = $context->getUser();
if ( $username === null ) {
return array();
}
+ if ( !$wgAllowUserJs && !$wgAllowUserCss ) {
+ return array();
+ }
// Get the normalized title of the user's user page
$userpageTitle = Title::makeTitleSafe( NS_USER, $username );
@@ -50,14 +54,15 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
$userpage = $userpageTitle->getPrefixedDBkey(); // Needed so $excludepages works
- $pages = array(
- "$userpage/common.js" => array( 'type' => 'script' ),
- "$userpage/" . $context->getSkin() . '.js' =>
- array( 'type' => 'script' ),
- "$userpage/common.css" => array( 'type' => 'style' ),
- "$userpage/" . $context->getSkin() . '.css' =>
- array( 'type' => 'style' ),
- );
+ $pages = array();
+ if ( $wgAllowUserJs ) {
+ $pages["$userpage/common.js"] = array( 'type' => 'script' );
+ $pages["$userpage/" . $context->getSkin() . '.js'] = array( 'type' => 'script' );
+ }
+ if ( $wgAllowUserCss ) {
+ $pages["$userpage/common.css"] = array( 'type' => 'style' );
+ $pages["$userpage/" . $context->getSkin() . '.css'] = array( 'type' => 'style' );
+ }
// Hack for bug 26283: if we're on a preview page for a CSS/JS page,
// we need to exclude that page from this module. In that case, the excludepage
diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
index 4624cbce..0b7e1964 100644
--- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -56,7 +56,9 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
public function getScript( ResourceLoaderContext $context ) {
global $wgUser;
return Xml::encodeJsCall( 'mw.user.options.set',
- array( $wgUser->getOptions() ) );
+ array( $wgUser->getOptions() ),
+ ResourceLoader::inDebugMode()
+ );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php
index 62d096a6..92ebbe93 100644
--- a/includes/resourceloader/ResourceLoaderUserTokensModule.php
+++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php
@@ -35,15 +35,15 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
/**
* Fetch the tokens for the current user.
*
- * @param $context ResourceLoaderContext: Context object
- * @return Array: List of tokens keyed by token type
+ * @return array: List of tokens keyed by token type
*/
- protected function contextUserTokens( ResourceLoaderContext $context ) {
+ protected function contextUserTokens() {
global $wgUser;
return array(
'editToken' => $wgUser->getEditToken(),
- 'watchToken' => ApiQueryInfo::getWatchToken(null, null),
+ 'patrolToken' => ApiQueryRecentChanges::getPatrolToken( null, null ),
+ 'watchToken' => ApiQueryInfo::getWatchToken( null, null ),
);
}
@@ -53,7 +53,9 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule {
*/
public function getScript( ResourceLoaderContext $context ) {
return Xml::encodeJsCall( 'mw.user.tokens.set',
- array( $this->contextUserTokens( $context ) ) );
+ array( $this->contextUserTokens() ),
+ ResourceLoader::inDebugMode()
+ );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php
index ee8dd1e5..3f10ae53 100644
--- a/includes/resourceloader/ResourceLoaderWikiModule.php
+++ b/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -42,7 +42,20 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
/* Abstract Protected Methods */
/**
+ * 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 $context ResourceLoaderContext
+ * @return array
*/
abstract protected function getPages( ResourceLoaderContext $context );
@@ -75,7 +88,22 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
if ( !$revision ) {
return null;
}
- return $revision->getRawText();
+
+ $content = $revision->getContent( Revision::RAW );
+
+ if ( !$content ) {
+ wfDebugLog( 'resourceloader', __METHOD__ . ': failed to load content of JS/CSS page!' );
+ return null;
+ }
+
+ $model = $content->getModel();
+
+ if ( $model !== CONTENT_MODEL_CSS && $model !== CONTENT_MODEL_JAVASCRIPT ) {
+ wfDebugLog( 'resourceloader', __METHOD__ . ': bad content model $model for JS/CSS page!' );
+ return null;
+ }
+
+ return $content->getNativeData(); //NOTE: this is safe, we know it's JS or CSS
}
/* Methods */
@@ -98,7 +126,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
if ( strval( $script ) !== '' ) {
$script = $this->validateScriptFile( $titleText, $script );
if ( strpos( $titleText, '*/' ) === false ) {
- $scripts .= "/* $titleText */\n";
+ $scripts .= "/* $titleText */\n";
}
$scripts .= $script . "\n";
}
@@ -119,7 +147,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
continue;
}
$title = Title::newFromText( $titleText );
- if ( !$title || $title->isRedirect() ) {
+ if ( !$title || $title->isRedirect() ) {
continue;
}
$media = isset( $options['media'] ) ? $options['media'] : 'all';
@@ -135,7 +163,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
$styles[$media] = array();
}
if ( strpos( $titleText, '*/' ) === false ) {
- $style = "/* $titleText */\n" . $style;
+ $style = "/* $titleText */\n" . $style;
}
$styles[$media][] = $style;
}