James Moger
2014-09-07 f7174e6984c08a153d1ba198c4bffe68c5afd873
Merge branch 'ticket/164' into develop
3 files added
33 files modified
1776 ■■■■■ changed files
.classpath 1 ●●●● patch | view | raw | blame | history
build.moxie 1 ●●●● patch | view | raw | blame | history
gitblit.iml 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/FederationClient.java 5 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/MigrateTickets.java 5 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/ReindexTickets.java 5 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/guice/CoreModule.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/GitblitManager.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/IRuntimeManager.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/RuntimeManager.java 21 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/JSoupXssFilter.java 92 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/XssFilter.java 64 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.java 14 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java 408 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitblitWicketApp.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/MarkupProcessor.java 930 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/SafeTextModel.java 96 ●●●●● 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/EditTicketPage.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/NewTicketPage.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java 3 ●●●● 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/panels/CommentPanel.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/MarkdownTextArea.java 6 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/AuthenticationManagerTest.java 5 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/BranchTicketServiceTest.java 6 ●●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/FileTicketServiceTest.java 6 ●●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java 8 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/LdapAuthenticationTest.java 8 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/LuceneExecutorTest.java 5 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/RedisTicketServiceTest.java 6 ●●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java 8 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java 7 ●●●●● patch | view | raw | blame | history
.classpath
@@ -77,6 +77,7 @@
    <classpathentry kind="lib" path="ext/commons-pool2-2.0.jar" sourcepath="ext/src/commons-pool2-2.0.jar" />
    <classpathentry kind="lib" path="ext/pf4j-0.9.0.jar" sourcepath="ext/src/pf4j-0.9.0.jar" />
    <classpathentry kind="lib" path="ext/tika-core-1.5.jar" sourcepath="ext/src/tika-core-1.5.jar" />
    <classpathentry kind="lib" path="ext/jsoup-1.7.3.jar" sourcepath="ext/src/jsoup-1.7.3.jar" />
    <classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" />
    <classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" />
    <classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" />
build.moxie
@@ -178,6 +178,7 @@
- compile 'redis.clients:jedis:2.3.1' :war
- compile 'ro.fortsoft.pf4j:pf4j:0.9.0' :war
- compile 'org.apache.tika:tika-core:1.5' :war
- compile 'org.jsoup:jsoup:1.7.3' :war
- test 'junit'
# Dependencies for Selenium web page testing
- test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
gitblit.iml
@@ -801,6 +801,17 @@
        </SOURCES>
      </library>
    </orderEntry>
    <orderEntry type="module-library">
      <library name="jsoup-1.7.3.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/jsoup-1.7.3.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/jsoup-1.7.3.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
    <orderEntry type="module-library" scope="TEST">
      <library name="junit-4.11.jar">
        <CLASSES>
src/main/java/com/gitblit/FederationClient.java
@@ -36,6 +36,8 @@
import com.gitblit.service.FederationPullService;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * Command-line client to pull federated Gitblit repositories.
@@ -92,7 +94,8 @@
        }
        // configure the Gitblit singleton for minimal, non-server operation
        RuntimeManager runtime = new RuntimeManager(settings, baseFolder).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(settings, xssFilter, baseFolder).start();
        NoopNotificationManager notifications = new NoopNotificationManager().start();
        UserManager users = new UserManager(runtime, null).start();
        RepositoryManager repositories = new RepositoryManager(runtime, null, users).start();
src/main/java/com/gitblit/MigrateTickets.java
@@ -39,6 +39,8 @@
import com.gitblit.tickets.ITicketService;
import com.gitblit.tickets.RedisTicketService;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * A command-line tool to move all tickets from one ticket service to another.
@@ -134,7 +136,8 @@
        settings.overrideSetting(Keys.web.activityCacheDays, 0);
        settings.overrideSetting(ITicketService.SETTING_UPDATE_DIFFSTATS, false);
        IRuntimeManager runtimeManager = new RuntimeManager(settings, baseFolder).start();
        XssFilter xssFilter = new AllowXssFilter();
        IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter, baseFolder).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, null, null).start();
        String inputServiceName = settings.getString(Keys.tickets.service, BranchTicketService.class.getSimpleName());
src/main/java/com/gitblit/ReindexTickets.java
@@ -33,6 +33,8 @@
import com.gitblit.tickets.ITicketService;
import com.gitblit.tickets.RedisTicketService;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * A command-line tool to reindex all tickets in all repositories when the
@@ -126,7 +128,8 @@
        settings.overrideSetting(Keys.git.enableMirroring, false);
        settings.overrideSetting(Keys.web.activityCacheDays, 0);
        IRuntimeManager runtimeManager = new RuntimeManager(settings, baseFolder).start();
        XssFilter xssFilter = new AllowXssFilter();
        IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter, baseFolder).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, null, null).start();
        String serviceName = settings.getString(Keys.tickets.service, BranchTicketService.class.getSimpleName());
