Markdown: optionally allow limited <iframe> tags

Allow the Gitiles administrator to set markdown.allowiframe to
a list of http:// or https:// URL prefixes that are considered
trustworthy enough to be embedded inside of iframes within the
markdown served by this Gitiles instance.

Implement a new strict parser for the <iframe> element inside of
the markdown extension, pulling out only the src, height and width
attributes. Other iframe attributes will cause the entire element
to be recognized as raw HTML and dropped by the parser and formatter.

Apply strict validation on the src attribute, dropping the iframe
if it is not acceptable.

Change-Id: I7d5decd9f0dbfa2acf1e4f59e571ac5518067d4a
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
index c1c7d4f..f2de56e 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
@@ -22,6 +22,8 @@
 import com.google.template.soy.data.SanitizedContent;
 import com.google.template.soy.shared.restricted.EscapingConventions;
 
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.util.StringUtils;
 import org.pegdown.ast.AbbreviationNode;
 import org.pegdown.ast.AutoLinkNode;
 import org.pegdown.ast.BlockQuoteNode;
@@ -71,10 +73,12 @@
   private final HtmlBuilder html = new HtmlBuilder();
   private final TocFormatter toc = new TocFormatter(html, 3);
   private final GitilesView view;
+  private final Config cfg;
   private TableState table;
 
-  public MarkdownToHtml(GitilesView view) {
+  public MarkdownToHtml(GitilesView view, Config cfg) {
     this.view = view;
+    this.cfg = cfg;
   }
 
   /** Render the document AST to sanitized HTML. */
@@ -127,6 +131,36 @@
   }
 
   @Override
+  public void visit(IframeNode node) {
+    if (HtmlBuilder.isValidHttpUri(node.src)
+        && HtmlBuilder.isValidCssDimension(node.height)
+        && HtmlBuilder.isValidCssDimension(node.width)
+        && canRender(node)) {
+      html.open("iframe")
+          .attribute("src", node.src)
+          .attribute("height", node.height)
+          .attribute("width", node.width);
+      if (!node.border) {
+        html.attribute("class", "noborder");
+      }
+      html.close("iframe");
+    }
+  }
+
+  private boolean canRender(IframeNode node) {
+    String[] ok = cfg.getStringList("markdown", null, "allowiframe");
+    if (ok.length == 1 && StringUtils.toBooleanOrNull(ok[0]) == Boolean.TRUE) {
+      return true;
+    }
+    for (String m : ok) {
+      if (m.equals(node.src) || (m.endsWith("/") && node.src.startsWith(m))) {
+        return true;
+      }
+    }
+    return false; // By default do not render iframe.
+  }
+
+  @Override
   public void visit(HeaderNode node) {
     String tag = "h" + node.getLevel();
     html.open(tag);