Author: lindner Date: Tue Feb 2 00:13:49 2010 New Revision: 905469 URL: http://svn.apache.org/viewvc?rev=905469&view=rev Log: SHINDIG-1260 | Patch from Jacky Wang | Improper JavaScript inclusion logic leads to out-of-order and duplicate includes / JavaScript errors
Modified: incubator/shindig/trunk/config/container.js incubator/shindig/trunk/php/config/container.php incubator/shindig/trunk/php/src/gadgets/GadgetContext.php incubator/shindig/trunk/php/src/gadgets/GadgetFactory.php incubator/shindig/trunk/php/src/gadgets/GadgetFeatureRegistry.php incubator/shindig/trunk/php/src/gadgets/GadgetSpecParser.php incubator/shindig/trunk/php/src/gadgets/render/GadgetBaseRenderer.php incubator/shindig/trunk/php/src/gadgets/render/GadgetHtmlRenderer.php incubator/shindig/trunk/php/src/gadgets/render/GadgetRenderer.php incubator/shindig/trunk/php/src/gadgets/render/GadgetUrlRenderer.php incubator/shindig/trunk/php/src/gadgets/servlet/JsServlet.php Modified: incubator/shindig/trunk/config/container.js URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/config/container.js?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/config/container.js (original) +++ incubator/shindig/trunk/config/container.js Tue Feb 2 00:13:49 2010 @@ -114,7 +114,7 @@ }, "rpc" : { // Path to the relay file. Automatically appended to the parent - /// parameter if it passes input validation and is not null. + // parameter if it passes input validation and is not null. // This should never be on the same host in a production environment! // Only use this for TESTING! "parentRelayUrl" : "/gadgets/files/container/rpc_relay.html", Modified: incubator/shindig/trunk/php/config/container.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/config/container.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/config/container.php (original) +++ incubator/shindig/trunk/php/config/container.php Tue Feb 2 00:13:49 2010 @@ -110,7 +110,7 @@ 'jsondb_path' => realpath(dirname(__FILE__) . '/../../javascript/sampledata') . '/canonicaldb.json', // Force these libraries to be external (included through <script src="..."> tags), this way they could be cached by the browser - 'focedJsLibs' => '', + 'forcedJsLibs' => '', // After checking the internal __autoload function, shindig can also call the 'extension_autoloader' function to load an // unknown custom class, this is particuarly useful for when intergrating shindig into an existing framework that also depends on autoloading Modified: incubator/shindig/trunk/php/src/gadgets/GadgetContext.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/GadgetContext.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/GadgetContext.php (original) +++ incubator/shindig/trunk/php/src/gadgets/GadgetContext.php Tue Feb 2 00:13:49 2010 @@ -46,7 +46,7 @@ // Request variables $this->setIgnoreCache($this->getIgnoreCacheParam()); - $this->setForcedJsLibs($this->getFocedJsLibsParam()); + $this->setForcedJsLibs($this->getForcedJsLibsParam()); $this->setUrl($this->getUrlParam()); $this->setModuleId($this->getModuleIdParam()); $this->setView($this->getViewParam()); @@ -79,7 +79,7 @@ return (isset($_GET['nocache']) && intval($_GET['nocache']) == 1) || (isset($_GET['bpc']) && intval($_GET['bpc']) == 1); } - private function getFocedJsLibsParam() { + private function getForcedJsLibsParam() { return isset($_GET['libs']) ? trim($_GET['libs']) : null; } Modified: incubator/shindig/trunk/php/src/gadgets/GadgetFactory.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/GadgetFactory.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/GadgetFactory.php (original) +++ incubator/shindig/trunk/php/src/gadgets/GadgetFactory.php Tue Feb 2 00:13:49 2010 @@ -70,7 +70,9 @@ */ private function parseFeatures(Gadget &$gadget) { $found = $missing = array(); - if (! $this->context->getRegistry()->resolveFeatures(array_merge($gadget->gadgetSpec->requiredFeatures, $gadget->gadgetSpec->optionalFeatures), $found, $missing)) { + if (! $this->context->getRegistry()->resolveFeatures( + array_merge($gadget->gadgetSpec->requiredFeatures, $gadget->gadgetSpec->optionalFeatures), + $found, $missing, true)) { $requiredMissing = false; foreach ($missing as $featureName) { if (in_array($featureName, $gadget->gadgetSpec->requiredFeatures)) { Modified: incubator/shindig/trunk/php/src/gadgets/GadgetFeatureRegistry.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/GadgetFeatureRegistry.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/GadgetFeatureRegistry.php (original) +++ incubator/shindig/trunk/php/src/gadgets/GadgetFeatureRegistry.php Tue Feb 2 00:13:49 2010 @@ -33,6 +33,14 @@ $this->registerFeatures($featurePath); } + public function getFeaturesContent($features, GadgetContext $context, $isGadgetContext) { + $ret = ''; + foreach ($features as $feature) { + $ret .= $this->getFeatureContent($feature, $context, $isGadgetContext); + } + return $ret; + } + public function getFeatureContent($feature, GadgetContext $context, $isGadgetContext) { if (empty($feature)) return ''; if (!isset($this->features[$feature])) { @@ -78,7 +86,7 @@ return $ret; } - public function resolveFeatures($needed, &$resultsFound, &$resultsMissing) { + public function resolveFeatures($needed, &$resultsFound, &$resultsMissing, $isExpand) { $resultsFound = array(); $resultsMissing = array(); if (! count($needed)) { @@ -91,18 +99,77 @@ if ($feature == null) { $resultsMissing[] = $featureName; } else { - $this->addFeatureToResults($resultsFound, $feature); + $this->addFeatureToResults($resultsFound, $feature, $isExpand); } } return count($resultsMissing) == 0; } + + public function sortFeatures($features, $forcedJsLibs, &$sortedFeatureGroups) { + $sortedFeatureGroups = array(); + if (empty($features)) return; - private function addFeatureToResults(&$results, $feature) { + // Topological sorting with constrain + // Build the topology matrix + $sortedFeatures = array(); + $reverseDeps = array(); + foreach ($features as $feature) { + $reverseDeps[$feature] = array(); + } + $depCount = array(); + foreach ($features as $feature) { + $deps = $this->features[$feature]['deps']; + $deps = array_uintersect($deps, $features, "strcasecmp"); + $depCount[$feature] = count($deps); + foreach ($deps as $dep) { + $reverseDeps[$dep][] = $feature; + } + } + + // iterate to do the sorting + $prevIsForcedJsLibs = true; + $featureQueue = array(); + while (! empty($depCount)) { + $fail = true; + while (! empty($depCount)) { // get grouped features greedily + $foundAll = true; + foreach ($depCount as $feature => $count) { + if ($count != 0) continue; + $fail = false; // found at least one root node + if ((! $prevIsForcedJsLibs) ^ in_array($feature, $forcedJsLibs)) { + $foundAll = false; + $featureQueue[] = $feature; + foreach ($reverseDeps[$feature] as $reverseDep) { + $depCount[$reverseDep] -= 1; + } + unset($depCount[$feature]); + } + } + if ($foundAll) break; + } + if ($fail) { + throw new GadgetException("Sorting feature dependence failed: it contains ring!"); + } + if (! empty($featureQueue)) { + if ($prevIsForcedJsLibs) { + $sortedFeatureGroups[] = array('type' => 'external', 'features' => $featureQueue); + } else { + $sortedFeatureGroups[] = array('type' => 'inline', 'features' => $featureQueue); + } + } + $prevIsForcedJsLibs = ! $prevIsForcedJsLibs; + $featureQueue = array(); + } + } + + private function addFeatureToResults(&$results, $feature, $isExpand) { if (in_array($feature['name'], $results)) { return; } - foreach ($feature['deps'] as $dep) { - $this->addFeatureToResults($results, $this->features[$dep]); + if ($isExpand) { + foreach ($feature['deps'] as $dep) { + $this->addFeatureToResults($results, $this->features[$dep], true); + } } if (!in_array($feature['name'], $results)) { $results[] = $feature['name']; Modified: incubator/shindig/trunk/php/src/gadgets/GadgetSpecParser.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/GadgetSpecParser.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/GadgetSpecParser.php (original) +++ incubator/shindig/trunk/php/src/gadgets/GadgetSpecParser.php Tue Feb 2 00:13:49 2010 @@ -215,7 +215,8 @@ */ private function parseFeatures(DOMElement &$modulePrefs, GadgetSpec &$gadget) { $gadget->requiredFeatures = $gadget->optionalFeatures = array(); - if (($requiredNodes = $modulePrefs->getElementsByTagName('Require')) != null) { + $requiredNodes = $modulePrefs->getElementsByTagName('Require'); + if ($requiredNodes->length != 0) { foreach ($requiredNodes as $requiredFeature) { $gadget->requiredFeatures[] = $requiredFeature->getAttribute('feature'); if ($requiredFeature->getAttribute('feature') == 'content-rewrite') { @@ -225,7 +226,8 @@ } } } - if (($optionalNodes = $modulePrefs->getElementsByTagName('Optional')) != null) { + $optionalNodes = $modulePrefs->getElementsByTagName('Optional'); + if ($optionalNodes->length != 0) { foreach ($optionalNodes as $optionalFeature) { $gadget->optionalFeatures[] = $optionalFeature->getAttribute('feature'); // Content-rewrite is a special case since it has Params as child nodes Modified: incubator/shindig/trunk/php/src/gadgets/render/GadgetBaseRenderer.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/render/GadgetBaseRenderer.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/render/GadgetBaseRenderer.php (original) +++ incubator/shindig/trunk/php/src/gadgets/render/GadgetBaseRenderer.php Tue Feb 2 00:13:49 2010 @@ -298,26 +298,27 @@ } public function getJavaScripts() { + $registry = $this->context->getRegistry(); $forcedJsLibs = $this->getForcedJsLibs(); - $externalScript = false; - if (! empty($forcedJsLibs)) { - // if some of the feature libraries are externalized (through a browser cachable <script src="/gadgets/js/opensocial-0.9:settitle.js"> type url) - // we inject the tag and don't inline those libs (and their dependencies) - $forcedJsLibs = explode(':', $forcedJsLibs); - $externalScript = Config::get('default_js_prefix') . $this->getJsUrl($forcedJsLibs, $this->gadget) . "&container=" . $this->context->getContainer(); - $registry = $this->context->getRegistry(); - $missing = array(); - $registry->resolveFeatures($forcedJsLibs, $forcedJsLibs, $missing); - } - $script = ''; - foreach ($this->gadget->features as $feature) { - if (! is_array($forcedJsLibs) || (is_array($forcedJsLibs) && ! in_array($feature, $forcedJsLibs))) { - $script .= $this->context->getRegistry()->getFeatureContent($feature, $this->context, true); + $sortedFeatureGroups = array(); + $registry->sortFeatures($this->gadget->features, $forcedJsLibs, $sortedFeatureGroups); + $scripts = array(); + + // if some of the feature libraries are externalized (through a browser cachable <script src="/gadgets/js/opensocial-0.9:settitle.js"> type url) + // we inject the tag and don't inline those libs (and their dependencies) + foreach ($sortedFeatureGroups as $featureGroup) { + if ($featureGroup['type'] == 'inline') { + $featureGroup['content'] = $registry->getFeaturesContent($featureGroup['features'], $this->context, true); + } else { + $featureGroup['content'] = Config::get('default_js_prefix') . $this->getJsUrl($featureGroup['features']) . "&container=" . $this->context->getContainer(); } + $scripts[] = $featureGroup; } + + $script = ''; // Add the JavaScript initialization strings for the configuration, localization and preloads $script .= "\n"; - $script .= $this->appendJsConfig($this->gadget, count($forcedJsLibs)); + $script .= $this->appendJsConfig($this->gadget, $sortedFeatureGroups); $script .= $this->appendMessages($this->gadget); $script .= $this->appendPreloads($this->gadget); if (count($this->dataInserts)) { @@ -330,7 +331,11 @@ if ($this->gadget->gadgetSpec->templatesDisableAutoProcessing) { $script .= "opensocial.template.Container.disableAutoProcessing();\n"; } - return array('inline' => $script, 'external' => $externalScript); + $scripts[] = array( + 'type' => 'inline', + 'content' => $script + ); + return $scripts; } /** @@ -348,62 +353,73 @@ // Inject the OpenSocial feature javascripts $scripts = $this->getJavaScripts(); - if ($scripts['external']) { + foreach ($scripts as $script) { $scriptNode = $doc->createElement('script'); - $scriptNode->setAttribute('src', $scripts['external']); + if ($script['type'] == 'inline') { + $scriptNode->setAttribute('type', 'text/javascript'); + $scriptNode->appendChild($doc->createTextNode($script['content'])); + } else { + $scriptNode->setAttribute('src', $script['content']); + } $node->appendChild($scriptNode); } - $scriptNode = $doc->createElement('script'); - $scriptNode->setAttribute('type', 'text/javascript'); - $scriptNode->appendChild($doc->createTextNode($scripts['inline'])); - $node->appendChild($scriptNode); } /** * Retrieve the forced javascript libraries (if any), using either the &libs= from the query * or if that's empty, from the config * - * @return unknown + * @return array contains the names of forced external javascript libs. */ private function getForcedJsLibs() { $forcedJsLibs = $this->context->getForcedJsLibs(); // allow the &libs=.. param to override our forced js libs configuration value if (empty($forcedJsLibs)) { - $forcedJsLibs = Config::get('focedJsLibs'); + $forcedJsLibs = Config::get('forcedJsLibs'); + } + if (empty($forcedJsLibs)) { + return array(); + } else { + return explode(':', $forcedJsLibs); } - return $forcedJsLibs; } /** * Appends the javascript features configuration string * * @param Gadget $gadget - * @param unknown_type $hasForcedLibs + * @param $featureGroups * @return string */ - private function appendJsConfig(Gadget $gadget, $hasForcedLibs) { + private function appendJsConfig(Gadget $gadget, $featureGroups) { $container = $this->context->getContainer(); $containerConfig = $this->context->getContainerConfig(); - //TODO some day we should parse the forcedLibs too, and include their config selectivly as well for now we just include everything if forced libs is set. - if ($hasForcedLibs) { - $gadgetConfig = $containerConfig->getConfig($container, 'gadgets.features'); - } else { - $gadgetConfig = array(); - $featureConfig = $containerConfig->getConfig($container, 'gadgets.features'); - foreach ($gadget->getJsLibraries() as $library) { - $feature = $library->getFeatureName(); - if (! isset($gadgetConfig[$feature]) && ! empty($featureConfig[$feature])) { - $gadgetConfig[$feature] = $featureConfig[$feature]; - } + $forcedJsLibs = $this->getForcedJsLibs(); + $gadgetConfig = array(); + $featureConfig = $containerConfig->getConfig($container, 'gadgets.features'); + + // TODO some day we should parse the forcedLibs too, and include their config selectivly as well. + // For now we just include everything. + $features = array(); + foreach ($featureGroups as $featureGroup) { + $features = array_merge($features, $featureGroup['features']); + } + + foreach ($features as $feature) { + if (! isset($gadgetConfig[$feature]) && ! empty($featureConfig[$feature])) { + $gadgetConfig[$feature] = $featureConfig[$feature]; } } + // Add gadgets.util support. This is calculated dynamically based on request inputs. // See java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java for reference. $requires = array(); - foreach ($gadget->features as $feature) { + foreach ($features as $feature) { $requires[$feature] = new EmptyClass(); } $gadgetConfig['core.util'] = $requires; + + // following are some quick-fixes for osml and osapi. if (isset($gadgetConfig['osml'])) { unset($gadgetConfig['osml']); } Modified: incubator/shindig/trunk/php/src/gadgets/render/GadgetHtmlRenderer.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/render/GadgetHtmlRenderer.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/render/GadgetHtmlRenderer.php (original) +++ incubator/shindig/trunk/php/src/gadgets/render/GadgetHtmlRenderer.php Tue Feb 2 00:13:49 2010 @@ -50,13 +50,16 @@ // Manually generate the html document using basic string concatinations instead of using our DOM based functions $content .= "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n"; $content .= '<style>'.Config::get('gadget_css')."</style>\n"; + $scripts = $this->getJavaScripts(); - if ($scripts['external']) { - $content .= "<script type=\"text/javascript\" src=\"{$scripts['external']}\"></script>\n"; - } - if (!empty($scripts['inline'])) { - $content .= "<script type=\"text/javascript\">{$scripts['inline']}</script>\n"; + foreach ($scripts as $script) { + if ($script['type'] == 'inline') { + $content .= "<script type=\"text/javascript\">{$script['content']}</script>\n"; + } else { + $content .= "<script type=\"text/javascript\" src=\"{$script['content']}\"></script>\n"; + } } + $content .= "</head>\n<body>\n"; $content .= $gadget->substitutions->substitute($view['content']); $content .= '<script type="text/javascript">'.$this->getBodyScript()."</script>\n"; Modified: incubator/shindig/trunk/php/src/gadgets/render/GadgetRenderer.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/render/GadgetRenderer.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/render/GadgetRenderer.php (original) +++ incubator/shindig/trunk/php/src/gadgets/render/GadgetRenderer.php Tue Feb 2 00:13:49 2010 @@ -38,12 +38,10 @@ * @return string the list of libraries in core:caja:etc.js?v=checksum> format */ protected function getJsUrl($features) { - $ret = ''; if (! is_array($features) || ! count($features)) { - $ret = 'core'; - } else { - $ret = implode(':', $features); + return 'null'; } + $ret = implode(':', $features); $cache = Cache::createCache(Config::get('feature_cache'), 'FeatureCache'); if (($md5 = $cache->get(md5('getJsUrlMD5'))) === false) { $registry = $this->context->getRegistry(); Modified: incubator/shindig/trunk/php/src/gadgets/render/GadgetUrlRenderer.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/render/GadgetUrlRenderer.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/render/GadgetUrlRenderer.php (original) +++ incubator/shindig/trunk/php/src/gadgets/render/GadgetUrlRenderer.php Tue Feb 2 00:13:49 2010 @@ -22,7 +22,7 @@ /** * Renders an 'URL' type view (where the iframe is redirected to the specified url) - * This is more a legacy iGoogle support feature then something that should be actually + * This is more a legacy iGoogle support feature than something that should be actually * used. Proxied content is the socially aware (and higher performance) version of this * See GadgetHrefRenderer for it's implementation. * @@ -35,13 +35,21 @@ $queryStr = strpos($redirURI, '?') !== false ? substr($redirURI, strpos($redirURI, '?')) : ''; $query = $queryStr; $query .= $this->getPrefsQueryString($gadget->gadgetSpec->userPrefs); + + // deal with features + $registry = $this->context->getRegistry(); + // since the URL mode doesn't actually have the gadget XML body, it can't inline + // the javascript content anyway - thus could us just ignore the 'forcedJsLibs' part. + $forcedJsLibs = array(); + $sortedFeatureGroups = array(); + $registry->sortFeatures($gadget->features, $forcedJsLibs, $sortedFeatureGroups); + + // join the groups $features = array(); - $forcedLibs = Config::get('focedJsLibs'); - if ($forcedLibs == null) { - $features = $gadget->features; - } else { - $features = explode(':', $forcedLibs); + foreach ($sortedFeatureGroups as $featureGroup) { + $features = array_merge($features, $featureGroup['features']); } + $query .= $this->appendLibsToQuery($features); $query .= '&lang=' . urlencode(isset($_GET['lang']) ? $_GET['lang'] : 'en'); $query .= '&country=' . urlencode(isset($_GET['country']) ? $_GET['country'] : 'US'); Modified: incubator/shindig/trunk/php/src/gadgets/servlet/JsServlet.php URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/php/src/gadgets/servlet/JsServlet.php?rev=905469&r1=905468&r2=905469&view=diff ============================================================================== --- incubator/shindig/trunk/php/src/gadgets/servlet/JsServlet.php (original) +++ incubator/shindig/trunk/php/src/gadgets/servlet/JsServlet.php Tue Feb 2 00:13:49 2010 @@ -25,7 +25,7 @@ /** * This event handler deals with the /js/core:caja:etc.js request which content type=url gadgets can use * to retrieve our features javascript code, or used to make the most frequently used part of the feature - * library external, and hence cachable by the browser + * library external, and hence cachable by the browser. */ class JsServlet extends HttpServlet { @@ -56,7 +56,7 @@ $missing = array(); $context = new GadgetContext('GADGET'); $registry = new GadgetFeatureRegistry(Config::get('features_path')); - if ($registry->resolveFeatures($needed, $found, $missing)) { + if ($registry->resolveFeatures($needed, $found, $missing, false)) { $isGadgetContext = !isset($_GET["c"]) || $_GET['c'] == 0 ? true : false; $jsData = ''; foreach ($found as $feature) {