From 11a1739389e9bafa0b89de910105967508b56dbf Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Sun, 07 Sep 2014 11:43:40 -0400
Subject: [PATCH] Enforce relaxed XSS filtering on markup documents

---
 src/main/java/com/gitblit/wicket/pages/DocsPage.java    |    2 
 src/main/java/com/gitblit/wicket/pages/BlobPage.java    |    2 
 src/main/java/com/gitblit/wicket/pages/DocPage.java     |    2 
 src/main/java/com/gitblit/wicket/pages/SummaryPage.java |    2 
 src/main/java/com/gitblit/wicket/WicketUtils.java       |    5 
 src/main/java/com/gitblit/wicket/MarkupProcessor.java   |  930 +++++++++++++++++++++++++++++----------------------------
 6 files changed, 480 insertions(+), 463 deletions(-)

diff --git a/src/main/java/com/gitblit/wicket/MarkupProcessor.java b/src/main/java/com/gitblit/wicket/MarkupProcessor.java
index e7681f2..b203204 100644
--- a/src/main/java/com/gitblit/wicket/MarkupProcessor.java
+++ b/src/main/java/com/gitblit/wicket/MarkupProcessor.java
@@ -1,457 +1,473 @@
-/*
- * Copyright 2013 gitblit.com.
- *
- * 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.gitblit.wicket;
-
-import static org.pegdown.FastEncoder.encode;
-
-import java.io.Serializable;
-import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.wicket.Page;
-import org.apache.wicket.RequestCycle;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.mylyn.wikitext.confluence.core.ConfluenceLanguage;
-import org.eclipse.mylyn.wikitext.core.parser.Attributes;
-import org.eclipse.mylyn.wikitext.core.parser.MarkupParser;
-import org.eclipse.mylyn.wikitext.core.parser.builder.HtmlDocumentBuilder;
-import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage;
-import org.eclipse.mylyn.wikitext.mediawiki.core.MediaWikiLanguage;
-import org.eclipse.mylyn.wikitext.textile.core.TextileLanguage;
-import org.eclipse.mylyn.wikitext.tracwiki.core.TracWikiLanguage;
-import org.eclipse.mylyn.wikitext.twiki.core.TWikiLanguage;
-import org.pegdown.DefaultVerbatimSerializer;
-import org.pegdown.LinkRenderer;
-import org.pegdown.ToHtmlSerializer;
-import org.pegdown.VerbatimSerializer;
-import org.pegdown.ast.ExpImageNode;
-import org.pegdown.ast.RefImageNode;
-import org.pegdown.ast.WikiLinkNode;
-import org.pegdown.plugins.ToHtmlSerializerPlugin;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.gitblit.IStoredSettings;
-import com.gitblit.Keys;
-import com.gitblit.models.PathModel;
-import com.gitblit.servlet.RawServlet;
-import com.gitblit.utils.JGitUtils;
-import com.gitblit.utils.MarkdownUtils;
-import com.gitblit.utils.StringUtils;
-import com.gitblit.wicket.pages.DocPage;
-import com.google.common.base.Joiner;
-
-/**
- * Processes markup content and generates html with repository-relative page and
- * image linking.
- *
- * @author James Moger
- *
- */
-public class MarkupProcessor {
-
-	public enum MarkupSyntax {
-		PLAIN, MARKDOWN, TWIKI, TRACWIKI, TEXTILE, MEDIAWIKI, CONFLUENCE
-	}
-
-	private Logger logger = LoggerFactory.getLogger(getClass());
-
-	private final IStoredSettings settings;
-
-	public MarkupProcessor(IStoredSettings settings) {
-		this.settings = settings;
-	}
-
-	public List<String> getMarkupExtensions() {
-		List<String> list = new ArrayList<String>();
-		list.addAll(settings.getStrings(Keys.web.confluenceExtensions));
-		list.addAll(settings.getStrings(Keys.web.markdownExtensions));
-		list.addAll(settings.getStrings(Keys.web.mediawikiExtensions));
-		list.addAll(settings.getStrings(Keys.web.textileExtensions));
-		list.addAll(settings.getStrings(Keys.web.tracwikiExtensions));
-		list.addAll(settings.getStrings(Keys.web.twikiExtensions));
-		return list;
-	}
-
-	public List<String> getAllExtensions() {
-		List<String> list = getMarkupExtensions();
-		list.add("txt");
-		list.add("TXT");
-		return list;
-	}
-
-	private List<String> getRoots() {
-		return settings.getStrings(Keys.web.documents);
-	}
-
-	private String [] getEncodings() {
-		return settings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
-	}
-
-	private MarkupSyntax determineSyntax(String documentPath) {
-		String ext = StringUtils.getFileExtension(documentPath).toLowerCase();
-		if (StringUtils.isEmpty(ext)) {
-			return MarkupSyntax.PLAIN;
-		}
-
-		if (settings.getStrings(Keys.web.confluenceExtensions).contains(ext)) {
-			return MarkupSyntax.CONFLUENCE;
-		} else if (settings.getStrings(Keys.web.markdownExtensions).contains(ext)) {
-			return MarkupSyntax.MARKDOWN;
-		} else if (settings.getStrings(Keys.web.mediawikiExtensions).contains(ext)) {
-			return MarkupSyntax.MEDIAWIKI;
-		} else if (settings.getStrings(Keys.web.textileExtensions).contains(ext)) {
-			return MarkupSyntax.TEXTILE;
-		} else if (settings.getStrings(Keys.web.tracwikiExtensions).contains(ext)) {
-			return MarkupSyntax.TRACWIKI;
-		} else if (settings.getStrings(Keys.web.twikiExtensions).contains(ext)) {
-			return MarkupSyntax.TWIKI;
-		}
-
-		return MarkupSyntax.PLAIN;
-	}
-
-	public boolean hasRootDocs(Repository r) {
-		List<String> roots = getRoots();
-		List<String> extensions = getAllExtensions();
-		List<PathModel> paths = JGitUtils.getFilesInPath(r, null, null);
-		for (PathModel path : paths) {
-			if (!path.isTree()) {
-				String ext = StringUtils.getFileExtension(path.name).toLowerCase();
-				String name = StringUtils.stripFileExtension(path.name).toLowerCase();
-
-				if (roots.contains(name)) {
-					if (StringUtils.isEmpty(ext) || extensions.contains(ext)) {
-						return true;
-					}
-				}
-			}
-		}
-		return false;
-	}
-
-	public List<MarkupDocument> getRootDocs(Repository r, String repositoryName, String commitId) {
-		List<String> roots = getRoots();
-		List<MarkupDocument> list = getDocs(r, repositoryName, commitId, roots);
-		return list;
-	}
-
-	public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) {
-		List<MarkupDocument> list = getDocs(r, repositoryName, commitId, Arrays.asList("readme"));
-		if (list.isEmpty()) {
-			return null;
-		}
-		return list.get(0);
-	}
-
-	private List<MarkupDocument> getDocs(Repository r, String repositoryName, String commitId, List<String> names) {
-		List<String> extensions = getAllExtensions();
-		String [] encodings = getEncodings();
-		Map<String, MarkupDocument> map = new HashMap<String, MarkupDocument>();
-		RevCommit commit = JGitUtils.getCommit(r, commitId);
-		List<PathModel> paths = JGitUtils.getFilesInPath(r, null, commit);
-		for (PathModel path : paths) {
-			if (!path.isTree()) {
-				String ext = StringUtils.getFileExtension(path.name).toLowerCase();
-				String name = StringUtils.stripFileExtension(path.name).toLowerCase();
-
-				if (names.contains(name)) {
-					if (StringUtils.isEmpty(ext) || extensions.contains(ext)) {
-						String markup = JGitUtils.getStringContent(r, commit.getTree(), path.name, encodings);
-						MarkupDocument doc = parse(repositoryName, commitId, path.name, markup);
-						map.put(name, doc);
-					}
-				}
-			}
-		}
-		// return document list in requested order
-		List<MarkupDocument> list = new ArrayList<MarkupDocument>();
-		for (String name : names) {
-			if (map.containsKey(name)) {
-				list.add(map.get(name));
-			}
-		}
-		return list;
-	}
-
-	public MarkupDocument parse(String repositoryName, String commitId, String documentPath, String markupText) {
-		final MarkupSyntax syntax = determineSyntax(documentPath);
-		final MarkupDocument doc = new MarkupDocument(documentPath, markupText, syntax);
-
-		if (markupText != null) {
-			try {
-				switch (syntax){
-				case CONFLUENCE:
-					parse(doc, repositoryName, commitId, new ConfluenceLanguage());
-					break;
-				case MARKDOWN:
-					parse(doc, repositoryName, commitId);
-					break;
-				case MEDIAWIKI:
-					parse(doc, repositoryName, commitId, new MediaWikiLanguage());
-					break;
-				case TEXTILE:
-					parse(doc, repositoryName, commitId, new TextileLanguage());
-					break;
-				case TRACWIKI:
-					parse(doc, repositoryName, commitId, new TracWikiLanguage());
-					break;
-				case TWIKI:
-					parse(doc, repositoryName, commitId, new TWikiLanguage());
-					break;
-				default:
-					doc.html = MarkdownUtils.transformPlainText(markupText);
-					break;
-				}
-			} catch (Exception e) {
-				logger.error("failed to transform " + syntax, e);
-			}
-		}
-
-		if (doc.html == null) {
-			// failed to transform markup
-			if (markupText == null) {
-				markupText = String.format("Document <b>%1$s</b> not found in <em>%2$s</em>", documentPath, repositoryName);
-			}
-			markupText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{2}", "Error", "failed to parse markup", markupText);
-			doc.html = StringUtils.breakLinesForHtml(markupText);
-		}
-
-		return doc;
-	}
-
-	/**
-	 * Parses the markup using the specified markup language
-	 *
-	 * @param doc
-	 * @param repositoryName
-	 * @param commitId
-	 * @param lang
-	 */
-	private void parse(final MarkupDocument doc, final String repositoryName, final String commitId, MarkupLanguage lang) {
-		StringWriter writer = new StringWriter();
-		HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer) {
-
-			@Override
-			public void image(Attributes attributes, String imagePath) {
-				String url;
-				if (imagePath.indexOf("://") == -1) {
-					// relative image
-					String path = doc.getRelativePath(imagePath);
-					String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
-					url = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
-				} else {
-					// absolute image
-					url = imagePath;
-				}
-				super.image(attributes, url);
-			}
-
-			@Override
-			public void link(Attributes attributes, String hrefOrHashName, String text) {
-				String url;
-				if (hrefOrHashName.charAt(0) != '#') {
-					if (hrefOrHashName.indexOf("://") == -1) {
-						// relative link
-						String path = doc.getRelativePath(hrefOrHashName);
-						url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
-					} else {
-						// absolute link
-						url = hrefOrHashName;
-					}
-				} else {
-					// page-relative hash link
-					url = hrefOrHashName;
-				}
-				super.link(attributes, url, text);
-			}
-		};
-
-		// avoid the <html> and <body> tags
-		builder.setEmitAsDocument(false);
-
-		MarkupParser parser = new MarkupParser(lang);
-		parser.setBuilder(builder);
-		parser.parse(doc.markup);
-		doc.html = writer.toString();
-	}
-
-	/**
-	 * Parses the document as Markdown using Pegdown.
-	 *
-	 * @param doc
-	 * @param repositoryName
-	 * @param commitId
-	 */
-	private void parse(final MarkupDocument doc, final String repositoryName, final String commitId) {
-		LinkRenderer renderer = new LinkRenderer() {
-
-			@Override
-			public Rendering render(ExpImageNode node, String text) {
-				if (node.url.indexOf("://") == -1) {
-					// repository-relative image link
-					String path = doc.getRelativePath(node.url);
-					String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
-					String url = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
-					return new Rendering(url, text);
-				}
-				// absolute image link
-				return new Rendering(node.url, text);
-			}
-
-			@Override
-			public Rendering render(RefImageNode node, String url, String title, String alt) {
-				Rendering rendering;
-				if (url.indexOf("://") == -1) {
-					// repository-relative image link
-					String path = doc.getRelativePath(url);
-					String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
-					String wurl = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
-					rendering = new Rendering(wurl, alt);
-				} else {
-					// absolute image link
-					rendering = new Rendering(url, alt);
-				}
-				return StringUtils.isEmpty(title) ? rendering : rendering.withAttribute("title", encode(title));
-			}
-
-			@Override
-			public Rendering render(WikiLinkNode node) {
-				String path = doc.getRelativePath(node.getText());
-				String name = getDocumentName(path);
-				String url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
-				return new Rendering(url, name);
-			}
-		};
-		doc.html = MarkdownUtils.transformMarkdown(doc.markup, renderer);
-	}
-
-	private String getWicketUrl(Class<? extends Page> pageClass, final String repositoryName, final String commitId, final String document) {
-		String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/");
-		String encodedPath = document.replace(' ', '-');
-		try {
-			encodedPath = URLEncoder.encode(encodedPath, "UTF-8");
-		} catch (UnsupportedEncodingException e) {
-			logger.error(null, e);
-		}
-		encodedPath = encodedPath.replace("/", fsc).replace("%2F", fsc);
-
-		String url = RequestCycle.get().urlFor(pageClass, WicketUtils.newPathParameter(repositoryName, commitId, encodedPath)).toString();
-		return url;
-	}
-
-	private String getDocumentName(final String document) {
-		// extract document name
-		String name = StringUtils.stripFileExtension(document);
-		name = name.replace('_', ' ');
-		if (name.indexOf('/') > -1) {
-			name = name.substring(name.lastIndexOf('/') + 1);
-		}
-		return name;
-	}
-
-	public static class MarkupDocument implements Serializable {
-
-		private static final long serialVersionUID = 1L;
-
-		public final String documentPath;
-		public final String markup;
-		public final MarkupSyntax syntax;
-		public String html;
-
-		MarkupDocument(String documentPath, String markup, MarkupSyntax syntax) {
-			this.documentPath = documentPath;
-			this.markup = markup;
-			this.syntax = syntax;
-		}
-
-		String getCurrentPath() {
-			String basePath = "";
-			if (documentPath.indexOf('/') > -1) {
-				basePath = documentPath.substring(0, documentPath.lastIndexOf('/') + 1);
-				if (basePath.charAt(0) == '/') {
-					return basePath.substring(1);
-				}
-			}
-			return basePath;
-		}
-
-		String getRelativePath(String ref) {
-			if (ref.charAt(0) == '/') {
-				// absolute path in repository
-				return ref.substring(1);
-			} else {
-				// resolve relative repository path
-				String cp = getCurrentPath();
-				if (StringUtils.isEmpty(cp)) {
-					return ref;
-				}
-				// this is a simple relative path resolver
-				List<String> currPathStrings = new ArrayList<String>(Arrays.asList(cp.split("/")));
-				String file = ref;
-				while (file.startsWith("../")) {
-					// strip ../ from the file reference
-					// drop the last path element
-					file = file.substring(3);
-					currPathStrings.remove(currPathStrings.size() - 1);
-				}
-				currPathStrings.add(file);
-				String path = Joiner.on("/").join(currPathStrings);
-				return path;
-			}
-		}
-	}
-
-	/**
-	 * This class implements a workaround for a bug reported in issue-379.
-	 * The bug was introduced by my own pegdown pull request #115.
-	 *
-	 * @author James Moger
-	 *
-	 */
-	public static class WorkaroundHtmlSerializer extends ToHtmlSerializer {
-
-		 public WorkaroundHtmlSerializer(final LinkRenderer linkRenderer) {
-			 super(linkRenderer,
-					 Collections.<String, VerbatimSerializer>singletonMap(VerbatimSerializer.DEFAULT, DefaultVerbatimSerializer.INSTANCE),
-					 Collections.<ToHtmlSerializerPlugin>emptyList());
-		    }
-	    private void printAttribute(String name, String value) {
-	        printer.print(' ').print(name).print('=').print('"').print(value).print('"');
-	    }
-
-	    /* Reimplement print image tag to eliminate a trailing double-quote */
-		@Override
-	    protected void printImageTag(LinkRenderer.Rendering rendering) {
-	        printer.print("<img");
-	        printAttribute("src", rendering.href);
-	        printAttribute("alt", rendering.text);
-	        for (LinkRenderer.Attribute attr : rendering.attributes) {
-	            printAttribute(attr.name, attr.value);
-	        }
-	        printer.print("/>");
-	    }
-	}
-}
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * 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.gitblit.wicket;
+
+import static org.pegdown.FastEncoder.encode;
+
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.Page;
+import org.apache.wicket.RequestCycle;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.mylyn.wikitext.confluence.core.ConfluenceLanguage;
+import org.eclipse.mylyn.wikitext.core.parser.Attributes;
+import org.eclipse.mylyn.wikitext.core.parser.MarkupParser;
+import org.eclipse.mylyn.wikitext.core.parser.builder.HtmlDocumentBuilder;
+import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage;
+import org.eclipse.mylyn.wikitext.mediawiki.core.MediaWikiLanguage;
+import org.eclipse.mylyn.wikitext.textile.core.TextileLanguage;
+import org.eclipse.mylyn.wikitext.tracwiki.core.TracWikiLanguage;
+import org.eclipse.mylyn.wikitext.twiki.core.TWikiLanguage;
+import org.pegdown.DefaultVerbatimSerializer;
+import org.pegdown.LinkRenderer;
+import org.pegdown.ToHtmlSerializer;
+import org.pegdown.VerbatimSerializer;
+import org.pegdown.ast.ExpImageNode;
+import org.pegdown.ast.RefImageNode;
+import org.pegdown.ast.WikiLinkNode;
+import org.pegdown.plugins.ToHtmlSerializerPlugin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.models.PathModel;
+import com.gitblit.servlet.RawServlet;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.XssFilter;
+import com.gitblit.wicket.pages.DocPage;
+import com.google.common.base.Joiner;
+
+/**
+ * Processes markup content and generates html with repository-relative page and
+ * image linking.
+ *
+ * @author James Moger
+ *
+ */
+public class MarkupProcessor {
+
+	public enum MarkupSyntax {
+		PLAIN, MARKDOWN, TWIKI, TRACWIKI, TEXTILE, MEDIAWIKI, CONFLUENCE
+	}
+
+	private Logger logger = LoggerFactory.getLogger(getClass());
+
+	private final IStoredSettings settings;
+
+	private final XssFilter xssFilter;
+
+	public static List<String> getMarkupExtensions(IStoredSettings settings) {
+		List<String> list = new ArrayList<String>();
+		list.addAll(settings.getStrings(Keys.web.confluenceExtensions));
+		list.addAll(settings.getStrings(Keys.web.markdownExtensions));
+		list.addAll(settings.getStrings(Keys.web.mediawikiExtensions));
+		list.addAll(settings.getStrings(Keys.web.textileExtensions));
+		list.addAll(settings.getStrings(Keys.web.tracwikiExtensions));
+		list.addAll(settings.getStrings(Keys.web.twikiExtensions));
+		return list;
+	}
+
+	public MarkupProcessor(IStoredSettings settings, XssFilter xssFilter) {
+		this.settings = settings;
+		this.xssFilter = xssFilter;
+	}
+
+	public List<String> getMarkupExtensions() {
+		return getMarkupExtensions(settings);
+	}
+
+	public List<String> getAllExtensions() {
+		List<String> list = getMarkupExtensions(settings);
+		list.add("txt");
+		list.add("TXT");
+		return list;
+	}
+
+	private List<String> getRoots() {
+		return settings.getStrings(Keys.web.documents);
+	}
+
+	private String [] getEncodings() {
+		return settings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
+	}
+
+	private MarkupSyntax determineSyntax(String documentPath) {
+		String ext = StringUtils.getFileExtension(documentPath).toLowerCase();
+		if (StringUtils.isEmpty(ext)) {
+			return MarkupSyntax.PLAIN;
+		}
+
+		if (settings.getStrings(Keys.web.confluenceExtensions).contains(ext)) {
+			return MarkupSyntax.CONFLUENCE;
+		} else if (settings.getStrings(Keys.web.markdownExtensions).contains(ext)) {
+			return MarkupSyntax.MARKDOWN;
+		} else if (settings.getStrings(Keys.web.mediawikiExtensions).contains(ext)) {
+			return MarkupSyntax.MEDIAWIKI;
+		} else if (settings.getStrings(Keys.web.textileExtensions).contains(ext)) {
+			return MarkupSyntax.TEXTILE;
+		} else if (settings.getStrings(Keys.web.tracwikiExtensions).contains(ext)) {
+			return MarkupSyntax.TRACWIKI;
+		} else if (settings.getStrings(Keys.web.twikiExtensions).contains(ext)) {
+			return MarkupSyntax.TWIKI;
+		}
+
+		return MarkupSyntax.PLAIN;
+	}
+
+	public boolean hasRootDocs(Repository r) {
+		List<String> roots = getRoots();
+		List<String> extensions = getAllExtensions();
+		List<PathModel> paths = JGitUtils.getFilesInPath(r, null, null);
+		for (PathModel path : paths) {
+			if (!path.isTree()) {
+				String ext = StringUtils.getFileExtension(path.name).toLowerCase();
+				String name = StringUtils.stripFileExtension(path.name).toLowerCase();
+
+				if (roots.contains(name)) {
+					if (StringUtils.isEmpty(ext) || extensions.contains(ext)) {
+						return true;
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	public List<MarkupDocument> getRootDocs(Repository r, String repositoryName, String commitId) {
+		List<String> roots = getRoots();
+		List<MarkupDocument> list = getDocs(r, repositoryName, commitId, roots);
+		return list;
+	}
+
+	public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) {
+		List<MarkupDocument> list = getDocs(r, repositoryName, commitId, Arrays.asList("readme"));
+		if (list.isEmpty()) {
+			return null;
+		}
+		return list.get(0);
+	}
+
+	private List<MarkupDocument> getDocs(Repository r, String repositoryName, String commitId, List<String> names) {
+		List<String> extensions = getAllExtensions();
+		String [] encodings = getEncodings();
+		Map<String, MarkupDocument> map = new HashMap<String, MarkupDocument>();
+		RevCommit commit = JGitUtils.getCommit(r, commitId);
+		List<PathModel> paths = JGitUtils.getFilesInPath(r, null, commit);
+		for (PathModel path : paths) {
+			if (!path.isTree()) {
+				String ext = StringUtils.getFileExtension(path.name).toLowerCase();
+				String name = StringUtils.stripFileExtension(path.name).toLowerCase();
+
+				if (names.contains(name)) {
+					if (StringUtils.isEmpty(ext) || extensions.contains(ext)) {
+						String markup = JGitUtils.getStringContent(r, commit.getTree(), path.name, encodings);
+						MarkupDocument doc = parse(repositoryName, commitId, path.name, markup);
+						map.put(name, doc);
+					}
+				}
+			}
+		}
+		// return document list in requested order
+		List<MarkupDocument> list = new ArrayList<MarkupDocument>();
+		for (String name : names) {
+			if (map.containsKey(name)) {
+				list.add(map.get(name));
+			}
+		}
+		return list;
+	}
+
+	public MarkupDocument parse(String repositoryName, String commitId, String documentPath, String markupText) {
+		final MarkupSyntax syntax = determineSyntax(documentPath);
+		final MarkupDocument doc = new MarkupDocument(documentPath, markupText, syntax);
+
+		if (markupText != null) {
+			try {
+				switch (syntax){
+				case CONFLUENCE:
+					parse(doc, repositoryName, commitId, new ConfluenceLanguage());
+					break;
+				case MARKDOWN:
+					parse(doc, repositoryName, commitId);
+					break;
+				case MEDIAWIKI:
+					parse(doc, repositoryName, commitId, new MediaWikiLanguage());
+					break;
+				case TEXTILE:
+					parse(doc, repositoryName, commitId, new TextileLanguage());
+					break;
+				case TRACWIKI:
+					parse(doc, repositoryName, commitId, new TracWikiLanguage());
+					break;
+				case TWIKI:
+					parse(doc, repositoryName, commitId, new TWikiLanguage());
+					break;
+				default:
+					doc.html = MarkdownUtils.transformPlainText(markupText);
+					break;
+				}
+			} catch (Exception e) {
+				logger.error("failed to transform " + syntax, e);
+			}
+		}
+
+		if (doc.html == null) {
+			// failed to transform markup
+			if (markupText == null) {
+				markupText = String.format("Document <b>%1$s</b> not found in <em>%2$s</em>", documentPath, repositoryName);
+			}
+			markupText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{2}", "Error", "failed to parse markup", markupText);
+			doc.html = StringUtils.breakLinesForHtml(markupText);
+		}
+
+		return doc;
+	}
+
+	/**
+	 * Parses the markup using the specified markup language
+	 *
+	 * @param doc
+	 * @param repositoryName
+	 * @param commitId
+	 * @param lang
+	 */
+	private void parse(final MarkupDocument doc, final String repositoryName, final String commitId, MarkupLanguage lang) {
+		StringWriter writer = new StringWriter();
+		HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer) {
+
+			@Override
+			public void image(Attributes attributes, String imagePath) {
+				String url;
+				if (imagePath.indexOf("://") == -1) {
+					// relative image
+					String path = doc.getRelativePath(imagePath);
+					String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
+					url = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
+				} else {
+					// absolute image
+					url = imagePath;
+				}
+				super.image(attributes, url);
+			}
+
+			@Override
+			public void link(Attributes attributes, String hrefOrHashName, String text) {
+				String url;
+				if (hrefOrHashName.charAt(0) != '#') {
+					if (hrefOrHashName.indexOf("://") == -1) {
+						// relative link
+						String path = doc.getRelativePath(hrefOrHashName);
+						url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
+					} else {
+						// absolute link
+						url = hrefOrHashName;
+					}
+				} else {
+					// page-relative hash link
+					url = hrefOrHashName;
+				}
+				super.link(attributes, url, text);
+			}
+		};
+
+		// avoid the <html> and <body> tags
+		builder.setEmitAsDocument(false);
+
+		MarkupParser parser = new MarkupParser(lang);
+		parser.setBuilder(builder);
+		parser.parse(doc.markup);
+
+		final String content = writer.toString();
+		final String safeContent = xssFilter.relaxed(content);
+
+		doc.html = safeContent;
+	}
+
+	/**
+	 * Parses the document as Markdown using Pegdown.
+	 *
+	 * @param doc
+	 * @param repositoryName
+	 * @param commitId
+	 */
+	private void parse(final MarkupDocument doc, final String repositoryName, final String commitId) {
+		LinkRenderer renderer = new LinkRenderer() {
+
+			@Override
+			public Rendering render(ExpImageNode node, String text) {
+				if (node.url.indexOf("://") == -1) {
+					// repository-relative image link
+					String path = doc.getRelativePath(node.url);
+					String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
+					String url = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
+					return new Rendering(url, text);
+				}
+				// absolute image link
+				return new Rendering(node.url, text);
+			}
+
+			@Override
+			public Rendering render(RefImageNode node, String url, String title, String alt) {
+				Rendering rendering;
+				if (url.indexOf("://") == -1) {
+					// repository-relative image link
+					String path = doc.getRelativePath(url);
+					String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
+					String wurl = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
+					rendering = new Rendering(wurl, alt);
+				} else {
+					// absolute image link
+					rendering = new Rendering(url, alt);
+				}
+				return StringUtils.isEmpty(title) ? rendering : rendering.withAttribute("title", encode(title));
+			}
+
+			@Override
+			public Rendering render(WikiLinkNode node) {
+				String path = doc.getRelativePath(node.getText());
+				String name = getDocumentName(path);
+				String url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
+				return new Rendering(url, name);
+			}
+		};
+
+		final String content = MarkdownUtils.transformMarkdown(doc.markup, renderer);
+		final String safeContent = xssFilter.relaxed(content);
+
+		doc.html = safeContent;
+	}
+
+	private String getWicketUrl(Class<? extends Page> pageClass, final String repositoryName, final String commitId, final String document) {
+		String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/");
+		String encodedPath = document.replace(' ', '-');
+		try {
+			encodedPath = URLEncoder.encode(encodedPath, "UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			logger.error(null, e);
+		}
+		encodedPath = encodedPath.replace("/", fsc).replace("%2F", fsc);
+
+		String url = RequestCycle.get().urlFor(pageClass, WicketUtils.newPathParameter(repositoryName, commitId, encodedPath)).toString();
+		return url;
+	}
+
+	private String getDocumentName(final String document) {
+		// extract document name
+		String name = StringUtils.stripFileExtension(document);
+		name = name.replace('_', ' ');
+		if (name.indexOf('/') > -1) {
+			name = name.substring(name.lastIndexOf('/') + 1);
+		}
+		return name;
+	}
+
+	public static class MarkupDocument implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+
+		public final String documentPath;
+		public final String markup;
+		public final MarkupSyntax syntax;
+		public String html;
+
+		MarkupDocument(String documentPath, String markup, MarkupSyntax syntax) {
+			this.documentPath = documentPath;
+			this.markup = markup;
+			this.syntax = syntax;
+		}
+
+		String getCurrentPath() {
+			String basePath = "";
+			if (documentPath.indexOf('/') > -1) {
+				basePath = documentPath.substring(0, documentPath.lastIndexOf('/') + 1);
+				if (basePath.charAt(0) == '/') {
+					return basePath.substring(1);
+				}
+			}
+			return basePath;
+		}
+
+		String getRelativePath(String ref) {
+			if (ref.charAt(0) == '/') {
+				// absolute path in repository
+				return ref.substring(1);
+			} else {
+				// resolve relative repository path
+				String cp = getCurrentPath();
+				if (StringUtils.isEmpty(cp)) {
+					return ref;
+				}
+				// this is a simple relative path resolver
+				List<String> currPathStrings = new ArrayList<String>(Arrays.asList(cp.split("/")));
+				String file = ref;
+				while (file.startsWith("../")) {
+					// strip ../ from the file reference
+					// drop the last path element
+					file = file.substring(3);
+					currPathStrings.remove(currPathStrings.size() - 1);
+				}
+				currPathStrings.add(file);
+				String path = Joiner.on("/").join(currPathStrings);
+				return path;
+			}
+		}
+	}
+
+	/**
+	 * This class implements a workaround for a bug reported in issue-379.
+	 * The bug was introduced by my own pegdown pull request #115.
+	 *
+	 * @author James Moger
+	 *
+	 */
+	public static class WorkaroundHtmlSerializer extends ToHtmlSerializer {
+
+		 public WorkaroundHtmlSerializer(final LinkRenderer linkRenderer) {
+			 super(linkRenderer,
+					 Collections.<String, VerbatimSerializer>singletonMap(VerbatimSerializer.DEFAULT, DefaultVerbatimSerializer.INSTANCE),
+					 Collections.<ToHtmlSerializerPlugin>emptyList());
+		    }
+	    private void printAttribute(String name, String value) {
+	        printer.print(' ').print(name).print('=').print('"').print(value).print('"');
+	    }
+
+	    /* Reimplement print image tag to eliminate a trailing double-quote */
+		@Override
+	    protected void printImageTag(LinkRenderer.Rendering rendering) {
+	        printer.print("<img");
+	        printAttribute("src", rendering.href);
+	        printAttribute("alt", rendering.text);
+	        for (LinkRenderer.Attribute attr : rendering.attributes) {
+	            printAttribute(attr.name, attr.value);
+	        }
+	        printer.print("/>");
+	    }
+	}
+}
diff --git a/src/main/java/com/gitblit/wicket/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java
index 687f010..d47390d 100644
--- a/src/main/java/com/gitblit/wicket/WicketUtils.java
+++ b/src/main/java/com/gitblit/wicket/WicketUtils.java
@@ -42,6 +42,7 @@
 import com.gitblit.Constants;
 import com.gitblit.Constants.AccessPermission;
 import com.gitblit.Constants.FederationPullStatus;
+import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
 import com.gitblit.models.FederationModel;
 import com.gitblit.models.Metric;
@@ -186,9 +187,9 @@
 			return newImage(wicketId, "file_settings_16x16.png");
 		}
 
-		MarkupProcessor processor = new MarkupProcessor(GitBlitWebApp.get().settings());
 		String ext = StringUtils.getFileExtension(filename).toLowerCase();
-		if (processor.getMarkupExtensions().contains(ext)) {
+		IStoredSettings settings = GitBlitWebApp.get().settings();
+		if (MarkupProcessor.getMarkupExtensions(settings).contains(ext)) {
 			return newImage(wicketId, "file_world_16x16.png");
 		}
 		return newImage(wicketId, "file_16x16.png");
diff --git a/src/main/java/com/gitblit/wicket/pages/BlobPage.java b/src/main/java/com/gitblit/wicket/pages/BlobPage.java
index 0938fcd..e84056b 100644
--- a/src/main/java/com/gitblit/wicket/pages/BlobPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/BlobPage.java
@@ -79,7 +79,7 @@
 			}
 
 			// see if we should redirect to the doc page
-			MarkupProcessor processor = new MarkupProcessor(app().settings());
+			MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter());
 			for (String ext : processor.getMarkupExtensions()) {
 				if (ext.equals(extension)) {
 					setResponsePage(DocPage.class, params);
diff --git a/src/main/java/com/gitblit/wicket/pages/DocPage.java b/src/main/java/com/gitblit/wicket/pages/DocPage.java
index c06d806..567c6fb 100644
--- a/src/main/java/com/gitblit/wicket/pages/DocPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/DocPage.java
@@ -43,7 +43,7 @@
 		super(params);
 
 		final String path = WicketUtils.getPath(params).replace("%2f", "/").replace("%2F", "/");
-		MarkupProcessor processor = new MarkupProcessor(app().settings());
+		MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter());
 
 		Repository r = getRepository();
 		RevCommit commit = JGitUtils.getCommit(r, objectId);
diff --git a/src/main/java/com/gitblit/wicket/pages/DocsPage.java b/src/main/java/com/gitblit/wicket/pages/DocsPage.java
index fc56ee0..a3d0f21 100644
--- a/src/main/java/com/gitblit/wicket/pages/DocsPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/DocsPage.java
@@ -49,7 +49,7 @@
 	public DocsPage(PageParameters params) {
 		super(params);
 
-		MarkupProcessor processor = new MarkupProcessor(app().settings());
+		MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter());
 
 		Repository r = getRepository();
 		RevCommit head = JGitUtils.getCommit(r, null);
diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
index 090c095..3cfa152 100644
--- a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
@@ -138,7 +138,7 @@
 			MarkupDocument markupDoc = null;
 			RevCommit head = JGitUtils.getCommit(r, null);
 			if (head != null) {
-				MarkupProcessor processor = new MarkupProcessor(app().settings());
+				MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter());
 				markupDoc = processor.getReadme(r, repositoryName, getBestCommitId(head));
 			}
 			if (markupDoc == null || markupDoc.markup == null) {

--
Gitblit v1.9.1