diff options
Diffstat (limited to 'includes/resourceloader/ResourceLoader.php')
-rw-r--r-- | includes/resourceloader/ResourceLoader.php | 553 |
1 files changed, 295 insertions, 258 deletions
diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 150ccd07..c8ece147 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -22,13 +22,18 @@ * @author Trevor Parscal */ +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use WrappedString\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 { +class ResourceLoader implements LoggerAwareInterface { /** @var int */ protected static $filterCacheVersion = 7; @@ -78,6 +83,11 @@ class ResourceLoader { protected $blobStore; /** + * @var LoggerInterface + */ + private $logger; + + /** * Load information stored in the database about modules. * * This method grabs modules dependencies from the database and updates modules @@ -169,74 +179,98 @@ class ResourceLoader { * * @param string $filter Name of filter to run * @param string $data Text to filter, such as JavaScript or CSS text - * @param string $cacheReport Whether to include the cache key report + * @param array $options For back-compat, can also be the boolean value for "cacheReport". Keys: + * - (bool) cache: Whether to allow caching this data. Default: true. + * - (bool) cacheReport: Whether to include the "cache key" report comment. Default: false. * @return string Filtered data, or a comment containing an error message */ - public function filter( $filter, $data, $cacheReport = true ) { + public function filter( $filter, $data, $options = array() ) { + // Back-compat + if ( is_bool( $options ) ) { + $options = array( 'cacheReport' => $options ); + } + // Defaults + $options += array( 'cache' => true, 'cacheReport' => false ); + $stats = RequestContext::getMain()->getStats(); - // For empty/whitespace-only data or for unknown filters, don't perform - // any caching or processing - if ( trim( $data ) === '' || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) { + // Don't filter empty content + if ( trim( $data ) === '' ) { return $data; } - // Try for cache hit - // Use CACHE_ANYTHING since filtering is very slow compared to DB queries - $key = wfMemcKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) ); - $cache = wfGetCache( CACHE_ANYTHING ); - $cacheEntry = $cache->get( $key ); - if ( is_string( $cacheEntry ) ) { - wfIncrStats( "rl-$filter-cache-hits" ); - return $cacheEntry; + if ( !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) { + $this->logger->warning( 'Invalid filter {filter}', array( + 'filter' => $filter + ) ); + return $data; } - $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, - $this->config->get( 'ResourceLoaderMinifierStatementsOnOwnLine' ), - $this->config->get( 'ResourceLoaderMinifierMaxLineLength' ) - ); - if ( $cacheReport ) { - $result .= "\n/* cache key: $key */"; - } - break; - case 'minify-css': - $result = CSSMin::minify( $data ); - if ( $cacheReport ) { - $result .= "\n/* cache key: $key */"; - } - break; + if ( !$options['cache'] ) { + $result = self::applyFilter( $filter, $data, $this->config ); + } else { + $key = wfGlobalCacheKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) ); + $cache = ObjectCache::newAccelerator( CACHE_ANYTHING ); + $cacheEntry = $cache->get( $key ); + if ( is_string( $cacheEntry ) ) { + $stats->increment( "resourceloader_cache.$filter.hit" ); + return $cacheEntry; + } + $result = ''; + try { + $statStart = microtime( true ); + $result = self::applyFilter( $filter, $data, $this->config ); + $statTiming = microtime( true ) - $statStart; + $stats->increment( "resourceloader_cache.$filter.miss" ); + $stats->timing( "resourceloader_cache.$filter.timing", 1000 * $statTiming ); + if ( $options['cacheReport'] ) { + $result .= "\n/* cache key: $key */"; + } + // Set a TTL since HHVM's APC doesn't have any limitation or eviction logic. + $cache->set( $key, $result, 24 * 3600 ); + } catch ( Exception $e ) { + MWExceptionHandler::logException( $e ); + $this->logger->warning( 'Minification failed: {exception}', array( + 'exception' => $e + ) ); + $this->errors[] = self::formatExceptionNoComment( $e ); } - - // Save filtered text to Memcached - $cache->set( $key, $result ); - } catch ( Exception $e ) { - MWExceptionHandler::logException( $e ); - wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" ); - $this->errors[] = self::formatExceptionNoComment( $e ); } return $result; } + private static function applyFilter( $filter, $data, Config $config ) { + switch ( $filter ) { + case 'minify-js': + return JavaScriptMinifier::minify( $data, + $config->get( 'ResourceLoaderMinifierStatementsOnOwnLine' ), + $config->get( 'ResourceLoaderMinifierMaxLineLength' ) + ); + case 'minify-css': + return CSSMin::minify( $data ); + } + + return $data; + } + /* Methods */ /** * Register core modules and runs registration hooks. * @param Config|null $config */ - public function __construct( Config $config = null ) { + public function __construct( Config $config = null, LoggerInterface $logger = null ) { global $IP; - if ( $config === null ) { - wfDebug( __METHOD__ . ' was called without providing a Config instance' ); - $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); + if ( !$logger ) { + $logger = new NullLogger(); } + $this->setLogger( $logger ); + if ( !$config ) { + $this->logger->debug( __METHOD__ . ' was called without providing a Config instance' ); + $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); + } $this->config = $config; // Add 'local' source first @@ -247,9 +281,10 @@ class ResourceLoader { // Register core modules $this->register( include "$IP/resources/Resources.php" ); + $this->register( include "$IP/resources/ResourcesOOUI.php" ); // Register extension modules - Hooks::run( 'ResourceLoaderRegisterModules', array( &$this ) ); $this->register( $config->get( 'ResourceModules' ) ); + Hooks::run( 'ResourceLoaderRegisterModules', array( &$this ) ); if ( $config->get( 'EnableJavaScriptTest' ) === true ) { $this->registerTestModules(); @@ -265,9 +300,21 @@ class ResourceLoader { return $this->config; } + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * @since 1.26 + * @return MessageBlobStore + */ + public function getMessageBlobStore() { + return $this->blobStore; + } + /** - * @param MessageBlobStore $blobStore * @since 1.25 + * @param MessageBlobStore $blobStore */ public function setMessageBlobStore( MessageBlobStore $blobStore ) { $this->blobStore = $blobStore; @@ -566,19 +613,44 @@ class ResourceLoader { } /** + * @since 1.26 + * @param string $value + * @return string Hash + */ + public static function makeHash( $value ) { + // Use base64 to output more entropy in a more compact string (default hex is only base16). + // The first 8 chars of a base64 encoded digest represent the same binary as + // the first 12 chars of a hex encoded digest. + return substr( base64_encode( sha1( $value, true ) ), 0, 8 ); + } + + /** + * Helper method to get and combine versions of multiple modules. + * + * @since 1.26 + * @param ResourceLoaderContext $context + * @param array $modules List of ResourceLoaderModule objects + * @return string Hash + */ + public function getCombinedVersion( ResourceLoaderContext $context, Array $modules ) { + if ( !$modules ) { + return ''; + } + // Support: PHP 5.3 ("$this" for anonymous functions was added in PHP 5.4.0) + // http://php.net/functions.anonymous + $rl = $this; + $hashes = array_map( function ( $module ) use ( $rl, $context ) { + return $rl->getModule( $module )->getVersionHash( $context ); + }, $modules ); + return self::makeHash( implode( $hashes ) ); + } + + /** * 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 ) { - // Use file cache if enabled and available... - if ( $this->config->get( 'UseFileCache' ) ) { - $fileCache = ResourceFileCache::newFromContext( $context ); - if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) { - return; // output handled - } - } - // 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 @@ -597,7 +669,7 @@ 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' ) { - wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" ); + $this->logger->debug( "Request for private module '$name' denied" ); $this->errors[] = "Cannot show private module \"$name\""; continue; } @@ -607,37 +679,46 @@ class ResourceLoader { } } - // Preload information needed to the mtime calculation below try { + // Preload for getCombinedVersion() $this->preloadModuleInfo( array_keys( $modules ), $context ); } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); - wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" ); + $this->logger->warning( 'Preloading module info failed: {exception}', array( + 'exception' => $e + ) ); $this->errors[] = self::formatExceptionNoComment( $e ); } - // To send Last-Modified and support If-Modified-Since, we need to detect - // the last modified time - $mtime = wfTimestamp( TS_UNIX, $this->config->get( 'CacheEpoch' ) ); - foreach ( $modules as $module ) { - /** - * @var $module ResourceLoaderModule - */ - try { - // Calculate maximum modified time - $mtime = max( $mtime, $module->getModifiedTime( $context ) ); - } catch ( Exception $e ) { - MWExceptionHandler::logException( $e ); - wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" ); - $this->errors[] = self::formatExceptionNoComment( $e ); - } + // Combine versions to propagate cache invalidation + $versionHash = ''; + try { + $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) ); + } catch ( Exception $e ) { + MWExceptionHandler::logException( $e ); + $this->logger->warning( 'Calculating version hash failed: {exception}', array( + 'exception' => $e + ) ); + $this->errors[] = self::formatExceptionNoComment( $e ); } - // If there's an If-Modified-Since header, respond with a 304 appropriately - if ( $this->tryRespondLastModified( $context, $mtime ) ) { + // See RFC 2616 § 3.11 Entity Tags + // http://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 ); @@ -659,26 +740,25 @@ class ResourceLoader { } } - // Send content type and cache related headers - $this->sendResponseHeaders( $context, $mtime, (bool)$this->errors ); + $this->sendResponseHeaders( $context, $etag, (bool)$this->errors ); // 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. - $errorText = ''; - foreach ( $this->errors as $error ) { - $errorText .= $error . "\n"; - } - $response = $errorText; + $response = implode( "\n\n", $this->errors ); } elseif ( $this->errors ) { - // Prepend comments indicating errors - $errorText = ''; - foreach ( $this->errors as $error ) { - $errorText .= self::makeComment( $error ); + $errorText = implode( "\n\n", $this->errors ); + $errorResponse = self::makeComment( $errorText ); + if ( $context->shouldIncludeScripts() ) { + $errorResponse .= 'if (window.console && console.error) {' + . Xml::encodeJsCall( 'console.error', array( $errorText ) ) + . "}\n"; } - $response = $errorText . $response; + + // Prepend error info to the response + $response = $errorResponse . $response; } $this->errors = array(); @@ -687,13 +767,16 @@ class ResourceLoader { } /** - * Send content type and last modified headers to the client. + * Send main response headers to the client. + * + * Deals with Content-Type, CORS (for stylesheets), and caching. + * * @param ResourceLoaderContext $context - * @param string $mtime TS_MW timestamp to use for last-modified + * @param string $etag ETag header value * @param bool $errors Whether there are errors in the response * @return void */ - protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) { + protected function sendResponseHeaders( ResourceLoaderContext $context, $etag, $errors ) { $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); // If a version wasn't specified we need a shorter expiry time for updates // to propagate to clients quickly @@ -720,7 +803,9 @@ class ResourceLoader { } else { header( 'Content-Type: text/javascript; charset=utf-8' ); } - header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) ); + // See RFC 2616 § 14.19 ETag + // http://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' ); @@ -733,39 +818,36 @@ class ResourceLoader { } /** - * Respond with 304 Last Modified if appropiate. + * Respond with HTTP 304 Not Modified if appropiate. * - * If there's an If-Modified-Since header, respond with a 304 appropriately + * 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 $mtime The TS_MW timestamp to check the header against - * @return bool True if 304 header sent and output handled + * @param string $etag ETag header value + * @return bool True if HTTP 304 was sent and output handled */ - protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) { - // If there's an If-Modified-Since header, respond with a 304 appropriately - // Some clients send "timestamp;length=123". Strip the part after the first ';' - // so we get a valid timestamp. - $ims = $context->getRequest()->getHeader( 'If-Modified-Since' ); + protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) { + // See RFC 2616 § 14.26 If-None-Match + // http://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 ( $ims !== false && !$context->getDebug() ) { - $imsTS = strtok( $ims, ';' ); - if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) { - // 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 http://bugs.php.net/bug.php?id=51579 - // To work around this, we tear down all output buffering before - // sending the 304. - wfResetOutputBuffers( /* $resetGzipEncoding = */ true ); - - header( 'HTTP/1.0 304 Not Modified' ); - header( 'Status: 304 Not Modified' ); - return true; - } + 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 http://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; } @@ -775,10 +857,13 @@ class ResourceLoader { * * @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 + ResourceFileCache $fileCache, + ResourceLoaderContext $context, + $etag ) { $rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); // Buffer output to catch warnings. @@ -799,16 +884,12 @@ class ResourceLoader { if ( $good ) { $ts = $fileCache->cacheTimestamp(); // Send content type and cache headers - $this->sendResponseHeaders( $context, $ts, false ); - // If there's an If-Modified-Since header, respond with a 304 appropriately - if ( $this->tryRespondLastModified( $context, $ts ) ) { - return false; // output handled (buffers cleared) - } + $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() && strlen( $warnings = ob_get_contents() ) ) { - $response = "/*\n$warnings\n*/\n" . $response; + $response = self::makeComment( $warnings ) . $response; } // Remove the output buffer and output the response ob_end_clean(); @@ -854,11 +935,11 @@ class ResourceLoader { protected static function formatExceptionNoComment( $e ) { global $wgShowExceptionDetails; - if ( $wgShowExceptionDetails ) { - return $e->__toString(); - } else { - return wfMessage( 'internalerror' )->text(); + if ( !$wgShowExceptionDetails ) { + return 'Internal error'; } + + return $e->__toString(); } /** @@ -896,17 +977,14 @@ MESSAGE; // Pre-fetch blobs if ( $context->shouldIncludeMessages() ) { try { - $blobs = $this->blobStore->get( $this, $modules, $context->getLanguage() ); + $this->blobStore->get( $this, $modules, $context->getLanguage() ); } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); - wfDebugLog( - 'resourceloader', - __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" - ); + $this->logger->warning( 'Prefetching MessageBlobStore failed: {exception}', array( + 'exception' => $e + ) ); $this->errors[] = self::formatExceptionNoComment( $e ); } - } else { - $blobs = array(); } foreach ( $missing as $name ) { @@ -916,116 +994,43 @@ MESSAGE; // Generate output $isRaw = false; foreach ( $modules as $name => $module ) { - /** - * @var $module ResourceLoaderModule - */ - try { - $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() && $module->supportsURLLoading() ) { - $scripts = $module->getScriptURLsForDebug( $context ); - } else { - $scripts = $module->getScript( $context ); - // rtrim() because there are usually a few line breaks - // after the last ';'. A new line at EOF, a new line - // added by ResourceLoaderFileModule::readScriptFiles, etc. - if ( is_string( $scripts ) - && strlen( $scripts ) - && substr( rtrim( $scripts ), -1 ) !== ';' - ) { - // Append semicolon to prevent weird bugs caused by files not - // terminating their statements right (bug 27054) - $scripts .= ";\n"; - } - } - } - // Styles - $styles = array(); - if ( $context->shouldIncludeStyles() ) { - // Don't create empty stylesheets like array( '' => '' ) for modules - // that don't *have* any stylesheets (bug 38024). - $stylePairs = $module->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() && $module->supportsURLLoading() ) { - $styles = array( - 'url' => $module->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] = array(); - foreach ( $style as $cssText ) { - if ( is_string( $cssText ) ) { - $stylePairs[$media][] = $this->filter( 'minify-css', $cssText ); - } - } - } elseif ( is_string( $style ) ) { - $stylePairs[$media] = $this->filter( 'minify-css', $style ); - } - } - } - // Wrap styles into @media groups as needed and flatten into a numerical array - $styles = array( - 'css' => self::makeCombinedStyles( $stylePairs ) - ); - } - } - } - - // Messages - $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}'; + $content = $module->getModuleContent( $context ); // Append output switch ( $context->getOnly() ) { case 'scripts': + $scripts = $content['scripts']; if ( is_string( $scripts ) ) { // Load scripts raw... $out .= $scripts; } elseif ( is_array( $scripts ) ) { // ...except when $scripts is an array of URLs - $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() ); + $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array(), array() ); } 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. $out .= isset( $styles['css'] ) ? implode( '', $styles['css'] ) : ''; break; - case 'messages': - $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) ); - break; - case 'templates': - $out .= Xml::encodeJsCall( - 'mw.templates.set', - array( $name, (object)$module->getTemplates() ), - ResourceLoader::inDebugMode() - ); - break; default: $out .= self::makeLoaderImplementScript( $name, - $scripts, - $styles, - new XmlJsCode( $messagesBlob ), - $module->getTemplates() + isset( $content['scripts'] ) ? $content['scripts'] : '', + isset( $content['styles'] ) ? $content['styles'] : array(), + isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : array(), + isset( $content['templates'] ) ? $content['templates'] : array() ); break; } } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); - wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" ); + $this->logger->warning( 'Generating module package failed: {exception}', array( + 'exception' => $e + ) ); $this->errors[] = self::formatExceptionNoComment( $e ); // Respond to client with error-state instead of module implementation @@ -1056,11 +1061,19 @@ MESSAGE; } } + $enableFilterCache = true; + if ( count( $modules ) === 1 && reset( $modules ) instanceof ResourceLoaderUserTokensModule ) { + // If we're building the embedded user.tokens, don't cache (T84960) + $enableFilterCache = false; + } + if ( !$context->getDebug() ) { if ( $context->getOnly() === 'styles' ) { $out = $this->filter( 'minify-css', $out ); } else { - $out = $this->filter( 'minify-js', $out ); + $out = $this->filter( 'minify-js', $out, array( + 'cache' => $enableFilterCache + ) ); } } @@ -1084,11 +1097,22 @@ MESSAGE; * @throws MWException * @return string */ - public static function makeLoaderImplementScript( $name, $scripts, $styles, - $messages, $templates + public static function makeLoaderImplementScript( + $name, $scripts, $styles, $messages, $templates ) { if ( is_string( $scripts ) ) { - $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" ); + // Site and user module are a legacy scripts that run in the global scope (no closure). + // Transportation as string instructs mw.loader.implement to use globalEval. + if ( $name === 'site' || $name === 'user' ) { + // Minify manually because the general makeModuleResponse() minification won't be + // effective here due to the script being a string instead of a function. (T107377) + if ( !ResourceLoader::inDebugMode() ) { + $scripts = self::applyFilter( 'minify-js', $scripts, + ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) ); + } + } else { + $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" ); + } } elseif ( !is_array( $scripts ) ) { throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' ); } @@ -1098,9 +1122,9 @@ MESSAGE; $module = array( $name, $scripts, - (object) $styles, - (object) $messages, - (object) $templates, + (object)$styles, + (object)$messages, + (object)$templates, ); self::trimArray( $module ); @@ -1193,7 +1217,7 @@ MESSAGE; * and $group as supplied. * * @param string $name Module name - * @param int $version Module version number as a timestamp + * @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. @@ -1265,7 +1289,7 @@ MESSAGE; * Registers modules with the given names and parameters. * * @param string $name Module name - * @param int $version Module version number as a timestamp + * @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 @@ -1346,11 +1370,30 @@ MESSAGE; * Returns JS code which runs given JS code if the client-side framework is * present. * + * @deprecated since 1.25; use makeInlineScript instead * @param string $script JavaScript code * @return string */ public static function makeLoaderConditionalScript( $script ) { - return "if(window.mw){\n" . trim( $script ) . "\n}"; + return "window.RLQ = window.RLQ || []; window.RLQ.push( function () {\n" . trim( $script ) . "\n} );"; + } + + /** + * Construct an inline script tag with given JS code. + * + * The code will be wrapped in a closure, and it will be executed by ResourceLoader + * only 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 || []; window.RLQ.push( function () {\n", + "\n} );</script>" + ); } /** @@ -1361,11 +1404,13 @@ MESSAGE; * @return string */ public static function makeConfigSetScript( array $configuration ) { - return Xml::encodeJsCall( - 'mw.config.set', - array( $configuration ), - ResourceLoader::inDebugMode() - ); + if ( ResourceLoader::inDebugMode() ) { + return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), true ); + } + + $config = RequestContext::getMain()->getConfig(); + $js = Xml::encodeJsCall( 'mw.config.set', array( $configuration ), false ); + return self::applyFilter( 'minify-js', $js, $config ); } /** @@ -1427,7 +1472,7 @@ MESSAGE; * @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 procol-relative) + * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too. */ public function createLoaderURL( $source, ResourceLoaderContext $context, $extraQuery = array() @@ -1435,14 +1480,12 @@ MESSAGE; $query = self::createLoaderQuery( $context, $extraQuery ); $script = $this->getLoadScript( $source ); - // Prevent the IE6 extension check from being triggered (bug 28840) - // by appending a character that's invalid in Windows extensions ('*') - return wfExpandUrl( wfAppendQuery( $script, $query ) . '&*', PROTO_RELATIVE ); + return wfAppendQuery( $script, $query ); } /** * Build a load.php URL - * @deprecated since 1.24, use createLoaderURL instead + * @deprecated since 1.24 Use createLoaderURL() instead * @param array $modules Array of module names (strings) * @param string $lang Language code * @param string $skin Skin name @@ -1453,7 +1496,7 @@ MESSAGE; * @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) + * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too. */ public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null, $printable = false, @@ -1465,9 +1508,7 @@ MESSAGE; $only, $printable, $handheld, $extraQuery ); - // Prevent the IE6 extension check from being triggered (bug 28840) - // by appending a character that's invalid in Windows extensions ('*') - return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE ); + return wfAppendQuery( $wgLoadScript, $query ); } /** @@ -1562,27 +1603,23 @@ MESSAGE; * @param Config $config * @throws MWException * @since 1.22 - * @return lessc + * @return Less_Parser */ public static function getLessCompiler( Config $config ) { // 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 ( !class_exists( 'lessc' ) ) { - throw new MWException( 'MediaWiki requires the lessphp compiler' ); - } - if ( !function_exists( 'ctype_digit' ) ) { - throw new MWException( 'lessc requires the Ctype extension' ); + if ( !class_exists( 'Less_Parser' ) ) { + throw new MWException( 'MediaWiki requires the less.php parser' ); } - $less = new lessc(); - $less->setPreserveComments( true ); - $less->setVariables( self::getLessVars( $config ) ); - $less->setImportDir( $config->get( 'ResourceLoaderLESSImportPaths' ) ); - foreach ( $config->get( 'ResourceLoaderLESSFunctions' ) as $name => $func ) { - $less->registerFunction( $name, $func ); - } - return $less; + $parser = new Less_Parser; + $parser->ModifyVars( self::getLessVars( $config ) ); + $parser->SetImportDirs( array_fill_keys( $config->get( 'ResourceLoaderLESSImportPaths' ), '' ) ); + $parser->SetOption( 'relativeUrls', false ); + $parser->SetCacheDir( $config->get( 'CacheDirectory' ) ?: wfTempDir() ); + + return $parser; } /** |