James Moger
2014-05-05 8f0f665b9ee4e2cd21e9e0d5d7cfc69b1d19b86f
Merged #23 "Enhance the plugin infrastructure to allow deeper plugin integration"
1 files deleted
14 files added
24 files modified
2093 ■■■■ changed files
src/main/java/WEB-INF/web.xml 9 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/dagger/DaggerFilter.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java 49 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/HttpRequestFilter.java 49 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/NavLinkExtension.java 40 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java 42 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/UserMenuExtension.java 40 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/Menu.java 302 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/NavLink.java 140 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/AuthenticationFilter.java 3 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/EnforceAuthenticationFilter.java 3 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/FilterRuntimeConfig.java 71 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/GitFilter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/ProxyFilter.java 86 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/RpcFilter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/SyndicationFilter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.java 110 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitblitWicketApp.java 72 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/PageRegistration.java 243 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/PluginClassResolver.java 122 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ActivityPage.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DashboardPage.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectPage.java 30 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectsPage.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java 61 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RootPage.html 14 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RootPage.java 188 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TeamsPage.html 13 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TeamsPage.java 30 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UserPage.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UsersPage.html 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UsersPage.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/DropDownMenu.java 74 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/NavigationPanel.java 60 ●●●●● patch | view | raw | blame | history
src/site/plugins_extensions.mkd 100 ●●●●● patch | view | raw | blame | history
src/main/java/WEB-INF/web.xml
@@ -214,6 +214,15 @@
        <url-pattern>/robots.txt</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>ProxyFilter</filter-name>
        <filter-class>com.gitblit.servlet.ProxyFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ProxyFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- Git Access Restriction Filter
         <url-pattern> MUST match: 
            * GitServlet
