From 11a1739389e9bafa0b89de910105967508b56dbf Mon Sep 17 00:00:00 2001 From: James Moger Date: Sun, 7 Sep 2014 11:21:59 -0400 Subject: [PATCH] Enforce relaxed XSS filtering on markup documents --- .../com/gitblit/wicket/MarkupProcessor.java | 930 +++++++++--------- .../java/com/gitblit/wicket/WicketUtils.java | 5 +- .../com/gitblit/wicket/pages/BlobPage.java | 2 +- .../com/gitblit/wicket/pages/DocPage.java | 2 +- .../com/gitblit/wicket/pages/DocsPage.java | 2 +- .../com/gitblit/wicket/pages/SummaryPage.java | 2 +- 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 e7681f2c..b2032049 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 getMarkupExtensions() { - List list = new ArrayList(); - 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 getAllExtensions() { - List list = getMarkupExtensions(); - list.add("txt"); - list.add("TXT"); - return list; - } - - private List 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 roots = getRoots(); - List extensions = getAllExtensions(); - List 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 getRootDocs(Repository r, String repositoryName, String commitId) { - List roots = getRoots(); - List list = getDocs(r, repositoryName, commitId, roots); - return list; - } - - public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) { - List list = getDocs(r, repositoryName, commitId, Arrays.asList("readme")); - if (list.isEmpty()) { - return null; - } - return list.get(0); - } - - private List getDocs(Repository r, String repositoryName, String commitId, List names) { - List extensions = getAllExtensions(); - String [] encodings = getEncodings(); - Map map = new HashMap(); - RevCommit commit = JGitUtils.getCommit(r, commitId); - List 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 list = new ArrayList(); - 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 %1$s not found in %2$s", documentPath, repositoryName); - } - markupText = MessageFormat.format("
{0}: {1}
{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 and 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 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 currPathStrings = new ArrayList(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.singletonMap(VerbatimSerializer.DEFAULT, DefaultVerbatimSerializer.INSTANCE), - Collections.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(""); - } - } -} +/* + * 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 getMarkupExtensions(IStoredSettings settings) { + List list = new ArrayList(); + 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 getMarkupExtensions() { + return getMarkupExtensions(settings); + } + + public List getAllExtensions() { + List list = getMarkupExtensions(settings); + list.add("txt"); + list.add("TXT"); + return list; + } + + private List 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 roots = getRoots(); + List extensions = getAllExtensions(); + List 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 getRootDocs(Repository r, String repositoryName, String commitId) { + List roots = getRoots(); + List list = getDocs(r, repositoryName, commitId, roots); + return list; + } + + public MarkupDocument getReadme(Repository r, String repositoryName, String commitId) { + List list = getDocs(r, repositoryName, commitId, Arrays.asList("readme")); + if (list.isEmpty()) { + return null; + } + return list.get(0); + } + + private List getDocs(Repository r, String repositoryName, String commitId, List names) { + List extensions = getAllExtensions(); + String [] encodings = getEncodings(); + Map map = new HashMap(); + RevCommit commit = JGitUtils.getCommit(r, commitId); + List 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 list = new ArrayList(); + 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 %1$s not found in %2$s", documentPath, repositoryName); + } + markupText = MessageFormat.format("
{0}: {1}
{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 and 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 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 currPathStrings = new ArrayList(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.singletonMap(VerbatimSerializer.DEFAULT, DefaultVerbatimSerializer.INSTANCE), + Collections.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(""); + } + } +} diff --git a/src/main/java/com/gitblit/wicket/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java index 687f0105..d47390d4 100644 --- a/src/main/java/com/gitblit/wicket/WicketUtils.java +++ b/src/main/java/com/gitblit/wicket/WicketUtils.java @@ -42,6 +42,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType; 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 @@ public class WicketUtils { 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 0938fcde..e84056b3 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 @@ public class BlobPage extends RepositoryPage { } // 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 c06d8065..567c6fbd 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 @@ public class DocPage extends RepositoryPage { 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 fc56ee07..a3d0f214 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 class DocsPage extends RepositoryPage { 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 090c0952..3cfa152e 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 @@ public class SummaryPage extends RepositoryPage { 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) { -- 2.39.5