src/main/java/com/gitblit/guice/CoreModule.java
@@ -39,7 +39,9 @@
import com.gitblit.manager.UserManager;
import com.gitblit.tickets.ITicketService;
import com.gitblit.transport.ssh.IPublicKeyManager;
import com.gitblit.utils.JSoupXssFilter;
import com.gitblit.utils.WorkQueue;
import com.gitblit.utils.XssFilter;
import com.google.inject.AbstractModule;
/**
@@ -54,6 +56,7 @@
    protected void configure() {
        bind(IStoredSettings.class).toInstance(new FileSettings());
        bind(XssFilter.class).to(JSoupXssFilter.class);
        // bind complex providers
        bind(IPublicKeyManager.class).toProvider(IPublicKeyManagerProvider.class);
src/main/java/com/gitblit/manager/GitblitManager.java
@@ -76,6 +76,8 @@
import com.gitblit.transport.ssh.IPublicKeyManager;
import com.gitblit.transport.ssh.SshKey;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.JsonUtils;
import com.gitblit.utils.ObjectCache;
import com.gitblit.utils.StringUtils;
@@ -591,6 +593,11 @@
        return runtimeManager.getInjector();
    }
    @Override
    public XssFilter getXssFilter() {
        return runtimeManager.getXssFilter();
    }
    /*
     * NOTIFICATION MANAGER
     */
src/main/java/com/gitblit/manager/IRuntimeManager.java
@@ -24,6 +24,7 @@
import com.gitblit.IStoredSettings;
import com.gitblit.models.ServerSettings;
import com.gitblit.models.ServerStatus;
import com.gitblit.utils.XssFilter;
import com.google.inject.Injector;
public interface IRuntimeManager extends IManager {
@@ -118,4 +119,11 @@
      * @since 1.4.0
     */
    boolean updateSettings(Map<String, String> updatedSettings);
    /**
     * Returns the HTML sanitizer used to clean user content.
     *
     * @return the HTML sanitizer
     */
    XssFilter getXssFilter();
}
src/main/java/com/gitblit/manager/RuntimeManager.java
@@ -32,6 +32,7 @@
import com.gitblit.models.ServerStatus;
import com.gitblit.models.SettingModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.XssFilter;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
@@ -42,6 +43,8 @@
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final IStoredSettings settings;
    private final XssFilter xssFilter;
    private final ServerStatus serverStatus;
@@ -55,14 +58,15 @@
    private Injector injector;
    @Inject
    public RuntimeManager(IStoredSettings settings) {
        this(settings, null);
    public RuntimeManager(IStoredSettings settings, XssFilter xssFilter) {
        this(settings, xssFilter, null);
    }
    public RuntimeManager(IStoredSettings settings, File baseFolder) {
    public RuntimeManager(IStoredSettings settings, XssFilter xssFilter, File baseFolder) {
        this.settings = settings;
        this.settingsModel = new ServerSettings();
        this.serverStatus = new ServerStatus();
        this.xssFilter = xssFilter;
        this.baseFolder = baseFolder == null ? new File("") : baseFolder;
    }
@@ -229,4 +233,15 @@
        serverStatus.heapFree = Runtime.getRuntime().freeMemory();
        return serverStatus;
    }
    /**
     * Returns the XSS filter.
     *
     * @return the XSS filter
     */
    @Override
    public XssFilter getXssFilter() {
        return xssFilter;
    }
}
src/main/java/com/gitblit/utils/JSoupXssFilter.java
New file
@@ -0,0 +1,92 @@
/*
 * Copyright 2014 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.utils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Cleaner;
import org.jsoup.safety.Whitelist;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
 * Implementation of an XSS filter based on JSoup.
 *
 * @author James Moger
 *
 */