src/main/java/com/gitblit/dagger/DaggerFilter.java
@@ -36,10 +36,10 @@
    public final void init(FilterConfig filterConfig) throws ServletException {
        ServletContext context = filterConfig.getServletContext();
        ObjectGraph objectGraph = (ObjectGraph) context.getAttribute(DaggerContext.INJECTOR_NAME);
        inject(objectGraph);
        inject(objectGraph, filterConfig);
    }
    protected abstract void inject(ObjectGraph dagger);
    protected abstract void inject(ObjectGraph dagger, FilterConfig filterConfig) throws ServletException;
    @Override
    public void destroy() {
src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java
New file
@@ -0,0 +1,49 @@
/*
 * 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.extensions;
import org.apache.wicket.Application;
import org.apache.wicket.IInitializer;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.wicket.GitblitWicketApp;
/**
 * A Gitblit plugin that is allowed to extend the Wicket webapp.
 *
 * @author James Moger
 * @since 1.6.0
 */
public abstract class GitblitWicketPlugin extends GitblitPlugin implements IInitializer  {
    public GitblitWicketPlugin(PluginWrapper wrapper) {
        super(wrapper);
    }
    @Override
    public final void init(Application application) {
        init((GitblitWicketApp) application);
    }
    /**
     * Allows plugins to extend the web application.
     *
     * @param app
     * @since 1.6.0
     */
    protected abstract void init(GitblitWicketApp app);
}
src/main/java/com/gitblit/extensions/HttpRequestFilter.java
New file
@@ -0,0 +1,49 @@
/*
 * 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.extensions;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import ro.fortsoft.pf4j.ExtensionPoint;
/**
 * Extension point to intercept HTTP requests passing through the server.
 *
 * @author David Ostrovsky
 * @since 1.6.0
 *
 */
public abstract class HttpRequestFilter implements Filter, ExtensionPoint {
    @Override
    public void init(FilterConfig config) throws ServletException {
    }
    @Override
    public void destroy() {
    }
    @Override
    public abstract void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;
}
src/main/java/com/gitblit/extensions/NavLinkExtension.java
New file
@@ -0,0 +1,40 @@
/*
 * 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.extensions;
import java.util.List;
import ro.fortsoft.pf4j.ExtensionPoint;
import com.gitblit.models.NavLink;
import com.gitblit.models.UserModel;
/**
 * Extension point to contribute top-level navigation links.
 *
 * @author James Moger
 * @since 1.6.0
 *
 */
public abstract class NavLinkExtension implements ExtensionPoint {
    /**
     * @param user
     * @since 1.6.0
     * @return a list of nav links
     */
    public abstract List<NavLink> getNavLinks(UserModel user);
}
src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
New file
@@ -0,0 +1,42 @@
/*
 * 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.extensions;
import java.util.List;
import ro.fortsoft.pf4j.ExtensionPoint;
import com.gitblit.models.NavLink;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
/**
 * Extension point to contribute repository page navigation links.
 *
 * @author James Moger
 * @since 1.6.0
 *
 */
public abstract class RepositoryNavLinkExtension implements ExtensionPoint {
    /**
     * @param user
     * @param repository
     * @since 1.6.0
     * @return a list of nav links
     */
    public abstract List<NavLink> getNavLinks(UserModel user, RepositoryModel repository);
}
src/main/java/com/gitblit/extensions/UserMenuExtension.java
New file
@@ -0,0 +1,40 @@
/*
 * 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.extensions;
import java.util.List;
import ro.fortsoft.pf4j.ExtensionPoint;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.UserModel;
/**
 * Extension point to contribute user menu items.
 *
 * @author James Moger
 * @since 1.6.0
 *
 */
public abstract class UserMenuExtension implements ExtensionPoint {
    /**
     * @param user
     * @since 1.6.0
     * @return a list of menu items
     */
    public abstract List<MenuItem> getMenuItems(UserModel user);
}
src/main/java/com/gitblit/models/Menu.java
New file
@@ -0,0 +1,302 @@
package com.gitblit.models;
import java.io.Serializable;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import com.gitblit.utils.StringUtils;
public class Menu {
    /**
     * A MenuItem for a drop down menu.
     *
     * @author James Moger
     * @since 1.6.0
     */
    public abstract static class MenuItem implements Serializable {
        private static final long serialVersionUID = 1L;
        final String displayText;
        MenuItem(String displayText) {
            this.displayText = displayText;
        }
        @Override
        public int hashCode() {
            return displayText.hashCode();
        }
        @Override
        public boolean equals(Object o) {
            if (o instanceof MenuItem) {
                return hashCode() == o.hashCode();
            }
            return false;
        }
        @Override
        public String toString() {
            return displayText;
        }
    }
    /**
     * A divider for the menu.
     *
     * @since 1.6.0
     */
    public static class MenuDivider extends MenuItem {
        private static final long serialVersionUID = 1L;
        public MenuDivider() {
            super("");
        }
    }
    /**
     * A MenuItem for setting a parameter of the current url.
     *
     * @author James Moger
     *
     */
    public static class ParameterMenuItem extends MenuItem {
        private static final long serialVersionUID = 1L;
        final PageParameters parameters;
        final String parameter;
        final String value;
        final boolean isSelected;
        /**
         * @param displayText
         */
        public ParameterMenuItem(String displayText) {
            this(displayText, null, null, null);
        }
        /**
         * @param displayText
         * @param parameter
         * @param value
         */
        public ParameterMenuItem(String displayText, String parameter, String value) {
            this(displayText, parameter, value, null);
        }
        /**
         * @param displayText
         * @param parameter
         * @param value
         */
        public ParameterMenuItem(String displayText, String parameter, String value,
                PageParameters params) {
            super(displayText);
            this.parameter = parameter;
            this.value = value;
            if (params == null) {
                // no parameters specified
                parameters = new PageParameters();
                setParameter(parameter, value);
                isSelected = false;
            } else {
                parameters = new PageParameters(params);
                if (parameters.containsKey(parameter)) {
                    isSelected = params.getString(parameter).equals(value);
                    // set the new selection value
                    setParameter(parameter, value);
                } else {
                    // not currently selected
                    isSelected = false;
                    setParameter(parameter, value);
                }
            }
        }
        protected void setParameter(String parameter, String value) {
            if (!StringUtils.isEmpty(parameter)) {
                if (StringUtils.isEmpty(value)) {
                    this.parameters.remove(parameter);
                } else {
                    this.parameters.put(parameter, value);
                }
            }
        }
        public String formatParameter() {
            if (StringUtils.isEmpty(parameter) || StringUtils.isEmpty(value)) {
                return "";
            }
            return parameter + "=" + value;
        }
        public PageParameters getPageParameters() {
            return parameters;
        }
        public boolean isSelected() {
            return isSelected;
        }
        @Override
        public int hashCode() {
            if (StringUtils.isEmpty(displayText)) {
                return value.hashCode() + parameter.hashCode();
            }
            return displayText.hashCode();
        }
        @Override
        public boolean equals(Object o) {
            if (o instanceof MenuItem) {
                return hashCode() == o.hashCode();
            }
            return false;
        }
        @Override
        public String toString() {
            if (StringUtils.isEmpty(displayText)) {
                return formatParameter();
            }
            return displayText;
        }
    }
    /**
     * Menu item for toggling a parameter.
     *
     */
    public static class ToggleMenuItem extends ParameterMenuItem {
        private static final long serialVersionUID = 1L;
        /**
         * @param displayText
         * @param parameter
         * @param value
         */
        public ToggleMenuItem(String displayText, String parameter, String value,
                PageParameters params) {
            super(displayText, parameter, value, params);
            if (isSelected) {
                // already selected, so remove this enables toggling
                parameters.remove(parameter);
            }
        }
    }
    /**
     * Menu item for linking to another Wicket page.
     *
     * @since 1.6.0
     */
    public static class PageLinkMenuItem extends MenuItem {
        private static final long serialVersionUID = 1L;
        private final Class<? extends WebPage> pageClass;
        private final PageParameters params;
        /**
         * Page Link Item links to another page.
         *
         * @param displayText
         * @param pageClass
         * @since 1.6.0
         */
        public PageLinkMenuItem(String displayText, Class<? extends WebPage> pageClass) {
            this(displayText, pageClass, null);
        }
        /**
         * Page Link Item links to another page.
         *
         * @param displayText
         * @param pageClass
         * @param params
         * @since 1.6.0
         */
        public PageLinkMenuItem(String displayText, Class<? extends WebPage> pageClass, PageParameters params) {
            super(displayText);
            this.pageClass = pageClass;
            this.params = params;
        }
        /**
         * @return the page class
         * @since 1.6.0
         */
        public Class<? extends WebPage> getPageClass() {
            return pageClass;
        }
        /**
         * @return the page parameters
         * @since 1.6.0
         */
        public PageParameters getPageParameters() {
            return params;
        }
    }
    /**
     * Menu item to link to an external page.
     *
     * @since 1.6.0
     */
    public static class ExternalLinkMenuItem extends MenuItem {
        private static final long serialVersionUID = 1L;
        private final String href;
        private final boolean newWindow;
        /**
         * External Link Item links to something else.
         *
         * @param displayText
         * @param href
         * @since 1.6.0
         */
        public ExternalLinkMenuItem(String displayText, String href) {
            this(displayText, href, false);
        }
        /**
         * External Link Item links to something else.
         *
         * @param displayText
         * @param href
         * @since 1.6.0
         */
        public ExternalLinkMenuItem(String displayText, String href, boolean newWindow) {
            super(displayText);
            this.href = href;
            this.newWindow = newWindow;
        }
        /**
         * @since 1.6.0
         */
        public String getHref() {
            return href;
        }
        /**
         * @since 1.6.0
         */
        public boolean openInNewWindow() {
            return newWindow;
        }
    }
}
src/main/java/com/gitblit/models/NavLink.java
New file
@@ -0,0 +1,140 @@
/*
 * 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.models;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import com.gitblit.models.Menu.MenuItem;
/**
 * Represents a navigation link for the navigation panel.
 *
 * @author James Moger
 *
 */
public abstract class NavLink implements Serializable {
    private static final long serialVersionUID = 1L;
    public final String translationKey;
    public final boolean hiddenPhone;
    public NavLink(String translationKey, boolean hiddenPhone) {
        this.translationKey = translationKey;
        this.hiddenPhone = hiddenPhone;
    }
    /**
     * Represents a Wicket page link.
     *
     * @author James Moger
     *
     */
    public static class PageNavLink extends NavLink implements Serializable {
        private static final long serialVersionUID = 1L;
        public final Class<? extends WebPage> pageClass;
        public final PageParameters params;
        public PageNavLink(String translationKey, Class<? extends WebPage> pageClass) {
            this(translationKey, pageClass, null);
        }
        public PageNavLink(String translationKey, Class<? extends WebPage> pageClass,
                PageParameters params) {
            this(translationKey, pageClass, params, false);
        }
        public PageNavLink(String translationKey, Class<? extends WebPage> pageClass,
                PageParameters params, boolean hiddenPhone) {
            super(translationKey, hiddenPhone);
            this.pageClass = pageClass;
            this.params = params;
        }
    }
    /**
     * Represents an explicitly href link.
     *
     * @author James Moger
     *
     */
    public static class ExternalNavLink extends NavLink implements Serializable {
        private static final long serialVersionUID = 1L;
        public final String url;
        public ExternalNavLink(String keyOrText, String url) {
            super(keyOrText, false);
            this.url = url;
        }
        public ExternalNavLink(String keyOrText, String url, boolean hiddenPhone) {
            super(keyOrText,  hiddenPhone);
            this.url = url;
        }
    }
    /**
     * Represents a DropDownMenu for the current page.
     *
     * @author James Moger
     *
     */
    public static class DropDownPageMenuNavLink extends PageNavLink implements Serializable {
        private static final long serialVersionUID = 1L;
        public final List<MenuItem> menuItems;
        public DropDownPageMenuNavLink(String keyOrText, Class<? extends WebPage> pageClass) {
            this(keyOrText, pageClass, false);
        }
        public DropDownPageMenuNavLink(String keyOrText, Class<? extends WebPage> pageClass, boolean hiddenPhone) {
            super(keyOrText, pageClass, null, hiddenPhone);
            menuItems = new ArrayList<MenuItem>();
        }
    }
    /**
     * Represents a DropDownMenu.
     *
     * @author James Moger
     *
     */
    public static class DropDownMenuNavLink extends NavLink implements Serializable {
        private static final long serialVersionUID = 1L;
        public final List<MenuItem> menuItems;
        public DropDownMenuNavLink(String keyOrText) {
            this(keyOrText, false);
        }
        public DropDownMenuNavLink(String keyOrText, boolean hiddenPhone) {
            super(keyOrText, hiddenPhone);
            menuItems = new ArrayList<MenuItem>();
        }
    }
}
src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -54,8 +55,8 @@
    protected IRepositoryManager repositoryManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        super.inject(dagger);
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        super.inject(dagger, filterConfig);
        this.runtimeManager = dagger.get(IRuntimeManager.class);
        this.repositoryManager = dagger.get(IRepositoryManager.class);
    }
src/main/java/com/gitblit/servlet/AuthenticationFilter.java
@@ -24,6 +24,7 @@
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -64,7 +65,7 @@
    protected IAuthenticationManager authenticationManager;
    @Override
    protected void inject(ObjectGraph dagger) {
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        this.authenticationManager = dagger.get(IAuthenticationManager.class);
    }
src/main/java/com/gitblit/servlet/EnforceAuthenticationFilter.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -53,7 +54,7 @@
    private IAuthenticationManager authenticationManager;
    @Override
    protected void inject(ObjectGraph dagger) {
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        this.settings = dagger.get(IStoredSettings.class);
        this.authenticationManager = dagger.get(IAuthenticationManager.class);
    }
src/main/java/com/gitblit/servlet/FilterRuntimeConfig.java
New file
@@ -0,0 +1,71 @@
/*
 * 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.servlet;
import java.util.Enumeration;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import com.gitblit.IStoredSettings;
import com.gitblit.manager.IRuntimeManager;
/**
 * Wraps a filter config and will prefer a setting retrieved from IStoredSettings
 * if one is available.
 *
 * @author James Moger
 * @since 1.6.0
 */
