diff options
Diffstat (limited to 'includes/resourceloader/ResourceLoader.php')
-rw-r--r-- | includes/resourceloader/ResourceLoader.php | 282 |
1 files changed, 192 insertions, 90 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; + } } |