Render git:// links in Markdown

README.md and other Markdown pages sometimes contains links to Git
repositories using git:// protocol.  This is by default rejected by
Soy sanitizers, but its reasonable to allow within the context of a
Markdown page in a Git repository.

Change-Id: Ibfa389335a25d2315d929c9f67082b4099328bac
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 2dcebdd..d5218e7 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
@@ -364,6 +364,11 @@
   String href(String target) {
     if (target.startsWith("#") || HtmlBuilder.isValidHttpUri(target)) {
       return target;
+    } else if (target.startsWith("git:")) {
+      if (HtmlBuilder.isValidGitUri(target)) {
+        return target;
+      }
+      return FilterNormalizeUri.INSTANCE.getInnocuousOutput();
     }
 
     String anchor = "";
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
index 9639176..3239fe1 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
@@ -85,6 +85,17 @@
   private static final FilterNormalizeUri URI = FilterNormalizeUri.INSTANCE;
   private static final FilterImageDataUri IMAGE_DATA = FilterImageDataUri.INSTANCE;
 
+  private static final Pattern GIT_URI =
+      Pattern.compile(
+          "^"
+              +
+              // Reject paths containing /../ or ending in /..
+              "(?![^#?]*/(?:\\.|%2E){2}(?:[/?#]|\\z))"
+              +
+              // Accept git://host/path
+              "git://[^/]+/.+",
+          Pattern.CASE_INSENSITIVE);
+
   public static boolean isValidCssDimension(String val) {
     return val != null && val.matches("(?:[1-9][0-9]*px|100%|[1-9][0-9]?%)");
   }
@@ -99,6 +110,10 @@
     return IMAGE_DATA.getValueFilter().matcher(url).find();
   }
 
+  public static boolean isValidGitUri(String val) {
+    return GIT_URI.matcher(val).find();
+  }
+
   private final StringBuilder htmlBuf;
   private final Appendable textBuf;
   private String tag;
@@ -159,7 +174,7 @@
   }
 
   private String anchorHref(String val) {
-    if (URI.getValueFilter().matcher(val).find()) {
+    if (URI.getValueFilter().matcher(val).find() || isValidGitUri(val)) {
       return URI.escape(val);
     }
     return URI.getInnocuousOutput();
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/doc/DocServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/doc/DocServletTest.java
index 9d2b437..25f3d7f 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/doc/DocServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/doc/DocServletTest.java
@@ -132,4 +132,20 @@
     String html = buildHtml("/repo/+doc/master/README.md");
     assertThat(html).contains("<a href=\"/b/repo/+/master/x\">c</a>");
   }
+
+  @Test
+  public void gitUrlLink() throws Exception {
+    repo.branch("master").commit().add("README.md", "[c](git://example.com/repo.git)").create();
+
+    String html = buildHtml("/repo/+doc/master/README.md");
+    assertThat(html).contains("<a href=\"git://example.com/repo.git\">c</a>");
+  }
+
+  @Test
+  public void invalidGitUrlLink() throws Exception {
+    repo.branch("master").commit().add("README.md", "[c](git://example.com/repo/..)").create();
+
+    String html = buildHtml("/repo/+doc/master/README.md");
+    assertThat(html).contains("<a href=\"#zSoyz\">c</a>");
+  }
 }
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/doc/LinkTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/doc/LinkTest.java
index fdffffc..ad9f941 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/doc/LinkTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/doc/LinkTest.java
@@ -63,6 +63,25 @@
   }
 
   @Test
+  public void gitLink() {
+    MarkdownToHtml md =
+        MarkdownToHtml.builder()
+            .setGitilesView(view)
+            .setConfig(new MarkdownConfig(config))
+            .setFilePath("index.md")
+            .build();
+    String url;
+
+    url = "git://example.com/repo.git";
+    assertThat(md.href(url)).isEqualTo(url);
+
+    assertThat(md.href("git:example.com/repo.git")).isEqualTo("#zSoyz");
+    assertThat(md.href("git://")).isEqualTo("#zSoyz");
+    assertThat(md.href("git://example.com/../root")).isEqualTo("#zSoyz");
+    assertThat(md.href("git://example.com/root/..")).isEqualTo("#zSoyz");
+  }
+
+  @Test
   public void absolutePath() {
     MarkdownToHtml md =
         MarkdownToHtml.builder()