@Singleton
public class JSoupXssFilter implements XssFilter {
     private final Cleaner none;
     private final Cleaner relaxed;
     @Inject
     public JSoupXssFilter() {
         none = new Cleaner(Whitelist.none());
         relaxed = new Cleaner(getRelaxedWhiteList());
    }
    @Override
    public String none(String input) {
        return clean(input, none);
    }
    @Override
    public String relaxed(String input) {
        return clean(input, relaxed);
    }
    protected String clean(String input, Cleaner cleaner) {
        Document unsafe = Jsoup.parse(input);
        Document safe = cleaner.clean(unsafe);
        return safe.body().html();
    }
    /**
     * Builds & returns a loose HTML whitelist similar to Github.
     *
     * https://github.com/github/markup/tree/master#html-sanitization
     * @return a loose HTML whitelist
     */
    protected Whitelist getRelaxedWhiteList() {
        return new Whitelist()
        .addTags(
                "a", "b", "blockquote", "br", "caption", "cite", "code", "col",
                "colgroup", "dd", "del", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6", "hr",
                "i", "img", "ins", "kbd", "li", "ol", "p", "pre", "q", "samp", "small", "strike", "strong",
                "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "u",
                "ul", "var")
        .addAttributes("a", "href", "title")
        .addAttributes("blockquote", "cite")
        .addAttributes("col", "span", "width")
        .addAttributes("colgroup", "span", "width")
        .addAttributes("img", "align", "alt", "height", "src", "title", "width")
        .addAttributes("ol", "start", "type")
        .addAttributes("q", "cite")
        .addAttributes("table", "summary", "width")
        .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
        .addAttributes("th", "abbr", "axis", "colspan", "rowspan", "scope", "width")
        .addAttributes("ul", "type")
        .addEnforcedAttribute("a", "rel", "nofollow")
        ;
    }
}
src/main/java/com/gitblit/utils/XssFilter.java
New file
@@ -0,0 +1,64 @@
/*
 * Copyright 2014 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.utils;
/**
 * Defines the contract for an XSS filter implementation.
 *
 * @author James Moger
 *
 */
public interface XssFilter {
    /**
     * Returns a filtered version of the input value that contains no html
     * elements.
     *
     * @param input
     * @return a plain text value
     */
    String none(String input);
    /**
     * Returns a filtered version of the input that contains structural html
     * elements.
     *
     * @param input
     * @return a filtered html value
     */
    String relaxed(String input);
    /**
     * A NOOP XSS filter.
     *
     * @author James Moger
     *
     */
    public class AllowXssFilter implements XssFilter {
        @Override
        public String none(String input) {
            return input;
        }
        @Override
        public String relaxed(String input) {
            return input;
        }
    }
}
src/main/java/com/gitblit/wicket/GitBlitWebApp.java
@@ -47,6 +47,7 @@
import com.gitblit.manager.IUserManager;
import com.gitblit.tickets.ITicketService;
import com.gitblit.transport.ssh.IPublicKeyManager;
import com.gitblit.utils.XssFilter;
import com.gitblit.wicket.pages.ActivityPage;
import com.gitblit.wicket.pages.BlamePage;
import com.gitblit.wicket.pages.BlobDiffPage;
@@ -109,6 +110,8 @@
    private final IStoredSettings settings;
    private final XssFilter xssFilter;
    private final IRuntimeManager runtimeManager;
    private final IPluginManager pluginManager;
@@ -148,6 +151,7 @@
        this.publicKeyManagerProvider = publicKeyManagerProvider;
        this.ticketServiceProvider = ticketServiceProvider;
        this.settings = runtimeManager.getSettings();
        this.xssFilter = runtimeManager.getXssFilter();
        this.runtimeManager = runtimeManager;
        this.pluginManager = pluginManager;
        this.notificationManager = notificationManager;