public class FilterRuntimeConfig implements FilterConfig {
    final IRuntimeManager runtime;
    final IStoredSettings settings;
    final String namespace;
    final FilterConfig config;
    public FilterRuntimeConfig(IRuntimeManager runtime, String namespace, FilterConfig config) {
        this.runtime = runtime;
        this.settings = runtime.getSettings();
        this.namespace = namespace;
        this.config = config;
    }
    @Override
    public String getFilterName() {
        return config.getFilterName();
    }
    @Override
    public ServletContext getServletContext() {
        return config.getServletContext();
    }
    @Override
    public String getInitParameter(String name) {
        String key = namespace + "." + name;
        if (settings.hasSettings(key)) {
            String value = settings.getString(key, null);
            return value;
        }
        return config.getInitParameter(name);
    }
    @Override
    public Enumeration<String> getInitParameterNames() {
        return config.getInitParameterNames();
    }
}
src/main/java/com/gitblit/servlet/GitFilter.java
@@ -17,6 +17,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import com.gitblit.Constants.AccessRestrictionType;
@@ -53,8 +54,8 @@
    private IFederationManager federationManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        super.inject(dagger);
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        super.inject(dagger, filterConfig);
        this.settings = dagger.get(IStoredSettings.class);
        this.federationManager = dagger.get(IFederationManager.class);
    }
src/main/java/com/gitblit/servlet/ProxyFilter.java
New file
@@ -0,0 +1,86 @@
/*
 * 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.servlet;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.dagger.DaggerFilter;
import com.gitblit.extensions.HttpRequestFilter;
import com.gitblit.manager.IPluginManager;
import com.gitblit.manager.IRuntimeManager;
import dagger.ObjectGraph;
/**
 * A request filter than allows registered extension request filters to access
 * request data.  The intended purpose is for server monitoring plugins.
 *
 * @author David Ostrovsky
 * @since 1.6.0
 */
public class ProxyFilter extends DaggerFilter {
    private List<HttpRequestFilter> filters;
    @Override
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) throws ServletException {
        IRuntimeManager runtimeManager = dagger.get(IRuntimeManager.class);
        IPluginManager pluginManager = dagger.get(IPluginManager.class);
        filters = pluginManager.getExtensions(HttpRequestFilter.class);
        for (HttpRequestFilter f : filters) {
            // wrap the filter config for Gitblit settings retrieval
            PluginWrapper pluginWrapper = pluginManager.whichPlugin(f.getClass());
            FilterConfig runtimeConfig = new FilterRuntimeConfig(runtimeManager,
                    pluginWrapper.getPluginId(), filterConfig);
            f.init(runtimeConfig);
        }
    }
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, final FilterChain last)
            throws IOException, ServletException {
        final Iterator<HttpRequestFilter> itr = filters.iterator();
        new FilterChain() {
            @Override
            public void doFilter(ServletRequest req, ServletResponse res) throws IOException,
                    ServletException {
                if (itr.hasNext()) {
                    itr.next().doFilter(req, res, this);
                } else {
                    last.doFilter(req, res);
                }
            }
        }.doFilter(req, res);
    }
    @Override
    public void destroy() {
        for (HttpRequestFilter f : filters) {
            f.destroy();
        }
    }
}
src/main/java/com/gitblit/servlet/RpcFilter.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -53,8 +54,8 @@
    private IRuntimeManager runtimeManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        super.inject(dagger);
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        super.inject(dagger, filterConfig);
        this.settings = dagger.get(IStoredSettings.class);
        this.runtimeManager = dagger.get(IRuntimeManager.class);
    }
src/main/java/com/gitblit/servlet/SyndicationFilter.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -50,8 +51,8 @@
    private IProjectManager projectManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        super.inject(dagger);
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        super.inject(dagger, filterConfig);
        this.runtimeManager = dagger.get(IRuntimeManager.class);
        this.repositoryManager = dagger.get(IRepositoryManager.class);
        this.projectManager = dagger.get(IProjectManager.class);
