Author: lryan
Date: Sat May 16 00:26:56 2009
New Revision: 775404

URL: http://svn.apache.org/viewvc?rev=775404&view=rev
Log:
Fix for dangerous DOM cloning behavior in template processing. Cloning nodes 
from a singleton template DOM across many gadget renders can cause those cloned 
nodes to accumulate in the singleton''s template Document userData. Use 
importNode across different documents and cloning within the same document to 
avoid issue.

Modified:
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessor.java
    
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessorTest.java

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java?rev=775404&r1=775403&r2=775404&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java
 Sat May 16 00:26:56 2009
@@ -98,7 +98,7 @@
   private static enum Bypass { ALL, ONLY_SELF, NONE };
   private static UserDataHandler copyOnClone = new UserDataHandler() {
     public void handle(short operation, String key, Object data, Node src, 
Node dst) {
-      if (operation == NODE_CLONED) {
+      if (operation == NODE_IMPORTED || operation == NODE_CLONED) {
         dst.setUserData(key, data, copyOnClone);
       }
     }

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessor.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessor.java?rev=775404&r1=775403&r2=775404&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessor.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessor.java
 Sat May 16 00:26:56 2009
@@ -333,12 +333,15 @@
     if (handler != null) {
       handler.process(result, element, this);
     } else {
-      Element resultNode = (Element) element.cloneNode(false);
-      // Make sure that the resultNode is in the correct owner document.
-      // It would be cleaner to require that the incoming element is
-      // already in the correct document, but would require extra clones.
-      if (resultNode.getOwnerDocument() != result.getOwnerDocument()) {
-        result.getOwnerDocument().adoptNode(resultNode);
+      // Be careful cloning nodes! If a target node belongs to a different 
document than the
+      // template node then use importNode rather than cloneNode as that 
avoids side-effects
+      // in UserDataHandlers where the cloned template node would belong to 
its original
+      // document before being adopted by the target document.
+      Element resultNode;
+      if (element.getOwnerDocument() != result.getOwnerDocument()) {
+        resultNode = (Element)result.getOwnerDocument().importNode(element, 
false);
+      } else {
+        resultNode = (Element)element.cloneNode(false);
       }
       
       clearSpecialAttributes(resultNode);

Modified: 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessorTest.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessorTest.java?rev=775404&r1=775403&r2=775404&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessorTest.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessorTest.java
 Sat May 16 00:26:56 2009
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertFalse;
 
+import org.apache.shindig.common.xml.XmlUtil;
 import org.apache.shindig.expressions.Expressions;
 import org.apache.shindig.expressions.RootELResolver;
 import org.apache.shindig.gadgets.GadgetException;
@@ -29,6 +30,9 @@
 import org.apache.shindig.gadgets.parse.DefaultHtmlSerializer;
 import org.apache.shindig.gadgets.parse.ParseModule;
 import org.apache.shindig.gadgets.parse.nekohtml.SocialMarkupHtmlParser;
+import org.apache.shindig.gadgets.render.SanitizingGadgetRewriter;
+
+
 import org.json.JSONObject;
 import org.json.JSONArray;
 import org.junit.Before;
@@ -43,6 +47,8 @@
 import com.google.common.collect.Maps;
 
 import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Hashtable;
 import java.util.Map;
 import java.util.Set;
 
@@ -67,12 +73,16 @@
   private SocialMarkupHtmlParser parser;
   
   private static final String TEST_NS = "http://example.com";;
-  
+  protected SingletonElementHandler singletonElementHandler;
+
   @Before
   public void setUp() throws Exception {
     expressions = new Expressions();
     variables = Maps.newHashMap();
-    Set<TagHandler> handlers = ImmutableSet.of((TagHandler) new 
TestTagHandler());
+    singletonElementHandler = new SingletonElementHandler();
+    Set<TagHandler> handlers = ImmutableSet.<TagHandler>of(
+        new TestTagHandler(),
+        singletonElementHandler);
     registry = new DefaultTagRegistry(handlers);
 
     processor = new DefaultTemplateProcessor(expressions);
@@ -202,6 +212,44 @@
     assertEquals("<input class=\"false\" disabled=\"disabled\">", output);
   }
 
+  /**
+   * Ensure that the element cloning handling of processChildren correctly
+   * copies and element to the target element, including making sure that
+   * document references are properly cleaned up and user_data in the original
+   * content does not refer to the target document
+   * @throws Exception
+   */
+  @Test
+  public void testSafeCrossDocumentCloning() throws Exception {
+    String template = "<test:Bar text='${foo.title}' data='${user}'/>";
+    executeTemplate(template, "xmlns:test='" + TEST_NS + "'");
+    executeTemplate(template, "xmlns:test='" + TEST_NS + "'");
+
+    // This is a little hacky but is fine for testing purposes. Assumes that 
DOM implementation
+    // is based on Xerces which will always has a userData hashtable
+    Document doc = singletonElementHandler.elem.getOwnerDocument();
+    Class docClass = doc.getClass();
+    Field userDataField = null;
+    while (userDataField == null) {
+      try {
+        userDataField = docClass.getDeclaredField("userData");
+      } catch (NoSuchFieldException nsfe) {
+        // Ignore. Try the parent
+      }
+      docClass = docClass.getSuperclass();
+    }
+    // Access is typically protected so just bypass
+    userDataField.setAccessible(true);
+    Hashtable userDataMap = (Hashtable)userDataField.get(doc);
+
+    // There should be only one element in the user data map, if there are 
more then the
+    // cloning process has put them there which can be a nasty source of 
memory leaks. Consider
+    // the case of this test where the singleton template is a shared and 
re-used template where
+    // the  template documents userData starts to accumulate cloned nodes for 
every time that
+    // template is rendered
+    assertEquals(1, userDataMap.size());
+  }
+
   private String executeTemplate(String markup) throws Exception {
     return executeTemplate(markup, "");
   }
@@ -253,4 +301,21 @@
       result.appendChild(b);
     }
   }
+
+  /**
+   * A tag to test the correct behavior of user data and element cloning
+   */
+  private static class SingletonElementHandler extends AbstractTagHandler {
+
+    Element elem = 
XmlUtil.parseSilent("<wrapper><div><a>out</a></div></wrapper>");
+
+    public SingletonElementHandler() {
+      super(TEST_NS, "Bar");
+      
SanitizingGadgetRewriter.bypassSanitization((Element)elem.getFirstChild(), 
true);
+    }
+
+    public void process(Node result, Element tag, TemplateProcessor processor) 
{
+      processor.processChildNodes(result, elem);
+    }
+  }
 }


Reply via email to