]> source.dussan.org Git - gitblit.git/commitdiff
Enforce relaxed XSS filtering on markup documents
authorJames Moger <james.moger@gitblit.com>
Sun, 7 Sep 2014 15:21:59 +0000 (11:21 -0400)
committerJames Moger <james.moger@gitblit.com>
Sun, 7 Sep 2014 15:43:40 +0000 (11:43 -0400)
src/main/java/com/gitblit/wicket/MarkupProcessor.java
src/main/java/com/gitblit/wicket/WicketUtils.java
src/main/java/com/gitblit/wicket/pages/BlobPage.java
src/main/java/com/gitblit/wicket/pages/DocPage.java
src/main/java/com/gitblit/wicket/pages/DocsPage.java
src/main/java/com/gitblit/wicket/pages/SummaryPage.java

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