src/main/java/com/gitblit/wicket/GitBlitWebApp.java
@@ -28,8 +28,12 @@
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.protocol.http.WebApplication;
import ro.fortsoft.pf4j.PluginState;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.extensions.GitblitWicketPlugin;
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IGitblit;
@@ -77,12 +81,13 @@
import com.gitblit.wicket.pages.SummaryPage;
import com.gitblit.wicket.pages.TagPage;
import com.gitblit.wicket.pages.TagsPage;
import com.gitblit.wicket.pages.TeamsPage;
import com.gitblit.wicket.pages.TicketsPage;
import com.gitblit.wicket.pages.TreePage;
import com.gitblit.wicket.pages.UserPage;
import com.gitblit.wicket.pages.UsersPage;
public class GitBlitWebApp extends WebApplication {
public class GitBlitWebApp extends WebApplication implements GitblitWicketApp {
    private final Class<? extends WebPage> homePageClass = MyDashboardPage.class;
@@ -181,6 +186,7 @@
        mount("/metrics", MetricsPage.class, "r");
        mount("/blame", BlamePage.class, "r", "h", "f");
        mount("/users", UsersPage.class);
        mount("/teams", TeamsPage.class);
        mount("/logout", LogoutPage.class);
        // setup ticket urls
@@ -208,11 +214,29 @@
        mount("/forks", ForksPage.class, "r");
        mount("/fork", ForkPage.class, "r");
        getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
        super.init();
        // allow started Wicket plugins to initialize
        for (PluginWrapper pluginWrapper : pluginManager.getPlugins()) {
            if (PluginState.STARTED != pluginWrapper.getPluginState()) {
                continue;
            }
            if (pluginWrapper.getPlugin() instanceof GitblitWicketPlugin) {
                GitblitWicketPlugin wicketPlugin = (GitblitWicketPlugin) pluginWrapper.getPlugin();
                wicketPlugin.init(this);
            }
    }
    private void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
         // customize the Wicket class resolver to load from plugins
        PluginClassResolver classResolver = new PluginClassResolver(pluginManager);
        getApplicationSettings().setClassResolver(classResolver);
        getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#mount(java.lang.String, java.lang.Class, java.lang.String)
     */
    @Override
    public void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
        if (parameters == null) {
            parameters = new String[] {};
        }
@@ -228,15 +252,26 @@
        }
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getHomePage()
     */
    @Override
    public Class<? extends WebPage> getHomePage() {
        return homePageClass;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#isCacheablePage(java.lang.String)
     */
    @Override
    public boolean isCacheablePage(String mountPoint) {
        return cacheablePages.containsKey(mountPoint);
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getCacheControl(java.lang.String)
     */
    @Override
    public CacheControl getCacheControl(String mountPoint) {
        return cacheablePages.get(mountPoint);
    }
@@ -252,15 +287,18 @@
        return gitBlitWebSession;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#settings()
     */
    @Override
    public IStoredSettings settings() {
        return settings;
    }
    /**
     * Is Gitblit running in debug mode?
     *
     * @return true if Gitblit is running in debug mode
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#isDebugMode()
     */
    @Override
    public boolean isDebugMode() {
        return runtimeManager.isDebugMode();
    }
@@ -269,58 +307,114 @@
     * These methods look strange... and they are... but they are the first
     * step towards modularization across multiple commits.
     */
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getBootDate()
     */
    @Override
    public Date getBootDate() {
        return runtimeManager.getBootDate();
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getLastActivityDate()
     */
    @Override
    public Date getLastActivityDate() {
        return repositoryManager.getLastActivityDate();
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#runtime()
     */
    @Override
    public IRuntimeManager runtime() {
        return runtimeManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#plugins()
     */
    @Override
    public IPluginManager plugins() {
        return pluginManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#notifier()
     */
    @Override
    public INotificationManager notifier() {
        return notificationManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#users()
     */
    @Override
    public IUserManager users() {
        return userManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#authentication()
     */
    @Override
    public IAuthenticationManager authentication() {
        return authenticationManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#keys()
     */
    @Override
    public IPublicKeyManager keys() {
        return publicKeyManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#repositories()
     */
    @Override
    public IRepositoryManager repositories() {
        return repositoryManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#projects()
     */
    @Override
    public IProjectManager projects() {
        return projectManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#federation()
     */
    @Override
    public IFederationManager federation() {
        return federationManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#gitblit()
     */
    @Override
    public IGitblit gitblit() {
        return gitblit;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#tickets()
     */
    @Override
    public ITicketService tickets() {
        return gitblit.getTicketService();
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getTimezone()
     */
    @Override
    public TimeZone getTimezone() {
        return runtimeManager.getTimezone();
    }
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -680,3 +680,7 @@
gb.overdue = overdue
gb.openMilestones = open milestones
gb.closedMilestones = closed milestones
gb.administration = administration
gb.plugins = plugins
gb.extensions = extensions
src/main/java/com/gitblit/wicket/GitblitWicketApp.java
New file
@@ -0,0 +1,72 @@
package com.gitblit.wicket;
import java.util.Date;
import java.util.TimeZone;
import org.apache.wicket.markup.html.WebPage;
import com.gitblit.IStoredSettings;
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.INotificationManager;
import com.gitblit.manager.IPluginManager;
import com.gitblit.manager.IProjectManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.manager.IUserManager;
import com.gitblit.tickets.ITicketService;
import com.gitblit.transport.ssh.IPublicKeyManager;
public interface GitblitWicketApp {
    public abstract void mount(String location, Class<? extends WebPage> clazz, String... parameters);
    public abstract Class<? extends WebPage> getHomePage();
    public abstract boolean isCacheablePage(String mountPoint);
    public abstract CacheControl getCacheControl(String mountPoint);
    public abstract IStoredSettings settings();
    /**
     * Is Gitblit running in debug mode?
     *
     * @return true if Gitblit is running in debug mode
     */
    public abstract boolean isDebugMode();
    /*
     * These methods look strange... and they are... but they are the first
     * step towards modularization across multiple commits.
     */
    public abstract Date getBootDate();
    public abstract Date getLastActivityDate();
    public abstract IRuntimeManager runtime();
    public abstract IPluginManager plugins();
    public abstract INotificationManager notifier();
    public abstract IUserManager users();
    public abstract IAuthenticationManager authentication();
    public abstract IPublicKeyManager keys();
    public abstract IRepositoryManager repositories();
    public abstract IProjectManager projects();
    public abstract IFederationManager federation();
    public abstract IGitblit gitblit();
    public abstract ITicketService tickets();
    public abstract TimeZone getTimezone();
}
src/main/java/com/gitblit/wicket/PageRegistration.java
File was deleted
src/main/java/com/gitblit/wicket/PluginClassResolver.java
New file
@@ -0,0 +1,122 @@
/*
 * 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.wicket;
import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
import org.apache.wicket.Application;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.application.IClassResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.fortsoft.pf4j.PluginState;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.manager.IPluginManager;
/**
 * Resolves plugin classes and resources.
 */
public class PluginClassResolver implements IClassResolver {
    private static final Logger logger = LoggerFactory.getLogger(PluginClassResolver.class);
    private final IPluginManager pluginManager;
    public PluginClassResolver(IPluginManager pluginManager) {
        this.pluginManager = pluginManager;
    }
    @Override
    public Class<?> resolveClass(final String className) throws ClassNotFoundException {
        boolean debugEnabled = logger.isDebugEnabled();
        for (PluginWrapper plugin : pluginManager.getPlugins()) {
            if (PluginState.STARTED != plugin.getPluginState()) {
                // ignore this plugin
                continue;
            }
            try {
                return plugin.getPluginClassLoader().loadClass(className);
            } catch (ClassNotFoundException cnfx) {
                if (debugEnabled) {
                    logger.debug("ClassResolver '{}' cannot find class: '{}'", plugin.getPluginId(), className);
                }
            }
        }
        throw new ClassNotFoundException(className);
    }
    @Override
    public Iterator<URL> getResources(final String name) {
        Set<URL> urls = new TreeSet<URL>(new UrlExternalFormComparator());
        for (PluginWrapper plugin : pluginManager.getPlugins()) {
            if (PluginState.STARTED != plugin.getPluginState()) {
                // ignore this plugin
                continue;
            }
            Iterator<URL> it = getResources(name, plugin);
            while (it.hasNext()) {
                URL url = it.next();
                urls.add(url);
            }
        }
        return urls.iterator();
    }
    protected Iterator<URL> getResources(String name, PluginWrapper plugin) {
        HashSet<URL> loadedFiles = new HashSet<URL>();
        try {
            // Try the classloader for the wicket jar/bundle
            Enumeration<URL> resources = plugin.getPluginClassLoader().getResources(name);
            loadResources(resources, loadedFiles);
            // Try the classloader for the user's application jar/bundle
            resources = Application.get().getClass().getClassLoader().getResources(name);
            loadResources(resources, loadedFiles);
            // Try the context class loader
            resources = Thread.currentThread().getContextClassLoader().getResources(name);
            loadResources(resources, loadedFiles);
        } catch (IOException e) {
            throw new WicketRuntimeException(e);
        }
        return loadedFiles.iterator();
    }
    private void loadResources(Enumeration<URL> resources, Set<URL> loadedFiles) {
        if (resources != null) {
            while (resources.hasMoreElements()) {
                final URL url = resources.nextElement();
                if (!loadedFiles.contains(url)) {
                    loadedFiles.add(url);
                }
            }
        }
    }
}
src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java
New file
@@ -0,0 +1,39 @@
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.net.URL;
import java.util.Comparator;
/**
 * A comparator of URL instances.
 *
 * Comparing URLs with their implementation of #equals() is
 * bad because it may cause problems like DNS resolving, or other
 * slow checks. This comparator uses the external form of an URL
 * to make a simple comparison of two Strings.
 *
 * @since 1.5.6
 */
public class UrlExternalFormComparator implements Comparator<URL>
{
    @Override
    public int compare(URL url1, URL url2)
    {
        return url1.toExternalForm().compareTo(url2.toExternalForm());
    }
}
src/main/java/com/gitblit/wicket/pages/ActivityPage.java
@@ -31,15 +31,15 @@
import com.gitblit.Keys;
import com.gitblit.models.Activity;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.Metric;
import com.gitblit.models.NavLink;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.ActivityUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.CacheControl.LastModified;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.charting.Chart;
import com.gitblit.wicket.charting.Charts;
@@ -135,8 +135,8 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
        DropDownMenuRegistration filters = new DropDownMenuRegistration("gb.filters",
    protected void addDropDownMenus(List<NavLink> navLinks) {
        DropDownPageMenuNavLink filters = new DropDownPageMenuNavLink("gb.filters",
                ActivityPage.class);
        PageParameters currentParameters = getPageParameters();
@@ -153,9 +153,9 @@
        if (filters.menuItems.size() > 0) {
            // Reset Filter
            filters.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            filters.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(filters);
        navLinks.add(filters);
    }
    /**
src/main/java/com/gitblit/wicket/pages/DashboardPage.java
@@ -36,7 +36,10 @@
import com.gitblit.Keys;
import com.gitblit.models.DailyLogEntry;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.Metric;
import com.gitblit.models.NavLink;
import com.gitblit.models.RefLogEntry;
import com.gitblit.models.RepositoryCommit;
import com.gitblit.models.RepositoryModel;
@@ -45,9 +48,6 @@
import com.gitblit.utils.RefLogUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebApp;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.charting.Chart;
import com.gitblit.wicket.charting.Charts;
import com.gitblit.wicket.charting.Flotr2Charts;
@@ -141,10 +141,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                GitBlitWebApp.get().getHomePage());
        // preserve repository filter option on time choices
@@ -152,10 +152,10 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(menu);
        navLinks.add(menu);
    }
src/main/java/com/gitblit/wicket/pages/ProjectPage.java
@@ -26,6 +26,11 @@
import org.apache.wicket.markup.html.link.ExternalLink;
import com.gitblit.Keys;
import com.gitblit.models.Menu.MenuDivider;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.NavLink;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
@@ -37,9 +42,6 @@
import com.gitblit.wicket.GitBlitWebApp;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.GitblitRedirectException;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.FilterableRepositoryList;
@@ -159,10 +161,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                ProjectPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -172,15 +174,15 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), "p", WicketUtils.getProjectName(params)));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset"), "p", WicketUtils.getProjectName(params)));
        }
        pages.add(menu);
        navLinks.add(menu);
        DropDownMenuRegistration projects = new DropDownMenuRegistration("gb.projects",
        DropDownPageMenuNavLink projects = new DropDownPageMenuNavLink("gb.projects",
                ProjectPage.class);
        projects.menuItems.addAll(getProjectsMenu());
        pages.add(projects);
        navLinks.add(projects);
    }
    @Override
@@ -202,8 +204,8 @@
        return null;
    }
    protected List<DropDownMenuItem> getProjectsMenu() {
        List<DropDownMenuItem> menu = new ArrayList<DropDownMenuItem>();
    protected List<MenuItem> getProjectsMenu() {
        List<MenuItem> menu = new ArrayList<MenuItem>();
        List<ProjectModel> projects = new ArrayList<ProjectModel>();
        for (ProjectModel model : getProjectModels()) {
            if (!model.isUserProject()) {
@@ -230,11 +232,11 @@
        }
        for (ProjectModel project : projects) {
            menu.add(new DropDownMenuItem(project.getDisplayName(), "p", project.name));
            menu.add(new ParameterMenuItem(project.getDisplayName(), "p", project.name));
        }
        if (showAllProjects) {
            menu.add(new DropDownMenuItem());
            menu.add(new DropDownMenuItem("all projects", null, null));
            menu.add(new MenuDivider());
            menu.add(new ParameterMenuItem("all projects"));
        }
        return menu;
    }
src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
@@ -24,11 +24,11 @@
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.Keys;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.NavLink;
import com.gitblit.models.ProjectModel;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.LinkPanel;
@@ -115,10 +115,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                ProjectsPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -128,9 +128,9 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(menu);
        navLinks.add(menu);
    }
}
src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
@@ -29,15 +29,15 @@
import org.eclipse.jgit.lib.Constants;
import com.gitblit.Keys;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.NavLink;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.CacheControl.LastModified;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.RepositoriesPanel;
@@ -92,10 +92,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                RepositoriesPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -105,10 +105,10 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(menu);
        navLinks.add(menu);
    }
    private String readMarkdown(String messageSource, String resource) {
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -21,7 +21,6 @@
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -49,6 +48,10 @@
import com.gitblit.Constants;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.extensions.RepositoryNavLinkExtension;
import com.gitblit.models.NavLink;
import com.gitblit.models.NavLink.ExternalNavLink;
import com.gitblit.models.NavLink.PageNavLink;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RefModel;
import com.gitblit.models.RepositoryModel;
@@ -66,8 +69,6 @@
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.OtherPageLink;
import com.gitblit.wicket.SessionlessForm;
import com.gitblit.wicket.TicketsUI;
import com.gitblit.wicket.WicketUtils;
@@ -91,7 +92,6 @@
    private Map<String, SubmoduleModel> submodules;
    private final Map<String, PageRegistration> registeredPages;
    private boolean showAdmin;
    private boolean isOwner;
@@ -150,12 +150,11 @@
            }
        }
        // register the available page links for this page and user
        registeredPages = registerPages();
        // register the available navigation links for this page and user
        List<NavLink> navLinks = registerNavLinks();
        // standard page links
        List<PageRegistration> pages = new ArrayList<PageRegistration>(registeredPages.values());
        NavigationPanel navigationPanel = new NavigationPanel("repositoryNavPanel", getRepoNavPageClass(), pages);
        // standard navigation links
        NavigationPanel navigationPanel = new NavigationPanel("repositoryNavPanel", getRepoNavPageClass(), navLinks);
        add(navigationPanel);
        add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
@@ -183,45 +182,56 @@
        return new BugtraqProcessor(app().settings());
    }
    private Map<String, PageRegistration> registerPages() {
    private List<NavLink> registerNavLinks() {
        PageParameters params = null;
        if (!StringUtils.isEmpty(repositoryName)) {
            params = WicketUtils.newRepositoryParameter(repositoryName);
        }
        Map<String, PageRegistration> pages = new LinkedHashMap<String, PageRegistration>();
        List<NavLink> navLinks = new ArrayList<NavLink>();
        Repository r = getRepository();
        RepositoryModel model = getRepositoryModel();
        // standard links
        if (RefLogUtils.getRefLogBranch(r) == null) {
            pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
            navLinks.add(new PageNavLink("gb.summary", SummaryPage.class, params));
        } else {
            pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
            navLinks.add(new PageNavLink("gb.summary", SummaryPage.class, params));
//            pages.put("overview", new PageRegistration("gb.overview", OverviewPage.class, params));
            pages.put("reflog", new PageRegistration("gb.reflog", ReflogPage.class, params));
            navLinks.add(new PageNavLink("gb.reflog", ReflogPage.class, params));
        }
        pages.put("commits", new PageRegistration("gb.commits", LogPage.class, params));
        pages.put("tree", new PageRegistration("gb.tree", TreePage.class, params));
        navLinks.add(new PageNavLink("gb.commits", LogPage.class, params));
        navLinks.add(new PageNavLink("gb.tree", TreePage.class, params));
        if (app().tickets().isReady() && (app().tickets().isAcceptingNewTickets(getRepositoryModel()) || app().tickets().hasTickets(getRepositoryModel()))) {
            PageParameters tParams = new PageParameters(params);
            for (String state : TicketsUI.openStatii) {
                tParams.add(Lucene.status.name(), state);
            }
            pages.put("tickets", new PageRegistration("gb.tickets", TicketsPage.class, tParams));
            navLinks.add(new PageNavLink("gb.tickets", TicketsPage.class, tParams));
        }
        pages.put("docs", new PageRegistration("gb.docs", DocsPage.class, params, true));
        navLinks.add(new PageNavLink("gb.docs", DocsPage.class, params, true));
        if (app().settings().getBoolean(Keys.web.allowForking, true)) {
            pages.put("forks", new PageRegistration("gb.forks", ForksPage.class, params, true));
            navLinks.add(new PageNavLink("gb.forks", ForksPage.class, params, true));
        }
        pages.put("compare", new PageRegistration("gb.compare", ComparePage.class, params, true));
        navLinks.add(new PageNavLink("gb.compare", ComparePage.class, params, true));
        // conditional links
        // per-repository extra page links
        // per-repository extra navlinks
        if (JGitUtils.getPagesBranch(r) != null) {
            OtherPageLink pagesLink = new OtherPageLink("gb.pages", PagesServlet.asLink(
            ExternalNavLink pagesLink = new ExternalNavLink("gb.pages", PagesServlet.asLink(
                    getRequest().getRelativePathPrefixToContextRoot(), repositoryName, null), true);
            pages.put("pages", pagesLink);
            navLinks.add(pagesLink);
        }
        UserModel user = UserModel.ANONYMOUS;
        if (GitBlitWebSession.get().isLoggedIn()) {
            user = GitBlitWebSession.get().getUser();
        }
        // add repository nav link extensions
        List<RepositoryNavLinkExtension> extensions = app().plugins().getExtensions(RepositoryNavLinkExtension.class);
        for (RepositoryNavLinkExtension ext : extensions) {
            navLinks.addAll(ext.getNavLinks(user, model));
        }
        // Conditionally add edit link
@@ -233,9 +243,8 @@
            showAdmin = app().settings().getBoolean(Keys.web.allowAdministration, false);
        }
        isOwner = GitBlitWebSession.get().isLoggedIn()
                && (model.isOwner(GitBlitWebSession.get()
                        .getUsername()));
        return pages;
                && (model.isOwner(GitBlitWebSession.get().getUsername()));
        return navLinks;
    }
    protected boolean allowForkControls() {
src/main/java/com/gitblit/wicket/pages/RootPage.html
@@ -51,17 +51,19 @@
        <li class="dropdown">
            <a data-toggle="dropdown" class="dropdown-toggle" style="text-decoration: none;" href="#"><span wicket:id="username"></span> <b class="caret"></b></a>
            <ul class="dropdown-menu">
                <li style="color:#ccc;padding-left:15px;font-weight:bold;"><span wicket:id="displayName"></span></li>
                <li class="divider"></li>
                <li><a wicket:id="newRepository"><wicket:message key="gb.newRepository"></wicket:message></a></li>
                <li><a wicket:id="myProfile"><wicket:message key="gb.myProfile"></wicket:message></a></li>
                <li><a wicket:id="changePassword"><wicket:message key="gb.changePassword"></wicket:message></a></li>
                <li class="divider"></li>
                <span wicket:id="standardMenu"></span>
                <span wicket:id="adminMenu"></span>
                <span wicket:id="extensionsMenu"></span>
                <li><a wicket:id="logout"><wicket:message key="gb.logout"></wicket:message></a></li>
            </ul>
        </li>
    </wicket:fragment>
    
    <wicket:fragment wicket:id="submenuFragment">
        <li style="color:#ccc;padding-left:15px;font-weight:bold;"><span wicket:id="submenuTitle"></span></li>
        <li wicket:id="submenuItem"><span wicket:id="submenuLink"></span></li>
    </wicket:fragment>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/RootPage.java
@@ -41,24 +41,35 @@
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.protocol.http.WebResponse;
import com.gitblit.Constants;
import com.gitblit.Keys;
import com.gitblit.extensions.NavLinkExtension;
import com.gitblit.extensions.UserMenuExtension;
import com.gitblit.models.Menu.ExternalLinkMenuItem;
import com.gitblit.models.Menu.MenuDivider;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.Menu.PageLinkMenuItem;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.Menu.ToggleMenuItem;
import com.gitblit.models.NavLink;
import com.gitblit.models.NavLink.PageNavLink;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ModelUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownToggleItem;
import com.gitblit.wicket.SessionlessForm;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.NavigationPanel;
/**
@@ -164,36 +175,38 @@
            add(new Label("userPanel").setVisible(false));
        }
        boolean showRegistrations = app().federation().canFederate()
                && app().settings().getBoolean(Keys.web.showFederationRegistrations, false);
        // navigation links
        List<PageRegistration> pages = new ArrayList<PageRegistration>();
        List<NavLink> navLinks = new ArrayList<NavLink>();
        if (!authenticateView || (authenticateView && isLoggedIn)) {
            pages.add(new PageRegistration(isLoggedIn ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
            navLinks.add(new PageNavLink(isLoggedIn ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
                    getRootPageParameters()));
            if (isLoggedIn && app().tickets().isReady()) {
                pages.add(new PageRegistration("gb.myTickets", MyTicketsPage.class));
                navLinks.add(new PageNavLink("gb.myTickets", MyTicketsPage.class));
            }
            pages.add(new PageRegistration("gb.repositories", RepositoriesPage.class,
            navLinks.add(new PageNavLink("gb.repositories", RepositoriesPage.class,
                    getRootPageParameters()));
            pages.add(new PageRegistration("gb.activity", ActivityPage.class, getRootPageParameters()));
            navLinks.add(new PageNavLink("gb.activity", ActivityPage.class, getRootPageParameters()));
            if (allowLucene) {
                pages.add(new PageRegistration("gb.search", LuceneSearchPage.class));
            }
            if (showAdmin) {
                pages.add(new PageRegistration("gb.users", UsersPage.class));
            }
            if (showAdmin || showRegistrations) {
                pages.add(new PageRegistration("gb.federation", FederationPage.class));
                navLinks.add(new PageNavLink("gb.search", LuceneSearchPage.class));
            }
            if (!authenticateView || (authenticateView && isLoggedIn)) {
                addDropDownMenus(pages);
                addDropDownMenus(navLinks);
            }
            UserModel user = UserModel.ANONYMOUS;
            if (isLoggedIn) {
                user = GitBlitWebSession.get().getUser();
            }
            // add nav link extensions
            List<NavLinkExtension> extensions = app().plugins().getExtensions(NavLinkExtension.class);
            for (NavLinkExtension ext : extensions) {
                navLinks.addAll(ext.getNavLinks(user));
            }
        }
        NavigationPanel navPanel = new NavigationPanel("navPanel", getRootNavPageClass(), pages);
        NavigationPanel navPanel = new NavigationPanel("navPanel", getRootNavPageClass(), navLinks);
        add(navPanel);
        // display an error message cached from a redirect
@@ -285,13 +298,13 @@
        return repositoryModels;
    }
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
    }
    protected List<DropDownMenuItem> getRepositoryFilterItems(PageParameters params) {
    protected List<com.gitblit.models.Menu.MenuItem> getRepositoryFilterItems(PageParameters params) {
        final UserModel user = GitBlitWebSession.get().getUser();
        Set<DropDownMenuItem> filters = new LinkedHashSet<DropDownMenuItem>();
        Set<MenuItem> filters = new LinkedHashSet<MenuItem>();
        List<RepositoryModel> repositories = getRepositoryModels();
        // accessible repositories by federation set
@@ -310,11 +323,11 @@
            List<String> sets = new ArrayList<String>(setMap.keySet());
            Collections.sort(sets);
            for (String set : sets) {
                filters.add(new DropDownToggleItem(MessageFormat.format("{0} ({1})", set,
                filters.add(new ToggleMenuItem(MessageFormat.format("{0} ({1})", set,
                        setMap.get(set).get()), "set", set, params));
            }
            // divider
            filters.add(new DropDownMenuItem());
            filters.add(new MenuDivider());
        }
        // user's team memberships
@@ -322,11 +335,11 @@
            List<TeamModel> teams = new ArrayList<TeamModel>(user.teams);
            Collections.sort(teams);
            for (TeamModel team : teams) {
                filters.add(new DropDownToggleItem(MessageFormat.format("{0} ({1})", team.name,
                filters.add(new ToggleMenuItem(MessageFormat.format("{0} ({1})", team.name,
                        team.repositories.size()), "team", team.name, params));
            }
            // divider
            filters.add(new DropDownMenuItem());
            filters.add(new MenuDivider());
        }
        // custom filters
@@ -337,18 +350,18 @@
            for (String expression : expressions) {
                if (!StringUtils.isEmpty(expression)) {
                    addedExpression = true;
                    filters.add(new DropDownToggleItem(null, "x", expression, params));
                    filters.add(new ToggleMenuItem(null, "x", expression, params));
                }
            }
            // if we added any custom expressions, add a divider
            if (addedExpression) {
                filters.add(new DropDownMenuItem());
                filters.add(new MenuDivider());
            }
        }
        return new ArrayList<DropDownMenuItem>(filters);
        return new ArrayList<MenuItem>(filters);
    }
    protected List<DropDownMenuItem> getTimeFilterItems(PageParameters params) {
    protected List<MenuItem> getTimeFilterItems(PageParameters params) {
        // days back choices - additive parameters
        int daysBack = app().settings().getInteger(Keys.web.activityDuration, 7);
        int maxDaysBack = app().settings().getInteger(Keys.web.activityDurationMaximum, 30);
@@ -369,7 +382,7 @@
            clonedParams.put("db",  daysBack);
        }
        List<DropDownMenuItem> items = new ArrayList<DropDownMenuItem>();
        List<MenuItem> items = new ArrayList<MenuItem>();
        Set<Integer> choicesSet = new TreeSet<Integer>(app().settings().getIntegers(Keys.web.activityDurationChoices));
        if (choicesSet.isEmpty()) {
             choicesSet.addAll(Arrays.asList(1, 3, 7, 14, 21, 28));
@@ -379,13 +392,13 @@
        String lastDaysPattern = getString("gb.lastNDays");
        for (Integer db : choices) {
            if (db == 1) {
                items.add(new DropDownMenuItem(getString("gb.time.today"), "db", db.toString(), clonedParams));
                items.add(new ParameterMenuItem(getString("gb.time.today"), "db", db.toString(), clonedParams));
            } else {
                String txt = MessageFormat.format(lastDaysPattern, db);
                items.add(new DropDownMenuItem(txt, "db", db.toString(), clonedParams));
                items.add(new ParameterMenuItem(txt, "db", db.toString(), clonedParams));
            }
        }
        items.add(new DropDownMenuItem());
        items.add(new MenuDivider());
        return items;
    }
@@ -574,6 +587,11 @@
        public UserMenu(String id, String markupId, MarkupContainer markupProvider) {
            super(id, markupId, markupProvider);
            setRenderBodyOnly(true);
        }
        @Override
        protected void onInitialize() {
            super.onInitialize();
            GitBlitWebSession session = GitBlitWebSession.get();
            UserModel user = session.getUser();
@@ -586,19 +604,105 @@
                add(new Label("username", user.getDisplayName()));
            }
            add(new Label("displayName", user.getDisplayName()));
            List<MenuItem> standardItems = new ArrayList<MenuItem>();
            standardItems.add(new MenuDivider());
            if (user.canAdmin() || user.canCreate()) {
                standardItems.add(new PageLinkMenuItem("gb.newRepository", EditRepositoryPage.class));
            }
            standardItems.add(new PageLinkMenuItem("gb.myProfile", UserPage.class,
                    WicketUtils.newUsernameParameter(user.username)));
            if (editCredentials) {
                standardItems.add(new PageLinkMenuItem("gb.changePassword", ChangePasswordPage.class));
            }
            standardItems.add(new MenuDivider());
            add(newSubmenu("standardMenu", user.getDisplayName(), standardItems));
            add(new BookmarkablePageLink<Void>("newRepository",
                    EditRepositoryPage.class).setVisible(user.canAdmin() || user.canCreate()));
            if (showAdmin) {
                // admin menu
                List<MenuItem> adminItems = new ArrayList<MenuItem>();
                adminItems.add(new MenuDivider());
                adminItems.add(new PageLinkMenuItem("gb.users", UsersPage.class));
                adminItems.add(new PageLinkMenuItem("gb.teams", TeamsPage.class));
            add(new BookmarkablePageLink<Void>("myProfile",
                    UserPage.class, WicketUtils.newUsernameParameter(user.username)));
                boolean showRegistrations = app().federation().canFederate()
                        && app().settings().getBoolean(Keys.web.showFederationRegistrations, false);
                if (showRegistrations) {
                    adminItems.add(new PageLinkMenuItem("gb.federation", FederationPage.class));
                }
                adminItems.add(new MenuDivider());
            add(new BookmarkablePageLink<Void>("changePassword",
                    ChangePasswordPage.class).setVisible(editCredentials));
                add(newSubmenu("adminMenu", getString("gb.administration"), adminItems));
            } else {
                add(new Label("adminMenu").setVisible(false));
            }
            // plugin extension items
            List<MenuItem> extensionItems = new ArrayList<MenuItem>();
            List<UserMenuExtension> extensions = app().plugins().getExtensions(UserMenuExtension.class);
            for (UserMenuExtension ext : extensions) {
                List<MenuItem> items = ext.getMenuItems(user);
                extensionItems.addAll(items);
            }
            if (extensionItems.isEmpty()) {
                // no extension items
                add(new Label("extensionsMenu").setVisible(false));
            } else {
                // found extension items
                extensionItems.add(0, new MenuDivider());
                add(newSubmenu("extensionsMenu", getString("gb.extensions"), extensionItems));
                extensionItems.add(new MenuDivider());
            }
            add(new BookmarkablePageLink<Void>("logout",
                    LogoutPage.class).setVisible(standardLogin));
        }
        /**
         * Creates a submenu.  This is not actually submenu because we're using
         * an older Twitter Bootstrap which is pre-submenu.
         *
         * @param wicketId
         * @param submenuTitle
         * @param menuItems
         * @return a submenu fragment
         */
        private Fragment newSubmenu(String wicketId, String submenuTitle, List<MenuItem> menuItems) {
            Fragment submenu = new Fragment(wicketId, "submenuFragment", this);
            submenu.add(new Label("submenuTitle", submenuTitle).setRenderBodyOnly(true));
            ListDataProvider<MenuItem> menuItemsDp = new ListDataProvider<MenuItem>(menuItems);
            DataView<MenuItem> submenuItems = new DataView<MenuItem>("submenuItem", menuItemsDp) {
                private static final long serialVersionUID = 1L;
                @Override
                public void populateItem(final Item<MenuItem> menuItem) {
                    final MenuItem item = menuItem.getModelObject();
                    String name = item.toString();
                    try {
                        // try to lookup translation
                        name = getString(name);
                    } catch (Exception e) {
                    }
                    if (item instanceof PageLinkMenuItem) {
                        // link to another Wicket page
                        PageLinkMenuItem pageLink = (PageLinkMenuItem) item;
                        menuItem.add(new LinkPanel("submenuLink", null, null, name, pageLink.getPageClass(),
                                pageLink.getPageParameters(), false).setRenderBodyOnly(true));
                    } else if (item instanceof ExternalLinkMenuItem) {
                        // link to a specified href
                        ExternalLinkMenuItem extLink = (ExternalLinkMenuItem) item;
                        menuItem.add(new LinkPanel("submenuLink", null, name, extLink.getHref(),
                                extLink.openInNewWindow()).setRenderBodyOnly(true));
                    } else if (item instanceof MenuDivider) {
                        // divider
                        menuItem.add(new Label("submenuLink").setRenderBodyOnly(true));
                        WicketUtils.setCssClass(menuItem, "divider");
                    }
                }
            };
            submenu.add(submenuItems);
            submenu.setRenderBodyOnly(true);
            return submenu;
        }
    }
}
src/main/java/com/gitblit/wicket/pages/TeamsPage.html
New file
@@ -0,0 +1,13 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="en"
      lang="en">
<body>
<wicket:extend>
<div class="container">
    <div wicket:id="teamsPanel">[teams panel]</div>
</div>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/TeamsPage.java
New file
@@ -0,0 +1,30 @@
/*
 * 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.pages;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.panels.TeamsPanel;
@RequiresAdminRole
public class TeamsPage extends RootPage {
    public TeamsPage() {
        super();
        setupPage("", "");
        add(new TeamsPanel("teamsPanel", showAdmin).setVisible(showAdmin));
    }
}
src/main/java/com/gitblit/wicket/pages/UserPage.java
@@ -29,6 +29,9 @@
import org.eclipse.jgit.lib.PersonIdent;
import com.gitblit.Keys;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.NavLink;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
@@ -36,9 +39,6 @@
import com.gitblit.wicket.GitBlitWebApp;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.GitblitRedirectException;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
import com.gitblit.wicket.panels.LinkPanel;
@@ -127,10 +127,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                UserPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -140,9 +140,9 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(menu);
        navLinks.add(menu);
    }
}
src/main/java/com/gitblit/wicket/pages/UsersPage.html
@@ -6,8 +6,6 @@
<body>
<wicket:extend>
<div class="container">
    <div wicket:id="teamsPanel">[teams panel]</div>
    <div wicket:id="usersPanel">[users panel]</div>
</div>
</wicket:extend>
src/main/java/com/gitblit/wicket/pages/UsersPage.java
@@ -16,7 +16,6 @@
package com.gitblit.wicket.pages;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.panels.TeamsPanel;
import com.gitblit.wicket.panels.UsersPanel;
@RequiresAdminRole
@@ -25,8 +24,6 @@
    public UsersPage() {
        super();
        setupPage("", "");
        add(new TeamsPanel("teamsPanel", showAdmin).setVisible(showAdmin));
        add(new UsersPanel("usersPanel", showAdmin).setVisible(showAdmin));
    }
src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
@@ -21,38 +21,90 @@
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.models.Menu.ExternalLinkMenuItem;
import com.gitblit.models.Menu.MenuDivider;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.Menu.PageLinkMenuItem;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownMenuNavLink;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.wicket.WicketUtils;
public class DropDownMenu extends Panel {
    private static final long serialVersionUID = 1L;
    public DropDownMenu(String id, String label, final DropDownMenuRegistration menu) {
    public DropDownMenu(String id, String label, final DropDownPageMenuNavLink menu) {
        super(id);
        add(new Label("label", label).setRenderBodyOnly(true));
        ListDataProvider<DropDownMenuItem> items = new ListDataProvider<DropDownMenuItem>(
                menu.menuItems);
        DataView<DropDownMenuItem> view = new DataView<DropDownMenuItem>("menuItems", items) {
        ListDataProvider<MenuItem> items = new ListDataProvider<MenuItem>(menu.menuItems);
        DataView<MenuItem> view = new DataView<MenuItem>("menuItems", items) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<DropDownMenuItem> item) {
                DropDownMenuItem entry = item.getModelObject();
                if (entry.isDivider()) {
            public void populateItem(final Item<MenuItem> item) {
                MenuItem entry = item.getModelObject();
                if (entry instanceof PageLinkMenuItem) {
                    // link to another Wicket page
                    PageLinkMenuItem pageLink = (PageLinkMenuItem) entry;
                    item.add(new LinkPanel("menuItem", null, null, pageLink.toString(), pageLink.getPageClass(),
                            pageLink.getPageParameters(), false).setRenderBodyOnly(true));
                } else if (entry instanceof ExternalLinkMenuItem) {
                    // link to a specified href
                    ExternalLinkMenuItem extLink = (ExternalLinkMenuItem) entry;
                    item.add(new LinkPanel("menuItem", null, extLink.toString(), extLink.getHref(),
                            extLink.openInNewWindow()).setRenderBodyOnly(true));
                } else if (entry instanceof MenuDivider) {
                    // divider
                    item.add(new Label("menuItem").setRenderBodyOnly(true));
                    WicketUtils.setCssClass(item, "divider");
                } else {
                    ParameterMenuItem parameter = (ParameterMenuItem) entry;
                    // parameter link for the current page
                    String icon = null;
                    if (entry.isSelected()) {
                    if (parameter.isSelected()) {
                        icon = "icon-ok";
                    } else {
                        icon = "icon-ok-white";
                    }
                    item.add(new LinkPanel("menuItem", icon, null, entry.toString(), menu.pageClass,
                            entry.getPageParameters(), false).setRenderBodyOnly(true));
                            parameter.getPageParameters(), false).setRenderBodyOnly(true));
                }
            }
        };
        add(view);
        setRenderBodyOnly(true);
    }
    public DropDownMenu(String id, String label, final DropDownMenuNavLink menu) {
        super(id);
        add(new Label("label", label).setRenderBodyOnly(true));
        ListDataProvider<MenuItem> items = new ListDataProvider<MenuItem>(menu.menuItems);
        DataView<MenuItem> view = new DataView<MenuItem>("menuItems", items) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<MenuItem> item) {
                MenuItem entry = item.getModelObject();
                if (entry instanceof PageLinkMenuItem) {
                    // link to another Wicket page
                    PageLinkMenuItem pageLink = (PageLinkMenuItem) entry;
                    item.add(new LinkPanel("menuItem", null, null, pageLink.toString(), pageLink.getPageClass(),
                            pageLink.getPageParameters(), false).setRenderBodyOnly(true));
                } else if (entry instanceof ExternalLinkMenuItem) {
                    // link to a specified href
                    ExternalLinkMenuItem extLink = (ExternalLinkMenuItem) entry;
                    item.add(new LinkPanel("menuItem", null, extLink.toString(), extLink.getHref(),
                            extLink.openInNewWindow()).setRenderBodyOnly(true));
                } else if (entry instanceof MenuDivider) {
                    // divider
                    item.add(new Label("menuItem").setRenderBodyOnly(true));
                    WicketUtils.setCssClass(item, "divider");
                } else {
                    throw new IllegalArgumentException(String.format("Unexpected menuitem type %s",
                            entry.getClass().getSimpleName()));
                }
            }
        };
src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
@@ -23,9 +23,11 @@
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.PageRegistration.OtherPageLink;
import com.gitblit.models.NavLink;
import com.gitblit.models.NavLink.DropDownMenuNavLink;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.NavLink.ExternalNavLink;
import com.gitblit.models.NavLink.PageNavLink;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.BasePage;
@@ -34,45 +36,59 @@
    private static final long serialVersionUID = 1L;
    public NavigationPanel(String id, final Class<? extends BasePage> pageClass,
            List<PageRegistration> registeredPages) {
            List<NavLink> navLinks) {
        super(id);
        ListDataProvider<PageRegistration> refsDp = new ListDataProvider<PageRegistration>(
                registeredPages);
        DataView<PageRegistration> refsView = new DataView<PageRegistration>("navLink", refsDp) {
        ListDataProvider<NavLink> refsDp = new ListDataProvider<NavLink>(navLinks);
        DataView<NavLink> linksView = new DataView<NavLink>("navLink", refsDp) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<PageRegistration> item) {
                PageRegistration entry = item.getModelObject();
                if (entry.hiddenPhone) {
            public void populateItem(final Item<NavLink> item) {
                NavLink navLink = item.getModelObject();
                String linkText = navLink.translationKey;
                try {
                    // try to lookup translation key
                    linkText = getString(navLink.translationKey);
                } catch (Exception e) {
                }
                if (navLink.hiddenPhone) {
                    WicketUtils.setCssClass(item, "hidden-phone");
                }
                if (entry instanceof OtherPageLink) {
                if (navLink instanceof ExternalNavLink) {
                    // other link
                    OtherPageLink link = (OtherPageLink) entry;
                    Component c = new LinkPanel("link", null, getString(entry.translationKey), link.url);
                    ExternalNavLink link = (ExternalNavLink) navLink;
                    Component c = new LinkPanel("link", null, linkText, link.url);
                    c.setRenderBodyOnly(true);
                    item.add(c);
                } else if (entry instanceof DropDownMenuRegistration) {
                } else if (navLink instanceof DropDownPageMenuNavLink) {
                    // drop down menu
                    DropDownMenuRegistration reg = (DropDownMenuRegistration) entry;
                    Component c = new DropDownMenu("link", getString(entry.translationKey), reg);
                    DropDownPageMenuNavLink reg = (DropDownPageMenuNavLink) navLink;
                    Component c = new DropDownMenu("link", linkText, reg);
                    c.setRenderBodyOnly(true);
                    item.add(c);
                    WicketUtils.setCssClass(item, "dropdown");
                } else {
                    // standard page link
                    Component c = new LinkPanel("link", null, getString(entry.translationKey),
                            entry.pageClass, entry.params);
                } else if (navLink instanceof DropDownMenuNavLink) {
                    // drop down menu
                    DropDownMenuNavLink reg = (DropDownMenuNavLink) navLink;
                    Component c = new DropDownMenu("link", linkText, reg);
                    c.setRenderBodyOnly(true);
                    if (entry.pageClass.equals(pageClass)) {
                    item.add(c);
                    WicketUtils.setCssClass(item, "dropdown");
                } else if (navLink instanceof PageNavLink) {
                    PageNavLink reg = (PageNavLink) navLink;
                    // standard page link
                    Component c = new LinkPanel("link", null, linkText,
                            reg.pageClass, reg.params);
                    c.setRenderBodyOnly(true);
                    if (reg.pageClass.equals(pageClass)) {
                        WicketUtils.setCssClass(item, "active");
                    }
                    item.add(c);
                }
            }
        };
        add(refsView);
        add(linksView);
    }
}
src/site/plugins_extensions.mkd
@@ -52,6 +52,37 @@
    public void onUninstall() {
    }
}
/**
 * You can also create Webapp plugins that register mounted pages.
 */
public class ExampleWicketPlugin extends GitblitWicketPlugin {
    @Override
    public void start() {
    }
    @Override
    public void stop() {
    }
    @Override
    public void onInstall() {
    }
    @Override
    public void onUpgrade(Version oldVersion) {
    }
    @Override
    public void onUninstall() {
    }
    @Override
    protected void init(GitblitWicketApp app) {
        app.mount("/logo", LogoPage.class);
        app.mount("/hello", HelloWorldPage.class);
    }
}
```
### SSH Dispatch Command
@@ -185,3 +216,72 @@
}
```
### Request Filter
*SINCE 1.6.0*
You can provide your own custom request filter by subclassing the *HttpRequestFilter* class.
```java
import com.gitblit.extensions.HttpRequestFilter;
import ro.fortsoft.pf4j.Extension;
@Extension
public class MyRequestFilter extends HttpRequestFilter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
    }
}
```
### User Menu Items
*SINCE 1.6.0*
You can provide your own user menu items by subclassing the *UserMenuExtension* class.
```java
import java.util.Arrays;
import java.util.List;
import ro.fortsoft.pf4j.Extension;
import com.gitblit.extensions.UserMenuExtension;
import com.gitblit.models.Menu.ExternalLinkMenuItem;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.UserModel;
@Extension
public class MyUserMenuContributor extends UserMenuExtension {
    @Override
    public List<MenuItem> getMenuItems(UserModel user) {
        MenuItem item = new ExternalLinkMenuItem("Github", String.format("https://github.com/%s", user.username));
        return Arrays.asList(item);
    }
}
```
### Navigation Links
*SINCE 1.6.0*
You can provide your own top-level navigation links by subclassing the *NavLinkExtension* class.
```java
import java.util.Arrays;
import java.util.List;
import ro.fortsoft.pf4j.Extension;
import com.gitblit.extensions.NavLinkExtension;
import com.gitblit.models.UserModel;
@Extension
public class MyNavLink extends NavLinkExtension {
    @Override
    public List<NavLink> getNavLinks(UserModel user) {
        NavLink link = new ExternalLinkMenuItem("Github", String.format("https://github.com/%s", user.username));
        return Arrays.asList(link);
    }
}
```