@@ -265,7 +269,7 @@
        if (!settings.getBoolean(Keys.web.mountParameters, true)) {
            parameters = new String[] {};
        }
        mount(new GitblitParamUrlCodingStrategy(settings, location, clazz, parameters));
        mount(new GitblitParamUrlCodingStrategy(settings, xssFilter, location, clazz, parameters));
        // map the mount point to the cache control definition
        if (clazz.isAnnotationPresent(CacheControl.class)) {
@@ -322,6 +326,14 @@
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#xssFilter()
     */
    @Override
    public XssFilter xssFilter() {
        return xssFilter;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#isDebugMode()
     */
    @Override
src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java
@@ -1,189 +1,221 @@
/*
 * Copyright 2011 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 java.text.MessageFormat;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.wicket.IRequestTarget;
import org.apache.wicket.Page;
import org.apache.wicket.request.RequestParameters;
import org.apache.wicket.request.target.coding.MixedParamUrlCodingStrategy;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
/**
 * Simple subclass of mixed parameter url coding strategy that works around the
 * encoded forward-slash issue that is present in some servlet containers.
 *
 * https://issues.apache.org/jira/browse/WICKET-1303
 * http://tomcat.apache.org/security-6.html
 *
 * @author James Moger
 *
 */
public class GitblitParamUrlCodingStrategy extends MixedParamUrlCodingStrategy {
    private final String[] parameterNames;
    private Logger logger = LoggerFactory.getLogger(GitblitParamUrlCodingStrategy.class);
    private IStoredSettings settings;
    /**
     * Construct.
     *
     * @param <C>
     * @param mountPath
     *            mount path (not empty)
     * @param bookmarkablePageClass
     *            class of mounted page (not null)
     * @param parameterNames
     *            the parameter names (not null)
     */
    public <C extends Page> GitblitParamUrlCodingStrategy(
            IStoredSettings settings,
            String mountPath,
            Class<C> bookmarkablePageClass, String[] parameterNames) {
        super(mountPath, bookmarkablePageClass, parameterNames);
        this.parameterNames = parameterNames;
        this.settings = settings;
    }
    /**
     * Url encodes a string that is mean for a URL path (e.g., between slashes)
     *
     * @param string
     *            string to be encoded
     * @return encoded string
     */
    @Override
    protected String urlEncodePathComponent(String string) {
        char altChar = settings.getChar(Keys.web.forwardSlashCharacter, '/');
        if (altChar != '/') {
            string = string.replace('/', altChar);
        }
        return super.urlEncodePathComponent(string);
    }
    /**
     * Returns a decoded value of the given value (taken from a URL path
     * section)
     *
     * @param value
     * @return Decodes the value
     */
    @Override
    protected String urlDecodePathComponent(String value) {
        char altChar = settings.getChar(Keys.web.forwardSlashCharacter, '/');
        if (altChar != '/') {
            value = value.replace(altChar, '/');
        }
        return super.urlDecodePathComponent(value);
    }
    /**
     * Gets the decoded request target.
     *
     * @param requestParameters
     *            the request parameters
     * @return the decoded request target
     */
    @Override
    public IRequestTarget decode(RequestParameters requestParameters) {
        final String parametersFragment = requestParameters.getPath().substring(
                getMountPath().length());
        logger.debug(MessageFormat
                .format("REQ: {0} PARAMS {1}", getMountPath(), parametersFragment));
        return super.decode(requestParameters);
    }
    /**
     * @see org.apache.wicket.request.target.coding.AbstractRequestTargetUrlCodingStrategy#appendParameters(org.apache.wicket.util.string.AppendingStringBuffer,
     *      java.util.Map)
     */
    @Override
    protected void appendParameters(AppendingStringBuffer url, Map<String, ?> parameters)
    {
        if (!url.endsWith("/"))
        {
            url.append("/");
        }
        Set<String> parameterNamesToAdd = new HashSet<String>(parameters.keySet());
        // Find index of last specified parameter
        boolean foundParameter = false;
        int lastSpecifiedParameter = parameterNames.length;
        while (lastSpecifiedParameter != 0 && !foundParameter)
        {
            foundParameter = parameters.containsKey(parameterNames[--lastSpecifiedParameter]);
        }
        if (foundParameter)
        {
            for (int i = 0; i <= lastSpecifiedParameter; i++)
            {
                String parameterName = parameterNames[i];
                final Object param = parameters.get(parameterName);
                String value = param instanceof String[] ? ((String[])param)[0] : ((param == null)
                    ? null : param.toString());
                if (value == null)
                {
                    value = "";
                }
                if (!url.endsWith("/"))
                {
                    url.append("/");
                }
                url.append(urlEncodePathComponent(value));
                parameterNamesToAdd.remove(parameterName);
            }
        }
        if (!parameterNamesToAdd.isEmpty())
        {
            boolean first = true;
            for (String parameterName : parameterNamesToAdd)
            {
                final Object param = parameters.get(parameterName);
                if (param instanceof String[]) {
                    String [] values = (String[]) param;
                    for (String value : values) {
                        url.append(first ? '?' : '&');
                        url.append(urlEncodeQueryComponent(parameterName)).append("=").append(
                                urlEncodeQueryComponent(value));
                        first = false;
                    }
                } else {
                    url.append(first ? '?' : '&');
                    String value = String.valueOf(param);
                    url.append(urlEncodeQueryComponent(parameterName)).append("=").append(
                        urlEncodeQueryComponent(value));
                }
                first = false;
            }
        }
    }
/*
 * Copyright 2011 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 java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.wicket.IRequestTarget;
import org.apache.wicket.Page;
import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy;
import org.apache.wicket.request.RequestParameters;
import org.apache.wicket.request.target.coding.MixedParamUrlCodingStrategy;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.utils.XssFilter;
/**
 * Simple subclass of mixed parameter url coding strategy that works around the
 * encoded forward-slash issue that is present in some servlet containers.
 *
 * https://issues.apache.org/jira/browse/WICKET-1303
 * http://tomcat.apache.org/security-6.html
 *
 * @author James Moger
 *
 */
public class GitblitParamUrlCodingStrategy extends MixedParamUrlCodingStrategy {
    private final String[] parameterNames;
    private Logger logger = LoggerFactory.getLogger(GitblitParamUrlCodingStrategy.class);
    private IStoredSettings settings;
    private XssFilter xssFilter;
    /**
     * Construct.
     *
     * @param <C>
     * @param mountPath
     *            mount path (not empty)
     * @param bookmarkablePageClass
     *            class of mounted page (not null)
     * @param parameterNames
     *            the parameter names (not null)
     */
    public <C extends Page> GitblitParamUrlCodingStrategy(
            IStoredSettings settings,
            XssFilter xssFilter,
            String mountPath,
            Class<C> bookmarkablePageClass, String[] parameterNames) {
        super(mountPath, bookmarkablePageClass, parameterNames);
        this.parameterNames = parameterNames;
        this.settings = settings;
        this.xssFilter = xssFilter;
    }
    /**
     * Url encodes a string that is mean for a URL path (e.g., between slashes)
     *
     * @param string
     *            string to be encoded
     * @return encoded string
     */
    @Override
    protected String urlEncodePathComponent(String string) {
        char altChar = settings.getChar(Keys.web.forwardSlashCharacter, '/');
        if (altChar != '/') {
            string = string.replace('/', altChar);
        }
        return super.urlEncodePathComponent(string);
    }
    /**
     * Returns a decoded value of the given value (taken from a URL path
     * section)
     *
     * @param value
     * @return Decodes the value
     */
    @Override
    protected String urlDecodePathComponent(String value) {
        char altChar = settings.getChar(Keys.web.forwardSlashCharacter, '/');
        if (altChar != '/') {
            value = value.replace(altChar, '/');
        }
        return super.urlDecodePathComponent(value);
    }
    /**
     * Gets the decoded request target.
     *
     * @param requestParameters
     *            the request parameters
     * @return the decoded request target
     */
    @Override
    public IRequestTarget decode(RequestParameters requestParameters) {
        Map<String, Object> parameterMap = (Map<String, Object>) requestParameters.getParameters();
        for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
            String parameter = entry.getKey();
            if (parameter.startsWith(WebRequestCodingStrategy.NAME_SPACE)) {
                // ignore Wicket parameters
                continue;
            }
            // sanitize Gitblit request parameters
            Object o = entry.getValue();
            if (o instanceof String) {
                String value = o.toString();
                String safeValue = xssFilter.none(value);
                if (!value.equals(safeValue)) {
                    logger.warn("XSS filter triggered on {} URL parameter: {}={}",
                            getMountPath(), parameter, value);
                    parameterMap.put(parameter, safeValue);
                }
            } else if (o instanceof String[]) {
                String[] values = (String[]) o;
                for (int i = 0; i < values.length; i++) {
                    String value = values[i].toString();
                    String safeValue = xssFilter.none(value);
                    if (!value.equals(safeValue)) {
                        logger.warn("XSS filter triggered on {} URL parameter: {}={}",
                                getMountPath(), parameter, value);
                        values[i] = safeValue;
                    }
                }
            }
        }
        return super.decode(requestParameters);
    }
    /**
     * @see org.apache.wicket.request.target.coding.AbstractRequestTargetUrlCodingStrategy#appendParameters(org.apache.wicket.util.string.AppendingStringBuffer,
     *      java.util.Map)
     */
    @Override
    protected void appendParameters(AppendingStringBuffer url, Map<String, ?> parameters)
    {
        if (!url.endsWith("/"))
        {
            url.append("/");
        }
        Set<String> parameterNamesToAdd = new HashSet<String>(parameters.keySet());
        // Find index of last specified parameter
        boolean foundParameter = false;
        int lastSpecifiedParameter = parameterNames.length;
        while (lastSpecifiedParameter != 0 && !foundParameter)
        {
            foundParameter = parameters.containsKey(parameterNames[--lastSpecifiedParameter]);
        }
        if (foundParameter)
        {
            for (int i = 0; i <= lastSpecifiedParameter; i++)
            {
                String parameterName = parameterNames[i];
                final Object param = parameters.get(parameterName);
                String value = param instanceof String[] ? ((String[])param)[0] : ((param == null)
                    ? null : param.toString());
                if (value == null)
                {
                    value = "";
                }
                if (!url.endsWith("/"))
                {
                    url.append("/");
                }
                url.append(urlEncodePathComponent(value));
                parameterNamesToAdd.remove(parameterName);
            }
        }
        if (!parameterNamesToAdd.isEmpty())
        {
            boolean first = true;
            for (String parameterName : parameterNamesToAdd)
            {
                final Object param = parameters.get(parameterName);
                if (param instanceof String[]) {
                    String [] values = (String[]) param;
                    for (String value : values) {
                        url.append(first ? '?' : '&');
                        url.append(urlEncodeQueryComponent(parameterName)).append("=").append(
                                urlEncodeQueryComponent(value));
                        first = false;
                    }
                } else {
                    url.append(first ? '?' : '&');
                    String value = String.valueOf(param);
                    url.append(urlEncodeQueryComponent(parameterName)).append("=").append(
                        urlEncodeQueryComponent(value));
                }
                first = false;
            }
        }
    }
}
src/main/java/com/gitblit/wicket/GitblitWicketApp.java
@@ -18,6 +18,7 @@
import com.gitblit.manager.IUserManager;
import com.gitblit.tickets.ITicketService;
import com.gitblit.transport.ssh.IPublicKeyManager;
import com.gitblit.utils.XssFilter;
public interface GitblitWicketApp {
@@ -31,6 +32,8 @@
    public abstract IStoredSettings settings();
    public abstract XssFilter xssFilter();
    /**
     * Is Gitblit running in debug mode?
     *
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/SafeTextModel.java
New file
@@ -0,0 +1,96 @@
package com.gitblit.wicket;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.lang.Objects;
import org.parboiled.common.StringUtils;
import org.slf4j.LoggerFactory;
public class SafeTextModel implements IModel<String> {
    private static final long serialVersionUID = 1L;
    public enum Mode {
        relaxed, none
    }
    private final Mode mode;
    private String value;
    public static SafeTextModel none() {
        return new SafeTextModel(Mode.none);
    }
    public static SafeTextModel none(String value) {
        return new SafeTextModel(Mode.none);
    }
    public static SafeTextModel relaxed() {
        return new SafeTextModel(Mode.relaxed);
    }
    public static SafeTextModel relaxed(String value) {
        return new SafeTextModel(Mode.relaxed);
    }
    public SafeTextModel(Mode mode) {
        this.mode = mode;
    }
    public SafeTextModel(String value, Mode mode) {
        this.value = value;
        this.mode = mode;
    }
    @Override
    public void detach() {
    }
    @Override
    public String getObject() {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        String safeValue;
        switch (mode) {
        case none:
            safeValue = GitBlitWebApp.get().xssFilter().none(value);
            break;
        default:
            safeValue = GitBlitWebApp.get().xssFilter().relaxed(value);
            break;
        }
        if (!value.equals(safeValue)) {
            LoggerFactory.getLogger(getClass()).warn("XSS filter trigggered on suspicious form field value {}",
                    value);
        }
        return safeValue;
    }
    @Override
    public void setObject(String input) {
        this.value = input;
    }
    @Override
    public int hashCode()
    {
        return Objects.hashCode(value);
    }
    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        if (!(obj instanceof Model<?>))
        {
            return false;
        }
        Model<?> that = (Model<?>)obj;
        return Objects.equal(value, that.getObject());
    }
}
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/EditTicketPage.java
@@ -50,6 +50,8 @@
import com.gitblit.tickets.TicketResponsible;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.SafeTextModel;
import com.gitblit.wicket.SafeTextModel.Mode;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.MarkdownTextArea;
@@ -110,8 +112,8 @@
        }
        typeModel = Model.of(ticket.type);
        titleModel = Model.of(ticket.title);
        topicModel = Model.of(ticket.topic == null ? "" : ticket.topic);
        titleModel = SafeTextModel.none(ticket.title);
        topicModel = SafeTextModel.none(ticket.topic == null ? "" : ticket.topic);
        responsibleModel = Model.of();
        milestoneModel = Model.of();
        mergeToModel = Model.of(ticket.mergeTo == null ? getRepositoryModel().mergeTo : ticket.mergeTo);
@@ -134,7 +136,7 @@
        form.add(new TextField<String>("title", titleModel));
        form.add(new TextField<String>("topic", topicModel));
        final IModel<String> markdownPreviewModel = new Model<String>();
        final SafeTextModel markdownPreviewModel = new SafeTextModel(Mode.none);
        descriptionPreview = new Label("descriptionPreview", markdownPreviewModel);
        descriptionPreview.setEscapeModelStrings(false);
        descriptionPreview.setOutputMarkupId(true);
src/main/java/com/gitblit/wicket/pages/NewTicketPage.java
@@ -46,6 +46,8 @@
import com.gitblit.tickets.TicketResponsible;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.SafeTextModel;
import com.gitblit.wicket.SafeTextModel.Mode;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.MarkdownTextArea;
@@ -87,8 +89,8 @@
        }
        typeModel = Model.of(TicketModel.Type.defaultType);
        titleModel = Model.of();
        topicModel = Model.of();
        titleModel = SafeTextModel.none();
        topicModel = SafeTextModel.none();
        mergeToModel = Model.of(Repository.shortenRefName(getRepositoryModel().mergeTo));
        responsibleModel = Model.of();
        milestoneModel = Model.of();
@@ -103,7 +105,7 @@
        form.add(new TextField<String>("title", titleModel));
        form.add(new TextField<String>("topic", topicModel));
        final IModel<String> markdownPreviewModel = new Model<String>();
        final SafeTextModel markdownPreviewModel = new SafeTextModel(Mode.none);
        descriptionPreview = new Label("descriptionPreview", markdownPreviewModel);
        descriptionPreview.setEscapeModelStrings(false);
        descriptionPreview.setOutputMarkupId(true);
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -550,7 +550,8 @@
        String html;
        switch (model.commitMessageRenderer) {
        case MARKDOWN:
            html = MessageFormat.format("<div class='commit_message'>{0}</div>", content);
            String safeContent = app().xssFilter().relaxed(content);
            html = MessageFormat.format("<div class='commit_message'>{0}</div>", safeContent);
            break;
        default:
            html = MessageFormat.format("<pre class='commit_message'>{0}</pre>", content);
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) {
src/main/java/com/gitblit/wicket/panels/CommentPanel.java
@@ -19,13 +19,14 @@
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
import com.gitblit.models.TicketModel.Change;
import com.gitblit.models.UserModel;
import com.gitblit.wicket.SafeTextModel;
import com.gitblit.wicket.SafeTextModel.Mode;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.BasePage;
@@ -89,7 +90,7 @@
            }
        }.setVisible(ticket != null && ticket.number > 0));
        final IModel<String> markdownPreviewModel = new Model<String>();
        final SafeTextModel markdownPreviewModel = new SafeTextModel(Mode.none);
        markdownPreview = new Label("markdownPreview", markdownPreviewModel);
        markdownPreview.setEscapeModelStrings(false);
        markdownPreview.setOutputMarkupId(true);
src/main/java/com/gitblit/wicket/panels/MarkdownTextArea.java
@@ -20,12 +20,12 @@
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.util.time.Duration;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.wicket.GitBlitWebApp;
import com.gitblit.wicket.SafeTextModel;
public class MarkdownTextArea extends TextArea {
@@ -35,7 +35,7 @@
    protected String text = "";
    public MarkdownTextArea(String id, final IModel<String> previewModel, final Label previewLabel) {
    public MarkdownTextArea(String id, final SafeTextModel previewModel, final Label previewLabel) {
        super(id);
        setModel(new PropertyModel(this, "text"));
        add(new AjaxFormComponentUpdatingBehavior("onblur") {
@@ -65,7 +65,7 @@
        setOutputMarkupId(true);
    }
    protected void renderPreview(IModel<String> previewModel) {
    protected void renderPreview(SafeTextModel previewModel) {
        if (text == null) {
            return;
        }
src/test/java/com/gitblit/tests/AuthenticationManagerTest.java
@@ -26,6 +26,8 @@
import com.gitblit.manager.UserManager;
import com.gitblit.models.UserModel;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * Class for testing local authentication.
@@ -42,7 +44,8 @@
    }
    IAuthenticationManager newAuthenticationManager() {
        RuntimeManager runtime = new RuntimeManager(getSettings(), GitBlitSuite.BASEFOLDER).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(getSettings(), xssFilter, GitBlitSuite.BASEFOLDER).start();
        users = new UserManager(runtime, null).start();
        AuthenticationManager auth = new AuthenticationManager(runtime, users).start();
        return auth;
src/test/java/com/gitblit/tests/BranchTicketServiceTest.java
@@ -29,6 +29,8 @@
import com.gitblit.models.RepositoryModel;
import com.gitblit.tickets.BranchTicketService;
import com.gitblit.tickets.ITicketService;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * Tests the branch ticket service.
@@ -50,8 +52,8 @@
    protected ITicketService getService(boolean deleteAll) throws Exception {
        IStoredSettings settings = getSettings(deleteAll);
        IRuntimeManager runtimeManager = new RuntimeManager(settings).start();
        XssFilter xssFilter = new AllowXssFilter();
        IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter).start();
        IPluginManager pluginManager = new PluginManager(runtimeManager).start();
        INotificationManager notificationManager = new NotificationManager(settings).start();
        IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
src/test/java/com/gitblit/tests/FileTicketServiceTest.java
@@ -29,6 +29,8 @@
import com.gitblit.models.RepositoryModel;
import com.gitblit.tickets.FileTicketService;
import com.gitblit.tickets.ITicketService;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * Tests the file ticket service.
@@ -49,8 +51,8 @@
    protected ITicketService getService(boolean deleteAll) throws Exception {
        IStoredSettings settings = getSettings(deleteAll);
        IRuntimeManager runtimeManager = new RuntimeManager(settings).start();
        XssFilter xssFilter = new AllowXssFilter();
        IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter).start();
        IPluginManager pluginManager = new PluginManager(runtimeManager).start();
        INotificationManager notificationManager = new NotificationManager(settings).start();
        IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java
@@ -32,6 +32,8 @@
import com.gitblit.manager.UserManager;
import com.gitblit.models.UserModel;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * Test the Htpasswd user service.
@@ -74,7 +76,8 @@
    }
    private HtpasswdAuthProvider newHtpasswdAuthentication(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime, null).start();
        HtpasswdAuthProvider htpasswd = new HtpasswdAuthProvider();
        htpasswd.setup(runtime, users);
@@ -82,7 +85,8 @@
    }
    private AuthenticationManager newAuthenticationManager(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime, null).start();
        HtpasswdAuthProvider htpasswd = new HtpasswdAuthProvider();
        htpasswd.setup(runtime, users);
src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
@@ -39,6 +39,8 @@
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
@@ -96,7 +98,8 @@
    }
    private LdapAuthProvider newLdapAuthentication(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start();
        userManager = new UserManager(runtime, null).start();
        LdapAuthProvider ldap = new LdapAuthProvider();
        ldap.setup(runtime, userManager);
@@ -104,7 +107,8 @@
    }
    private AuthenticationManager newAuthenticationManager(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start();
        AuthenticationManager auth = new AuthenticationManager(runtime, userManager);
        auth.addAuthenticationProvider(newLdapAuthentication(settings));
        return auth;
src/test/java/com/gitblit/tests/LuceneExecutorTest.java
@@ -34,6 +34,8 @@
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * Tests Lucene indexing and querying.
@@ -48,7 +50,8 @@
    private LuceneService newLuceneExecutor() {
        MemorySettings settings = new MemorySettings();
        settings.put(Keys.git.repositoriesFolder, GitBlitSuite.REPOSITORIES);
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime, null).start();
        RepositoryManager repos = new RepositoryManager(runtime, null, users);
        return new LuceneService(settings, repos);
src/test/java/com/gitblit/tests/RedisTicketServiceTest.java
@@ -30,6 +30,8 @@
import com.gitblit.models.RepositoryModel;
import com.gitblit.tickets.ITicketService;
import com.gitblit.tickets.RedisTicketService;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
/**
 * Tests the Redis ticket service.
@@ -57,8 +59,8 @@
    protected ITicketService getService(boolean deleteAll) throws Exception {
        IStoredSettings settings = getSettings(deleteAll);
        IRuntimeManager runtimeManager = new RuntimeManager(settings).start();
        XssFilter xssFilter = new AllowXssFilter();
        IRuntimeManager runtimeManager = new RuntimeManager(settings, xssFilter).start();
        IPluginManager pluginManager = new PluginManager(runtimeManager).start();
        INotificationManager notificationManager = new NotificationManager(settings).start();
        IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java
@@ -13,6 +13,8 @@
import com.gitblit.manager.UserManager;
import com.gitblit.models.UserModel;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
public class RedmineAuthenticationTest extends GitblitUnitTest {
@@ -25,7 +27,8 @@
    }
    RedmineAuthProvider newRedmineAuthentication(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(settings, xssFilter, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime, null).start();
        RedmineAuthProvider redmine = new RedmineAuthProvider();
        redmine.setup(runtime, users);
@@ -37,7 +40,8 @@
    }
    AuthenticationManager newAuthenticationManager() {
        RuntimeManager runtime = new RuntimeManager(getSettings(), GitBlitSuite.BASEFOLDER).start();
        XssFilter xssFilter = new AllowXssFilter();
        RuntimeManager runtime = new RuntimeManager(getSettings(), xssFilter, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime, null).start();
        RedmineAuthProvider redmine = new RedmineAuthProvider();
        redmine.setup(runtime, users);
src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
@@ -28,6 +28,8 @@
import com.gitblit.models.ServerSettings;
import com.gitblit.models.ServerStatus;
import com.gitblit.models.SettingModel;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
import com.google.inject.Injector;
public class MockRuntimeManager implements IRuntimeManager {
@@ -134,6 +136,11 @@
    }
    @Override
    public XssFilter getXssFilter() {
        return new AllowXssFilter();
    }
    @Override
    public boolean updateSettings(Map<String, String> updatedSettings) {
        return settings.saveSettings(updatedSettings);
    }