Support format=TEXT for /+refs List refs in the same format in an /info/refs request for the git protocol, plus some minor sanitization to deal with XSS. Support scoping of refs by adding additional path components (e.g. /heads) after /+refs. This means allowing refs views with paths, but note that this only applies to the TEXT (and soon JSON) format, not HTML. Change-Id: I7c7074544af366f38791d5a90d0a024d2555c92e
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java index 42079ee..67040fe 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesView.java
@@ -205,6 +205,7 @@ case DIFF: this.path = maybeTrimLeadingAndTrailingSlash(checkNotNull(path)); return this; + case REFS: case LOG: this.path = path != null ? maybeTrimLeadingAndTrailingSlash(path) : null; return this; @@ -348,7 +349,7 @@ return new Builder(Type.LOG); } - private static String maybeTrimLeadingAndTrailingSlash(String str) { + static String maybeTrimLeadingAndTrailingSlash(String str) { if (str.startsWith("/")) { str = str.substring(1); } @@ -522,6 +523,8 @@ * auto-diving into one-entry subtrees. */ public List<Map<String, String>> getBreadcrumbs(List<Boolean> hasSingleTree) { + checkArgument(type != Type.REFS || Strings.isNullOrEmpty(path), + "breadcrumbs for REFS view with path not supported"); checkArgument(hasSingleTree == null || type == Type.PATH, "hasSingleTree must be null for %s view", type); String path = this.path; @@ -551,7 +554,8 @@ breadcrumbs.add(breadcrumb(revision.getName(), revision().copyFrom(this))); } if (path != null) { - if (type != Type.LOG) { // The "." breadcrumb would be no different for LOG. + if (type != Type.LOG && type != Type.REFS) { + // The "." breadcrumb would be no different for LOG or REFS. breadcrumbs.add(breadcrumb(".", copyWithPath().setTreePath(""))); } StringBuilder cur = new StringBuilder();
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java index a046ff1..84cfdb2 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/HostIndexServlet.java
@@ -15,7 +15,6 @@ package com.google.gitiles; import static com.google.common.base.Preconditions.checkNotNull; - import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java index b75965c..f3dcc43 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
@@ -15,7 +15,6 @@ package com.google.gitiles; import static com.google.common.base.Preconditions.checkNotNull; - import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java index bb51f04..fd5e793 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/RefServlet.java
@@ -15,6 +15,7 @@ package com.google.gitiles; import static com.google.common.base.Preconditions.checkNotNull; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; @@ -24,13 +25,16 @@ import com.google.common.util.concurrent.UncheckedExecutionException; import org.eclipse.jgit.http.server.ServletUtils; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefComparator; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.RefAdvertiser; import java.io.IOException; +import java.io.PrintWriter; import java.util.Collection; import java.util.List; import java.util.Map; @@ -51,25 +55,41 @@ } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse res) + protected void doGetHtml(HttpServletRequest req, HttpServletResponse res) throws IOException { + if (!ViewFilter.getView(req).getTreePath().isEmpty()) { + res.setStatus(SC_NOT_FOUND); + return; + } RevWalk walk = new RevWalk(ServletUtils.getRepository(req)); List<Map<String, Object>> tags; try { - tags = getTags(req, timeCache, walk, 0); + tags = getTagsSoyData(req, timeCache, walk, 0); } finally { walk.release(); } renderHtml(req, res, "gitiles.refsDetail", - ImmutableMap.of("branches", getBranches(req, 0), "tags", tags)); + ImmutableMap.of("branches", getBranchesSoyData(req, 0), "tags", tags)); } - static List<Map<String, Object>> getBranches(HttpServletRequest req, int limit) + @Override + protected void doGetText(HttpServletRequest req, HttpServletResponse res) + throws IOException { + GitilesView view = ViewFilter.getView(req); + Map<String, Ref> refs = getRefs(ServletUtils.getRepository(req).getRefDatabase(), + view.getTreePath()); + TextRefAdvertiser adv = new TextRefAdvertiser(startRenderText(req, res)); + adv.setDerefTags(true); + adv.send(refs); + adv.end(); + } + + static List<Map<String, Object>> getBranchesSoyData(HttpServletRequest req, int limit) throws IOException { RefDatabase refdb = ServletUtils.getRepository(req).getRefDatabase(); Ref head = refdb.getRef(Constants.HEAD); Ref headLeaf = head != null && head.isSymbolic() ? head.getLeaf() : null; - return getRefs( + return getRefsSoyData( refdb, ViewFilter.getView(req), Constants.R_HEADS, @@ -97,9 +117,9 @@ }.compound(RefComparator.INSTANCE); } - static List<Map<String, Object>> getTags(HttpServletRequest req, + static List<Map<String, Object>> getTagsSoyData(HttpServletRequest req, TimeCache timeCache, RevWalk walk, int limit) throws IOException { - return getRefs( + return getRefsSoyData( ServletUtils.getRepository(req).getRefDatabase(), ViewFilter.getView(req), Constants.R_TAGS, @@ -121,7 +141,7 @@ }).reverse().compound(RefComparator.INSTANCE); } - private static List<Map<String, Object>> getRefs( + private static List<Map<String, Object>> getRefsSoyData( RefDatabase refdb, GitilesView view, String prefix, @@ -146,4 +166,46 @@ } return result; } + + private static String sanitizeRefForText(String refName) { + return refName.replace("&", "&") + .replace("<", "<") + .replace(">", ">"); + } + + private static Map<String, Ref> getRefs(RefDatabase refdb, String path) throws IOException { + path = GitilesView.maybeTrimLeadingAndTrailingSlash(path); + if (path.isEmpty()) { + return refdb.getRefs(RefDatabase.ALL); + } + path = Constants.R_REFS + path; + Ref singleRef = refdb.getRef(path); + if (singleRef != null) { + return ImmutableMap.of(singleRef.getName(), singleRef); + } + return refdb.getRefs(path + '/'); + } + + private static class TextRefAdvertiser extends RefAdvertiser { + private final PrintWriter writer; + + private TextRefAdvertiser(PrintWriter writer) { + this.writer = writer; + } + + @Override + public void advertiseId(AnyObjectId id, String refName) throws IOException { + super.advertiseId(id, sanitizeRefForText(refName)); + } + + @Override + protected void writeOne(CharSequence line) throws IOException { + writer.print(line); + } + + @Override + public void end() throws IOException { + writer.close(); + } + } }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java index 9672815..5a92dea 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/RepositoryIndexServlet.java
@@ -67,7 +67,7 @@ List<Map<String, Object>> tags; Map<String, Object> data; try { - tags = RefServlet.getTags(req, timeCache, walk, REF_LIMIT); + tags = RefServlet.getTagsSoyData(req, timeCache, walk, REF_LIMIT); ObjectId headId = repo.resolve(Constants.HEAD); if (headId != null) { RevObject head = walk.parseAny(headId); @@ -88,7 +88,7 @@ if (!data.containsKey("entries")) { data.put("entries", ImmutableList.of()); } - List<Map<String, Object>> branches = RefServlet.getBranches(req, REF_LIMIT); + List<Map<String, Object>> branches = RefServlet.getBranchesSoyData(req, REF_LIMIT); data.put("cloneUrl", desc.cloneUrl); data.put("mirroredFromUrl", Strings.nullToEmpty(desc.mirroredFromUrl));
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java index 4f629be..c24b4f0 100644 --- a/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java +++ b/gitiles-servlet/src/main/java/com/google/gitiles/ViewFilter.java
@@ -102,8 +102,8 @@ // Non-path cases. if (repoName.isEmpty()) { return GitilesView.hostIndex(); - } else if (command.equals(CMD_REFS) && path.isEmpty()) { - return GitilesView.refs().setRepositoryName(repoName); + } else if (command.equals(CMD_REFS)) { + return GitilesView.refs().setRepositoryName(repoName).setTreePath(path); } else if (command.equals(CMD_LOG) && (path.isEmpty() || path.equals("/"))) { return GitilesView.log().setRepositoryName(repoName); } else if (command.isEmpty()) {
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/RefServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/RefServletTest.java new file mode 100644 index 0000000..9e8bac4 --- /dev/null +++ b/gitiles-servlet/src/test/java/com/google/gitiles/RefServletTest.java
@@ -0,0 +1,177 @@ +// Copyright 2012 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gitiles; + +import junit.framework.TestCase; + +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.storage.dfs.DfsRepository; +import org.eclipse.jgit.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.storage.dfs.InMemoryRepository; + +import java.io.IOException; + +/** Tests for {@link Linkifier}. */ +public class RefServletTest extends TestCase { + private TestRepository<DfsRepository> repo;; + private GitilesServlet servlet; + + @Override + protected void setUp() throws Exception { + DfsRepository r = new InMemoryRepository(new DfsRepositoryDescription("test")); + repo = new TestRepository<DfsRepository>(r); + + RevCommit commit = repo.branch("refs/heads/master").commit().create(); + repo.update("refs/heads/branch", commit); + repo.update("refs/tags/ctag", commit); + RevTag tag = repo.tag("atag", commit); + repo.update("refs/tags/atag", tag); + r.updateRef("HEAD").link("refs/heads/master"); + + servlet = TestGitilesServlet.create(repo); + } + + private String id(String refName) throws IOException { + return ObjectId.toString(repo.getRepository().getRef(refName).getObjectId()); + } + + private String peeled(String refName) throws IOException { + return ObjectId.toString(repo.getRepository().peel( + repo.getRepository().getRef(refName)).getPeeledObjectId()); + } + + public void testEvilRefName() throws Exception { + String evilRefName = "refs/evil/<script>window.close();</script>/&foo"; + assertTrue(Repository.isValidRefName(evilRefName)); + repo.branch(evilRefName).commit().create(); + + FakeHttpServletRequest req = FakeHttpServletRequest.newRequest(); + req.setPathInfo("/test/+refs/evil"); + req.setQueryString("format=TEXT"); + FakeHttpServletResponse res = new FakeHttpServletResponse(); + servlet.service(req, res); + + assertEquals( + id(evilRefName) + " refs/evil/<script>window.close();</script>/&foo\n", + res.getActualBodyString()); + } + + public void testGetRefsTextAll() throws Exception { + FakeHttpServletRequest req = FakeHttpServletRequest.newRequest(); + req.setPathInfo("/test/+refs"); + req.setQueryString("format=TEXT"); + FakeHttpServletResponse res = new FakeHttpServletResponse(); + servlet.service(req, res); + + assertEquals(200, res.getStatus()); + assertEquals( + id("HEAD") + " HEAD\n" + + id("refs/heads/branch") + " refs/heads/branch\n" + + id("refs/heads/master") + " refs/heads/master\n" + + id("refs/tags/atag") + " refs/tags/atag\n" + + peeled("refs/tags/atag") + " refs/tags/atag^{}\n" + + id("refs/tags/ctag") + " refs/tags/ctag\n", + res.getActualBodyString()); + } + + public void testGetRefsTextAllTrailingSlash() throws Exception { + FakeHttpServletRequest req = FakeHttpServletRequest.newRequest(); + req.setPathInfo("/test/+refs"); + req.setQueryString("format=TEXT"); + FakeHttpServletResponse res = new FakeHttpServletResponse(); + servlet.service(req, res); + + assertEquals(200, res.getStatus()); + assertEquals( + id("HEAD") + " HEAD\n" + + id("refs/heads/branch") + " refs/heads/branch\n" + + id("refs/heads/master") + " refs/heads/master\n" + + id("refs/tags/atag") + " refs/tags/atag\n" + + peeled("refs/tags/atag") + " refs/tags/atag^{}\n" + + id("refs/tags/ctag") + " refs/tags/ctag\n", + res.getActualBodyString()); + } + + public void testGetRefsHeadsText() throws Exception { + FakeHttpServletRequest req = FakeHttpServletRequest.newRequest(); + req.setPathInfo("/test/+refs/heads"); + req.setQueryString("format=TEXT"); + FakeHttpServletResponse res = new FakeHttpServletResponse(); + servlet.service(req, res); + + assertEquals(200, res.getStatus()); + assertEquals( + id("refs/heads/branch") + " refs/heads/branch\n" + + id("refs/heads/master") + " refs/heads/master\n", + res.getActualBodyString()); + } + + public void testGetRefsHeadsTextTrailingSlash() throws Exception { + FakeHttpServletRequest req = FakeHttpServletRequest.newRequest(); + req.setPathInfo("/test/+refs/heads/"); + req.setQueryString("format=TEXT"); + FakeHttpServletResponse res = new FakeHttpServletResponse(); + servlet.service(req, res); + + assertEquals(200, res.getStatus()); + assertEquals( + id("refs/heads/branch") + " refs/heads/branch\n" + + id("refs/heads/master") + " refs/heads/master\n", + res.getActualBodyString()); + } + + public void testNoHeadText() throws Exception { + FakeHttpServletRequest req = FakeHttpServletRequest.newRequest(); + req.setPathInfo("/test/+refs/HEAD"); + req.setQueryString("format=TEXT"); + FakeHttpServletResponse res = new FakeHttpServletResponse(); + servlet.service(req, res); + + assertEquals(200, res.getStatus()); + // /+refs/foo means refs/foo(/*), so this is empty. + assertEquals("", res.getActualBodyString()); + } + + public void testSingleHeadText() throws Exception { + FakeHttpServletRequest req = FakeHttpServletRequest.newRequest(); + req.setPathInfo("/test/+refs/heads/master"); + req.setQueryString("format=TEXT"); + FakeHttpServletResponse res = new FakeHttpServletResponse(); + servlet.service(req, res); + + assertEquals(200, res.getStatus()); + assertEquals( + id("refs/heads/master") + " refs/heads/master\n", + res.getActualBodyString()); + } + + public void testSinglePeeledTagText() throws Exception { + FakeHttpServletRequest req = FakeHttpServletRequest.newRequest(); + req.setPathInfo("/test/+refs/tags/atag"); + req.setQueryString("format=TEXT"); + FakeHttpServletResponse res = new FakeHttpServletResponse(); + servlet.service(req, res); + + assertEquals(200, res.getStatus()); + assertEquals( + id("refs/tags/atag") + " refs/tags/atag\n" + + peeled("refs/tags/atag") + " refs/tags/atag^{}\n", + res.getActualBodyString()); + } +}
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java index 2c3ae2a..1088692 100644 --- a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java +++ b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
@@ -97,12 +97,42 @@ } public void testRefs() throws Exception { - GitilesView view = getView("/repo/+refs"); + GitilesView view; + + view = getView("/repo/+refs"); assertEquals(Type.REFS, view.getType()); assertEquals("repo", view.getRepositoryName()); assertEquals(Revision.NULL, view.getRevision()); assertEquals(Revision.NULL, view.getOldRevision()); - assertNull(view.getTreePath()); + assertEquals("", view.getTreePath()); + + view = getView("/repo/+refs/"); + assertEquals(Type.REFS, view.getType()); + assertEquals("repo", view.getRepositoryName()); + assertEquals(Revision.NULL, view.getRevision()); + assertEquals(Revision.NULL, view.getOldRevision()); + assertEquals("", view.getTreePath()); + + view = getView("/repo/+refs/heads"); + assertEquals(Type.REFS, view.getType()); + assertEquals("repo", view.getRepositoryName()); + assertEquals(Revision.NULL, view.getRevision()); + assertEquals(Revision.NULL, view.getOldRevision()); + assertEquals("heads", view.getTreePath()); + + view = getView("/repo/+refs/heads/"); + assertEquals(Type.REFS, view.getType()); + assertEquals("repo", view.getRepositoryName()); + assertEquals(Revision.NULL, view.getRevision()); + assertEquals(Revision.NULL, view.getOldRevision()); + assertEquals("heads", view.getTreePath()); + + view = getView("/repo/+refs/heads/master"); + assertEquals(Type.REFS, view.getType()); + assertEquals("repo", view.getRepositoryName()); + assertEquals(Revision.NULL, view.getRevision()); + assertEquals(Revision.NULL, view.getOldRevision()); + assertEquals("heads/master", view.getTreePath()); } public void testBranches() throws Exception {