James Moger
2014-09-07 11a1739389e9bafa0b89de910105967508b56dbf
Enforce relaxed XSS filtering on markup documents
6 files modified
943 ■■■■ changed files
src/main/java/com/gitblit/wicket/MarkupProcessor.java 930 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/WicketUtils.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/BlobPage.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DocPage.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DocsPage.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/SummaryPage.java 2 ●●● patch | view | raw | blame | history
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("/>");
        }
    }
}
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");
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);
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);
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);
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) {