From 8f0f665b9ee4e2cd21e9e0d5d7cfc69b1d19b86f Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Mon, 05 May 2014 12:46:02 -0400
Subject: [PATCH] Merged #23 "Enhance the plugin infrastructure to allow deeper plugin integration"

---
 src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java      |   39 
 src/main/java/com/gitblit/wicket/pages/UsersPage.java                |    3 
 src/main/java/com/gitblit/dagger/DaggerFilter.java                   |    4 
 src/main/java/com/gitblit/models/Menu.java                           |  302 +++++
 src/main/java/WEB-INF/web.xml                                        |   11 
 src/main/java/com/gitblit/wicket/pages/RootPage.html                 |   14 
 src/main/java/com/gitblit/wicket/PluginClassResolver.java            |  122 ++
 src/main/java/com/gitblit/wicket/GitBlitWebApp.java                  |  108 +
 src/main/java/com/gitblit/wicket/pages/UsersPage.html                |    2 
 src/main/java/com/gitblit/wicket/panels/DropDownMenu.java            |   74 +
 src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java       |    5 
 src/main/java/com/gitblit/servlet/FilterRuntimeConfig.java           |   71 +
 src/main/java/com/gitblit/wicket/pages/RootPage.java                 | 1312 +++++++++++++-----------
 src/main/java/com/gitblit/servlet/SyndicationFilter.java             |    5 
 src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java |   42 
 src/main/java/com/gitblit/wicket/pages/ProjectPage.java              |   30 
 src/main/java/com/gitblit/wicket/pages/RepositoryPage.java           |   61 
 src/main/java/com/gitblit/wicket/pages/TeamsPage.html                |   13 
 src/main/java/com/gitblit/wicket/pages/ProjectsPage.java             |   14 
 src/main/java/com/gitblit/extensions/NavLinkExtension.java           |   40 
 src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java         |   14 
 src/main/java/com/gitblit/wicket/pages/TeamsPage.java                |   30 
 src/main/java/com/gitblit/wicket/pages/UserPage.java                 |   14 
 src/main/java/com/gitblit/extensions/HttpRequestFilter.java          |   49 
 src/main/java/com/gitblit/wicket/GitBlitWebApp.properties            |    4 
 src/main/java/com/gitblit/servlet/RpcFilter.java                     |    5 
 src/main/java/com/gitblit/servlet/ProxyFilter.java                   |   86 +
 src/main/java/com/gitblit/servlet/GitFilter.java                     |    5 
 src/main/java/com/gitblit/wicket/GitblitWicketApp.java               |   72 +
 src/main/java/com/gitblit/extensions/UserMenuExtension.java          |   40 
 /dev/null                                                            |  243 ----
 src/site/plugins_extensions.mkd                                      |  100 +
 src/main/java/com/gitblit/servlet/EnforceAuthenticationFilter.java   |    3 
 src/main/java/com/gitblit/wicket/pages/ActivityPage.java             |   16 
 src/main/java/com/gitblit/wicket/pages/DashboardPage.java            |   14 
 src/main/java/com/gitblit/wicket/panels/NavigationPanel.java         |   60 
 src/main/java/com/gitblit/servlet/AuthenticationFilter.java          |    3 
 src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java        |   49 
 src/main/java/com/gitblit/models/NavLink.java                        |  140 ++
 39 files changed, 2,232 insertions(+), 987 deletions(-)

diff --git a/src/main/java/WEB-INF/web.xml b/src/main/java/WEB-INF/web.xml
index cb483af..3a6c449 100644
--- a/src/main/java/WEB-INF/web.xml
+++ b/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
@@ -353,4 +362,4 @@
         <url-pattern>/*</url-pattern>
     </filter-mapping>
     
-</web-app>
+</web-app>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/dagger/DaggerFilter.java b/src/main/java/com/gitblit/dagger/DaggerFilter.java
index 1c73d4b..01c07a4 100644
--- a/src/main/java/com/gitblit/dagger/DaggerFilter.java
+++ b/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() {
diff --git a/src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java b/src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java
new file mode 100644
index 0000000..130f499
--- /dev/null
+++ b/src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java
@@ -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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/extensions/HttpRequestFilter.java b/src/main/java/com/gitblit/extensions/HttpRequestFilter.java
new file mode 100644
index 0000000..e3e330c
--- /dev/null
+++ b/src/main/java/com/gitblit/extensions/HttpRequestFilter.java
@@ -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;
+}
diff --git a/src/main/java/com/gitblit/extensions/NavLinkExtension.java b/src/main/java/com/gitblit/extensions/NavLinkExtension.java
new file mode 100644
index 0000000..c895860
--- /dev/null
+++ b/src/main/java/com/gitblit/extensions/NavLinkExtension.java
@@ -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);
+}
diff --git a/src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java b/src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
new file mode 100644
index 0000000..2b05c5a
--- /dev/null
+++ b/src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
@@ -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);
+}
diff --git a/src/main/java/com/gitblit/extensions/UserMenuExtension.java b/src/main/java/com/gitblit/extensions/UserMenuExtension.java
new file mode 100644
index 0000000..078dbfd
--- /dev/null
+++ b/src/main/java/com/gitblit/extensions/UserMenuExtension.java
@@ -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);
+}
diff --git a/src/main/java/com/gitblit/models/Menu.java b/src/main/java/com/gitblit/models/Menu.java
new file mode 100644
index 0000000..7c949b3
--- /dev/null
+++ b/src/main/java/com/gitblit/models/Menu.java
@@ -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;
+		}
+	}
+}
diff --git a/src/main/java/com/gitblit/models/NavLink.java b/src/main/java/com/gitblit/models/NavLink.java
new file mode 100644
index 0000000..993d695
--- /dev/null
+++ b/src/main/java/com/gitblit/models/NavLink.java
@@ -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>();
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java b/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
index e6a0169..0e6d323 100644
--- a/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
+++ b/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);
 	}
diff --git a/src/main/java/com/gitblit/servlet/AuthenticationFilter.java b/src/main/java/com/gitblit/servlet/AuthenticationFilter.java
index dd821ac..5710a4a 100644
--- a/src/main/java/com/gitblit/servlet/AuthenticationFilter.java
+++ b/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);
 	}
 
diff --git a/src/main/java/com/gitblit/servlet/EnforceAuthenticationFilter.java b/src/main/java/com/gitblit/servlet/EnforceAuthenticationFilter.java
index 5fdccb7..c015021 100644
--- a/src/main/java/com/gitblit/servlet/EnforceAuthenticationFilter.java
+++ b/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);
 	}
diff --git a/src/main/java/com/gitblit/servlet/FilterRuntimeConfig.java b/src/main/java/com/gitblit/servlet/FilterRuntimeConfig.java
new file mode 100644
index 0000000..9f0c0ac
--- /dev/null
+++ b/src/main/java/com/gitblit/servlet/FilterRuntimeConfig.java
@@ -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();
+	}
+}
diff --git a/src/main/java/com/gitblit/servlet/GitFilter.java b/src/main/java/com/gitblit/servlet/GitFilter.java
index f9c062d..bb3d321 100644
--- a/src/main/java/com/gitblit/servlet/GitFilter.java
+++ b/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);
 	}
diff --git a/src/main/java/com/gitblit/servlet/ProxyFilter.java b/src/main/java/com/gitblit/servlet/ProxyFilter.java
new file mode 100644
index 0000000..46f59de
--- /dev/null
+++ b/src/main/java/com/gitblit/servlet/ProxyFilter.java
@@ -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();
+		}
+	}
+}
diff --git a/src/main/java/com/gitblit/servlet/RpcFilter.java b/src/main/java/com/gitblit/servlet/RpcFilter.java
index e0b1a23..23bf956 100644
--- a/src/main/java/com/gitblit/servlet/RpcFilter.java
+++ b/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);
 	}
diff --git a/src/main/java/com/gitblit/servlet/SyndicationFilter.java b/src/main/java/com/gitblit/servlet/SyndicationFilter.java
index 67a845e..78da47e 100644
--- a/src/main/java/com/gitblit/servlet/SyndicationFilter.java
+++ b/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);
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
index dc79af2..d3aa62f 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
+++ b/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");
 
+		// 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);
+			}
+		}
+
+		 // customize the Wicket class resolver to load from plugins
+        PluginClassResolver classResolver = new PluginClassResolver(pluginManager);
+        getApplicationSettings().setClassResolver(classResolver);
+
 		getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
-		super.init();
 	}
 
-	private void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
+	/* (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();
 	}
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index 0ed2ed5..d0c2d48 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/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
+
diff --git a/src/main/java/com/gitblit/wicket/GitblitWicketApp.java b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java
new file mode 100644
index 0000000..a56e699
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitblitWicketApp.java
@@ -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();
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/PageRegistration.java b/src/main/java/com/gitblit/wicket/PageRegistration.java
deleted file mode 100644
index 1b98f2c..0000000
--- a/src/main/java/com/gitblit/wicket/PageRegistration.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright 2011 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.gitblit.wicket;
-
-import java.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.utils.StringUtils;
-
-/**
- * Represents a page link registration for the topbar.
- *
- * @author James Moger
- *
- */
-public class PageRegistration implements Serializable {
-	private static final long serialVersionUID = 1L;
-
-	public final String translationKey;
-	public final Class<? extends WebPage> pageClass;
-	public final PageParameters params;
-	public final boolean hiddenPhone;
-
-	public PageRegistration(String translationKey, Class<? extends WebPage> pageClass) {
-		this(translationKey, pageClass, null);
-	}
-
-	public PageRegistration(String translationKey, Class<? extends WebPage> pageClass,
-			PageParameters params) {
-		this(translationKey, pageClass, params, false);
-	}
-
-	public PageRegistration(String translationKey, Class<? extends WebPage> pageClass,
-			PageParameters params, boolean hiddenPhone) {
-		this.translationKey = translationKey;
-		this.pageClass = pageClass;
-		this.params = params;
-		this.hiddenPhone = hiddenPhone;
-	}
-
-	/**
-	 * Represents a page link to a non-Wicket page. Might be external.
-	 *
-	 * @author James Moger
-	 *
-	 */
-	public static class OtherPageLink extends PageRegistration {
-
-		private static final long serialVersionUID = 1L;
-
-		public final String url;
-
-		public OtherPageLink(String translationKey, String url) {
-			super(translationKey, null);
-			this.url = url;
-		}
-
-		public OtherPageLink(String translationKey, String url, boolean hiddenPhone) {
-			super(translationKey, null, null, hiddenPhone);
-			this.url = url;
-		}
-	}
-
-	/**
-	 * Represents a DropDownMenu for the topbar
-	 *
-	 * @author James Moger
-	 *
-	 */
-	public static class DropDownMenuRegistration extends PageRegistration {
-
-		private static final long serialVersionUID = 1L;
-
-		public final List<DropDownMenuItem> menuItems;
-
-		public DropDownMenuRegistration(String translationKey, Class<? extends WebPage> pageClass) {
-			super(translationKey, pageClass);
-			menuItems = new ArrayList<DropDownMenuItem>();
-		}
-	}
-
-	/**
-	 * A MenuItem for the DropDownMenu.
-	 *
-	 * @author James Moger
-	 *
-	 */
-	public static class DropDownMenuItem implements Serializable {
-
-		private static final long serialVersionUID = 1L;
-
-		final PageParameters parameters;
-		final String displayText;
-		final String parameter;
-		final String value;
-		final boolean isSelected;
-
-		/**
-		 * Divider constructor.
-		 */
-		public DropDownMenuItem() {
-			this(null, null, null, null);
-		}
-
-		/**
-		 * Standard Menu Item constructor.
-		 *
-		 * @param displayText
-		 * @param parameter
-		 * @param value
-		 */
-		public DropDownMenuItem(String displayText, String parameter, String value) {
-			this(displayText, parameter, value, null);
-		}
-
-		/**
-		 * Standard Menu Item constructor that preserves aggregate parameters.
-		 *
-		 * @param displayText
-		 * @param parameter
-		 * @param value
-		 */
-		public DropDownMenuItem(String displayText, String parameter, String value,
-				PageParameters params) {
-			this.displayText = 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 isDivider() {
-			return displayText == null && value == null && parameter == null;
-		}
-
-		public boolean isSelected() {
-			return isSelected;
-		}
-
-		@Override
-		public int hashCode() {
-			if (isDivider()) {
-				// divider menu item
-				return super.hashCode();
-			}
-			if (StringUtils.isEmpty(displayText)) {
-				return value.hashCode() + parameter.hashCode();
-			}
-			return displayText.hashCode();
-		}
-
-		@Override
-		public boolean equals(Object o) {
-			if (o instanceof DropDownMenuItem) {
-				return hashCode() == o.hashCode();
-			}
-			return false;
-		}
-
-		@Override
-		public String toString() {
-			if (StringUtils.isEmpty(displayText)) {
-				return formatParameter();
-			}
-			return displayText;
-		}
-	}
-
-	public static class DropDownToggleItem extends DropDownMenuItem {
-
-		private static final long serialVersionUID = 1L;
-
-		/**
-		 * Toggle Menu Item constructor that preserves aggregate parameters.
-		 *
-		 * @param displayText
-		 * @param parameter
-		 * @param value
-		 */
-		public DropDownToggleItem(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);
-			}
-		}
-	}
-}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/PluginClassResolver.java b/src/main/java/com/gitblit/wicket/PluginClassResolver.java
new file mode 100644
index 0000000..ba53b04
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/PluginClassResolver.java
@@ -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);
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java b/src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java
new file mode 100644
index 0000000..90f4b32
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java
@@ -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());
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ActivityPage.java b/src/main/java/com/gitblit/wicket/pages/ActivityPage.java
index f0e390d..c505a66 100644
--- a/src/main/java/com/gitblit/wicket/pages/ActivityPage.java
+++ b/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);
 	}
 
 	/**
@@ -209,7 +209,7 @@
 		}
 		charts.addChart(chart);
 
-		// active repositories pie chart 
+		// active repositories pie chart
 		chart = charts.createPieChart("chartRepositories", getString("gb.activeRepositories"),
 				getString("gb.repository"), getString("gb.commits"));
 		for (Metric metric : repositoryMetrics.values()) {
diff --git a/src/main/java/com/gitblit/wicket/pages/DashboardPage.java b/src/main/java/com/gitblit/wicket/pages/DashboardPage.java
index 9853449..9c10e01 100644
--- a/src/main/java/com/gitblit/wicket/pages/DashboardPage.java
+++ b/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);
 	}
 
 
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java b/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
index b92282b..d358b77 100644
--- a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
+++ b/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;
 	}
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java b/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
index 77d4984..f04fa78 100644
--- a/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
+++ b/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);
 	}
 }
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
index f4ddf40..a0b15a8 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
+++ b/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) {
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
index 5ea99fd..165feed 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/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() {
diff --git a/src/main/java/com/gitblit/wicket/pages/RootPage.html b/src/main/java/com/gitblit/wicket/pages/RootPage.html
index 11f7f38..2ff305f 100644
--- a/src/main/java/com/gitblit/wicket/pages/RootPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/RootPage.html
@@ -51,16 +51,18 @@
 		<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>
diff --git a/src/main/java/com/gitblit/wicket/pages/RootPage.java b/src/main/java/com/gitblit/wicket/pages/RootPage.java
index c59c189..a2f3a49 100644
--- a/src/main/java/com/gitblit/wicket/pages/RootPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RootPage.java
@@ -1,604 +1,708 @@
-/*
- * 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 java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.regex.Pattern;
-
-import org.apache.wicket.MarkupContainer;
-import org.apache.wicket.PageParameters;
-import org.apache.wicket.behavior.HeaderContributor;
-import org.apache.wicket.markup.html.IHeaderContributor;
-import org.apache.wicket.markup.html.IHeaderResponse;
-import org.apache.wicket.markup.html.basic.Label;
-import org.apache.wicket.markup.html.form.PasswordTextField;
-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.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.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.NavigationPanel;
-
-/**
- * Root page is a topbar, navigable page like Repositories, Users, or
- * Federation.
- *
- * @author James Moger
- *
- */
-public abstract class RootPage extends BasePage {
-
-	boolean showAdmin;
-
-	IModel<String> username = new Model<String>("");
-	IModel<String> password = new Model<String>("");
-	List<RepositoryModel> repositoryModels = new ArrayList<RepositoryModel>();
-
-	public RootPage() {
-		super();
-	}
-
-	public RootPage(PageParameters params) {
-		super(params);
-	}
-
-	@Override
-	protected void setupPage(String repositoryName, String pageName) {
-
-		// CSS header overrides
-		add(new HeaderContributor(new IHeaderContributor() {
-			private static final long serialVersionUID = 1L;
-
-			@Override
-			public void renderHead(IHeaderResponse response) {
-				StringBuilder buffer = new StringBuilder();
-				buffer.append("<style type=\"text/css\">\n");
-				buffer.append(".navbar-inner {\n");
-				final String headerBackground = app().settings().getString(Keys.web.headerBackgroundColor, null);
-				if (!StringUtils.isEmpty(headerBackground)) {
-					buffer.append(MessageFormat.format("background-color: {0};\n", headerBackground));
-				}
-				final String headerBorder = app().settings().getString(Keys.web.headerBorderColor, null);
-				if (!StringUtils.isEmpty(headerBorder)) {
-					buffer.append(MessageFormat.format("border-bottom: 1px solid {0} !important;\n", headerBorder));
-				}
-				buffer.append("}\n");
-				final String headerBorderFocus = app().settings().getString(Keys.web.headerBorderFocusColor, null);
-				if (!StringUtils.isEmpty(headerBorderFocus)) {
-					buffer.append(".navbar ul li:focus, .navbar .active {\n");
-					buffer.append(MessageFormat.format("border-bottom: 4px solid {0};\n", headerBorderFocus));
-					buffer.append("}\n");
-				}
-				final String headerForeground = app().settings().getString(Keys.web.headerForegroundColor, null);
-				if (!StringUtils.isEmpty(headerForeground)) {
-					buffer.append(".navbar ul.nav li a {\n");
-					buffer.append(MessageFormat.format("color: {0};\n", headerForeground));
-					buffer.append("}\n");
-					buffer.append(".navbar ul.nav .active a {\n");
-					buffer.append(MessageFormat.format("color: {0};\n", headerForeground));
-					buffer.append("}\n");
-				}
-				final String headerHover = app().settings().getString(Keys.web.headerHoverColor, null);
-				if (!StringUtils.isEmpty(headerHover)) {
-					buffer.append(".navbar ul.nav li a:hover {\n");
-					buffer.append(MessageFormat.format("color: {0} !important;\n", headerHover));
-					buffer.append("}\n");
-				}
-				buffer.append("</style>\n");
-				response.renderString(buffer.toString());
-				}
-			}));
-
-		boolean authenticateView = app().settings().getBoolean(Keys.web.authenticateViewPages, false);
-		boolean authenticateAdmin = app().settings().getBoolean(Keys.web.authenticateAdminPages, true);
-		boolean allowAdmin = app().settings().getBoolean(Keys.web.allowAdministration, true);
-		boolean allowLucene = app().settings().getBoolean(Keys.web.allowLuceneIndexing, true);
-		boolean isLoggedIn = GitBlitWebSession.get().isLoggedIn();
-
-		if (authenticateAdmin) {
-			showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
-			// authentication requires state and session
-			setStatelessHint(false);
-		} else {
-			showAdmin = allowAdmin;
-			if (authenticateView) {
-				// authentication requires state and session
-				setStatelessHint(false);
-			} else {
-				// no authentication required, no state and no session required
-				setStatelessHint(true);
-			}
-		}
-
-		if (authenticateView || authenticateAdmin) {
-			if (isLoggedIn) {
-				UserMenu userFragment = new UserMenu("userPanel", "userMenuFragment", RootPage.this);
-				add(userFragment);
-			} else {
-				LoginForm loginForm = new LoginForm("userPanel", "loginFormFragment", RootPage.this);
-				add(loginForm);
-			}
-		} else {
-			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>();
-		if (!authenticateView || (authenticateView && isLoggedIn)) {
-			pages.add(new PageRegistration(isLoggedIn ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
-					getRootPageParameters()));
-			if (isLoggedIn && app().tickets().isReady()) {
-				pages.add(new PageRegistration("gb.myTickets", MyTicketsPage.class));
-			}
-			pages.add(new PageRegistration("gb.repositories", RepositoriesPage.class,
-					getRootPageParameters()));
-			pages.add(new PageRegistration("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));
-			}
-
-			if (!authenticateView || (authenticateView && isLoggedIn)) {
-				addDropDownMenus(pages);
-			}
-		}
-
-		NavigationPanel navPanel = new NavigationPanel("navPanel", getRootNavPageClass(), pages);
-		add(navPanel);
-
-		// display an error message cached from a redirect
-		String cachedMessage = GitBlitWebSession.get().clearErrorMessage();
-		if (!StringUtils.isEmpty(cachedMessage)) {
-			error(cachedMessage);
-		} else if (showAdmin) {
-			int pendingProposals = app().federation().getPendingFederationProposals().size();
-			if (pendingProposals == 1) {
-				info(getString("gb.OneProposalToReview"));
-			} else if (pendingProposals > 1) {
-				info(MessageFormat.format(getString("gb.nFederationProposalsToReview"),
-						pendingProposals));
-			}
-		}
-
-		super.setupPage(repositoryName, pageName);
-	}
-
-	protected Class<? extends BasePage> getRootNavPageClass() {
-		return getClass();
-	}
-
-	private PageParameters getRootPageParameters() {
-		if (reusePageParameters()) {
-			PageParameters pp = getPageParameters();
-			if (pp != null) {
-				PageParameters params = new PageParameters(pp);
-				// remove named project parameter
-				params.remove("p");
-
-				// remove named repository parameter
-				params.remove("r");
-
-				// remove named user parameter
-				params.remove("user");
-
-				// remove days back parameter if it is the default value
-				if (params.containsKey("db")
-						&& params.getInt("db") == app().settings().getInteger(Keys.web.activityDuration, 7)) {
-					params.remove("db");
-				}
-				return params;
-			}
-		}
-		return null;
-	}
-
-	protected boolean reusePageParameters() {
-		return false;
-	}
-
-	private void loginUser(UserModel user) {
-		if (user != null) {
-			// Set the user into the session
-			GitBlitWebSession session = GitBlitWebSession.get();
-			// issue 62: fix session fixation vulnerability
-			session.replaceSession();
-			session.setUser(user);
-
-			// Set Cookie
-			if (app().settings().getBoolean(Keys.web.allowCookieAuthentication, false)) {
-				WebResponse response = (WebResponse) getRequestCycle().getResponse();
-				app().authentication().setCookie(response.getHttpServletResponse(), user);
-			}
-
-			if (!session.continueRequest()) {
-				PageParameters params = getPageParameters();
-				if (params == null) {
-					// redirect to this page
-					setResponsePage(getClass());
-				} else {
-					// Strip username and password and redirect to this page
-					params.remove("username");
-					params.remove("password");
-					setResponsePage(getClass(), params);
-				}
-			}
-		}
-	}
-
-	protected List<RepositoryModel> getRepositoryModels() {
-		if (repositoryModels.isEmpty()) {
-			final UserModel user = GitBlitWebSession.get().getUser();
-			List<RepositoryModel> repositories = app().repositories().getRepositoryModels(user);
-			repositoryModels.addAll(repositories);
-			Collections.sort(repositoryModels);
-		}
-		return repositoryModels;
-	}
-
-	protected void addDropDownMenus(List<PageRegistration> pages) {
-
-	}
-
-	protected List<DropDownMenuItem> getRepositoryFilterItems(PageParameters params) {
-		final UserModel user = GitBlitWebSession.get().getUser();
-		Set<DropDownMenuItem> filters = new LinkedHashSet<DropDownMenuItem>();
-		List<RepositoryModel> repositories = getRepositoryModels();
-
-		// accessible repositories by federation set
-		Map<String, AtomicInteger> setMap = new HashMap<String, AtomicInteger>();
-		for (RepositoryModel repository : repositories) {
-			for (String set : repository.federationSets) {
-				String key = set.toLowerCase();
-				if (setMap.containsKey(key)) {
-					setMap.get(key).incrementAndGet();
-				} else {
-					setMap.put(key, new AtomicInteger(1));
-				}
-			}
-		}
-		if (setMap.size() > 0) {
-			List<String> sets = new ArrayList<String>(setMap.keySet());
-			Collections.sort(sets);
-			for (String set : sets) {
-				filters.add(new DropDownToggleItem(MessageFormat.format("{0} ({1})", set,
-						setMap.get(set).get()), "set", set, params));
-			}
-			// divider
-			filters.add(new DropDownMenuItem());
-		}
-
-		// user's team memberships
-		if (user != null && user.teams.size() > 0) {
-			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,
-						team.repositories.size()), "team", team.name, params));
-			}
-			// divider
-			filters.add(new DropDownMenuItem());
-		}
-
-		// custom filters
-		String customFilters = app().settings().getString(Keys.web.customFilters, null);
-		if (!StringUtils.isEmpty(customFilters)) {
-			boolean addedExpression = false;
-			List<String> expressions = StringUtils.getStringsFromValue(customFilters, "!!!");
-			for (String expression : expressions) {
-				if (!StringUtils.isEmpty(expression)) {
-					addedExpression = true;
-					filters.add(new DropDownToggleItem(null, "x", expression, params));
-				}
-			}
-			// if we added any custom expressions, add a divider
-			if (addedExpression) {
-				filters.add(new DropDownMenuItem());
-			}
-		}
-		return new ArrayList<DropDownMenuItem>(filters);
-	}
-
-	protected List<DropDownMenuItem> 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);
-		if (daysBack < 1) {
-			daysBack = 7;
-		}
-		if (daysBack > maxDaysBack) {
-			daysBack = maxDaysBack;
-		}
-		PageParameters clonedParams;
-		if (params == null) {
-			clonedParams = new PageParameters();
-		} else {
-			clonedParams = new PageParameters(params);
-		}
-
-		if (!clonedParams.containsKey("db")) {
-			clonedParams.put("db",  daysBack);
-		}
-
-		List<DropDownMenuItem> items = new ArrayList<DropDownMenuItem>();
-		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));
-		}
-		List<Integer> choices = new ArrayList<Integer>(choicesSet);
-		Collections.sort(choices);
-		String lastDaysPattern = getString("gb.lastNDays");
-		for (Integer db : choices) {
-			if (db == 1) {
-				items.add(new DropDownMenuItem(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 DropDownMenuItem());
-		return items;
-	}
-
-	protected List<RepositoryModel> getRepositories(PageParameters params) {
-		if (params == null) {
-			return getRepositoryModels();
-		}
-
-		boolean hasParameter = false;
-		String projectName = WicketUtils.getProjectName(params);
-		String userName = WicketUtils.getUsername(params);
-		if (StringUtils.isEmpty(projectName)) {
-			if (!StringUtils.isEmpty(userName)) {
-				projectName = ModelUtils.getPersonalPath(userName);
-			}
-		}
-		String repositoryName = WicketUtils.getRepositoryName(params);
-		String set = WicketUtils.getSet(params);
-		String regex = WicketUtils.getRegEx(params);
-		String team = WicketUtils.getTeam(params);
-		int daysBack = params.getInt("db", 0);
-		int maxDaysBack = app().settings().getInteger(Keys.web.activityDurationMaximum, 30);
-
-		List<RepositoryModel> availableModels = getRepositoryModels();
-		Set<RepositoryModel> models = new HashSet<RepositoryModel>();
-
-		if (!StringUtils.isEmpty(repositoryName)) {
-			// try named repository
-			hasParameter = true;
-			for (RepositoryModel model : availableModels) {
-				if (model.name.equalsIgnoreCase(repositoryName)) {
-					models.add(model);
-					break;
-				}
-			}
-		}
-
-		if (!StringUtils.isEmpty(projectName)) {
-			// try named project
-			hasParameter = true;
-			if (projectName.equalsIgnoreCase(app().settings().getString(Keys.web.repositoryRootGroupName, "main"))) {
-				// root project/group
-				for (RepositoryModel model : availableModels) {
-					if (model.name.indexOf('/') == -1) {
-						models.add(model);
-					}
-				}
-			} else {
-				// named project/group
-				String group = projectName.toLowerCase() + "/";
-				for (RepositoryModel model : availableModels) {
-					if (model.name.toLowerCase().startsWith(group)) {
-						models.add(model);
-					}
-				}
-			}
-		}
-
-		if (!StringUtils.isEmpty(regex)) {
-			// filter the repositories by the regex
-			hasParameter = true;
-			Pattern pattern = Pattern.compile(regex);
-			for (RepositoryModel model : availableModels) {
-				if (pattern.matcher(model.name).find()) {
-					models.add(model);
-				}
-			}
-		}
-
-		if (!StringUtils.isEmpty(set)) {
-			// filter the repositories by the specified sets
-			hasParameter = true;
-			List<String> sets = StringUtils.getStringsFromValue(set, ",");
-			for (RepositoryModel model : availableModels) {
-				for (String curr : sets) {
-					if (model.federationSets.contains(curr)) {
-						models.add(model);
-					}
-				}
-			}
-		}
-
-		if (!StringUtils.isEmpty(team)) {
-			// filter the repositories by the specified teams
-			hasParameter = true;
-			List<String> teams = StringUtils.getStringsFromValue(team, ",");
-
-			// need TeamModels first
-			List<TeamModel> teamModels = new ArrayList<TeamModel>();
-			for (String name : teams) {
-				TeamModel teamModel = app().users().getTeamModel(name);
-				if (teamModel != null) {
-					teamModels.add(teamModel);
-				}
-			}
-
-			// brute-force our way through finding the matching models
-			for (RepositoryModel repositoryModel : availableModels) {
-				for (TeamModel teamModel : teamModels) {
-					if (teamModel.hasRepositoryPermission(repositoryModel.name)) {
-						models.add(repositoryModel);
-					}
-				}
-			}
-		}
-
-		if (!hasParameter) {
-			models.addAll(availableModels);
-		}
-
-		// time-filter the list
-		if (daysBack > 0) {
-			if (maxDaysBack > 0 && daysBack > maxDaysBack) {
-				daysBack = maxDaysBack;
-			}
-			Calendar cal = Calendar.getInstance();
-			cal.set(Calendar.HOUR_OF_DAY, 0);
-			cal.set(Calendar.MINUTE, 0);
-			cal.set(Calendar.SECOND, 0);
-			cal.set(Calendar.MILLISECOND, 0);
-			cal.add(Calendar.DATE, -1 * daysBack);
-			Date threshold = cal.getTime();
-			Set<RepositoryModel> timeFiltered = new HashSet<RepositoryModel>();
-			for (RepositoryModel model : models) {
-				if (model.lastChange.after(threshold)) {
-					timeFiltered.add(model);
-				}
-			}
-			models = timeFiltered;
-		}
-
-		List<RepositoryModel> list = new ArrayList<RepositoryModel>(models);
-		Collections.sort(list);
-		return list;
-	}
-
-	/**
-	 * Inline login form.
-	 */
-	private class LoginForm extends Fragment {
-		private static final long serialVersionUID = 1L;
-
-		public LoginForm(String id, String markupId, MarkupContainer markupProvider) {
-			super(id, markupId, markupProvider);
-			setRenderBodyOnly(true);
-
-			SessionlessForm<Void> loginForm = new SessionlessForm<Void>("loginForm", RootPage.this.getClass(), getPageParameters()) {
-
-				private static final long serialVersionUID = 1L;
-
-				@Override
-				public void onSubmit() {
-					String username = RootPage.this.username.getObject();
-					char[] password = RootPage.this.password.getObject().toCharArray();
-
-					UserModel user = app().authentication().authenticate(username, password);
-					if (user == null) {
-						error(getString("gb.invalidUsernameOrPassword"));
-					} else if (user.username.equals(Constants.FEDERATION_USER)) {
-						// disallow the federation user from logging in via the
-						// web ui
-						error(getString("gb.invalidUsernameOrPassword"));
-						user = null;
-					} else {
-						loginUser(user);
-					}
-				}
-			};
-			TextField<String> unameField = new TextField<String>("username", username);
-			WicketUtils.setInputPlaceholder(unameField, markupProvider.getString("gb.username"));
-			loginForm.add(unameField);
-			PasswordTextField pwField = new PasswordTextField("password", password);
-			WicketUtils.setInputPlaceholder(pwField, markupProvider.getString("gb.password"));
-			loginForm.add(pwField);
-			add(loginForm);
-		}
-	}
-
-	/**
-	 * Menu for the authenticated user.
-	 */
-	class UserMenu extends Fragment {
-
-		private static final long serialVersionUID = 1L;
-
-		public UserMenu(String id, String markupId, MarkupContainer markupProvider) {
-			super(id, markupId, markupProvider);
-			setRenderBodyOnly(true);
-
-			GitBlitWebSession session = GitBlitWebSession.get();
-			UserModel user = session.getUser();
-			boolean editCredentials = app().authentication().supportsCredentialChanges(user);
-			boolean standardLogin = session.authenticationType.isStandard();
-
-			if (app().settings().getBoolean(Keys.web.allowGravatar, true)) {
-				add(new GravatarImage("username", user, "navbarGravatar", 20, false));
-			} else {
-				add(new Label("username", user.getDisplayName()));
-			}
-
-			add(new Label("displayName", user.getDisplayName()));
-
-			add(new BookmarkablePageLink<Void>("newRepository",
-					EditRepositoryPage.class).setVisible(user.canAdmin() || user.canCreate()));
-
-			add(new BookmarkablePageLink<Void>("myProfile",
-					UserPage.class, WicketUtils.newUsernameParameter(user.username)));
-
-			add(new BookmarkablePageLink<Void>("changePassword",
-					ChangePasswordPage.class).setVisible(editCredentials));
-
-			add(new BookmarkablePageLink<Void>("logout",
-					LogoutPage.class).setVisible(standardLogin));
-		}
-	}
-}
+/*
+ * 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 java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.HeaderContributor;
+import org.apache.wicket.markup.html.IHeaderContributor;
+import org.apache.wicket.markup.html.IHeaderResponse;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.PasswordTextField;
+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.SessionlessForm;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.GravatarImage;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.NavigationPanel;
+
+/**
+ * Root page is a topbar, navigable page like Repositories, Users, or
+ * Federation.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class RootPage extends BasePage {
+
+	boolean showAdmin;
+
+	IModel<String> username = new Model<String>("");
+	IModel<String> password = new Model<String>("");
+	List<RepositoryModel> repositoryModels = new ArrayList<RepositoryModel>();
+
+	public RootPage() {
+		super();
+	}
+
+	public RootPage(PageParameters params) {
+		super(params);
+	}
+
+	@Override
+	protected void setupPage(String repositoryName, String pageName) {
+
+		// CSS header overrides
+		add(new HeaderContributor(new IHeaderContributor() {
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public void renderHead(IHeaderResponse response) {
+				StringBuilder buffer = new StringBuilder();
+				buffer.append("<style type=\"text/css\">\n");
+				buffer.append(".navbar-inner {\n");
+				final String headerBackground = app().settings().getString(Keys.web.headerBackgroundColor, null);
+				if (!StringUtils.isEmpty(headerBackground)) {
+					buffer.append(MessageFormat.format("background-color: {0};\n", headerBackground));
+				}
+				final String headerBorder = app().settings().getString(Keys.web.headerBorderColor, null);
+				if (!StringUtils.isEmpty(headerBorder)) {
+					buffer.append(MessageFormat.format("border-bottom: 1px solid {0} !important;\n", headerBorder));
+				}
+				buffer.append("}\n");
+				final String headerBorderFocus = app().settings().getString(Keys.web.headerBorderFocusColor, null);
+				if (!StringUtils.isEmpty(headerBorderFocus)) {
+					buffer.append(".navbar ul li:focus, .navbar .active {\n");
+					buffer.append(MessageFormat.format("border-bottom: 4px solid {0};\n", headerBorderFocus));
+					buffer.append("}\n");
+				}
+				final String headerForeground = app().settings().getString(Keys.web.headerForegroundColor, null);
+				if (!StringUtils.isEmpty(headerForeground)) {
+					buffer.append(".navbar ul.nav li a {\n");
+					buffer.append(MessageFormat.format("color: {0};\n", headerForeground));
+					buffer.append("}\n");
+					buffer.append(".navbar ul.nav .active a {\n");
+					buffer.append(MessageFormat.format("color: {0};\n", headerForeground));
+					buffer.append("}\n");
+				}
+				final String headerHover = app().settings().getString(Keys.web.headerHoverColor, null);
+				if (!StringUtils.isEmpty(headerHover)) {
+					buffer.append(".navbar ul.nav li a:hover {\n");
+					buffer.append(MessageFormat.format("color: {0} !important;\n", headerHover));
+					buffer.append("}\n");
+				}
+				buffer.append("</style>\n");
+				response.renderString(buffer.toString());
+				}
+			}));
+
+		boolean authenticateView = app().settings().getBoolean(Keys.web.authenticateViewPages, false);
+		boolean authenticateAdmin = app().settings().getBoolean(Keys.web.authenticateAdminPages, true);
+		boolean allowAdmin = app().settings().getBoolean(Keys.web.allowAdministration, true);
+		boolean allowLucene = app().settings().getBoolean(Keys.web.allowLuceneIndexing, true);
+		boolean isLoggedIn = GitBlitWebSession.get().isLoggedIn();
+
+		if (authenticateAdmin) {
+			showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
+			// authentication requires state and session
+			setStatelessHint(false);
+		} else {
+			showAdmin = allowAdmin;
+			if (authenticateView) {
+				// authentication requires state and session
+				setStatelessHint(false);
+			} else {
+				// no authentication required, no state and no session required
+				setStatelessHint(true);
+			}
+		}
+
+		if (authenticateView || authenticateAdmin) {
+			if (isLoggedIn) {
+				UserMenu userFragment = new UserMenu("userPanel", "userMenuFragment", RootPage.this);
+				add(userFragment);
+			} else {
+				LoginForm loginForm = new LoginForm("userPanel", "loginFormFragment", RootPage.this);
+				add(loginForm);
+			}
+		} else {
+			add(new Label("userPanel").setVisible(false));
+		}
+
+		// navigation links
+		List<NavLink> navLinks = new ArrayList<NavLink>();
+		if (!authenticateView || (authenticateView && isLoggedIn)) {
+			navLinks.add(new PageNavLink(isLoggedIn ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
+					getRootPageParameters()));
+			if (isLoggedIn && app().tickets().isReady()) {
+				navLinks.add(new PageNavLink("gb.myTickets", MyTicketsPage.class));
+			}
+			navLinks.add(new PageNavLink("gb.repositories", RepositoriesPage.class,
+					getRootPageParameters()));
+			navLinks.add(new PageNavLink("gb.activity", ActivityPage.class, getRootPageParameters()));
+			if (allowLucene) {
+				navLinks.add(new PageNavLink("gb.search", LuceneSearchPage.class));
+			}
+
+			if (!authenticateView || (authenticateView && isLoggedIn)) {
+				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(), navLinks);
+		add(navPanel);
+
+		// display an error message cached from a redirect
+		String cachedMessage = GitBlitWebSession.get().clearErrorMessage();
+		if (!StringUtils.isEmpty(cachedMessage)) {
+			error(cachedMessage);
+		} else if (showAdmin) {
+			int pendingProposals = app().federation().getPendingFederationProposals().size();
+			if (pendingProposals == 1) {
+				info(getString("gb.OneProposalToReview"));
+			} else if (pendingProposals > 1) {
+				info(MessageFormat.format(getString("gb.nFederationProposalsToReview"),
+						pendingProposals));
+			}
+		}
+
+		super.setupPage(repositoryName, pageName);
+	}
+
+	protected Class<? extends BasePage> getRootNavPageClass() {
+		return getClass();
+	}
+
+	private PageParameters getRootPageParameters() {
+		if (reusePageParameters()) {
+			PageParameters pp = getPageParameters();
+			if (pp != null) {
+				PageParameters params = new PageParameters(pp);
+				// remove named project parameter
+				params.remove("p");
+
+				// remove named repository parameter
+				params.remove("r");
+
+				// remove named user parameter
+				params.remove("user");
+
+				// remove days back parameter if it is the default value
+				if (params.containsKey("db")
+						&& params.getInt("db") == app().settings().getInteger(Keys.web.activityDuration, 7)) {
+					params.remove("db");
+				}
+				return params;
+			}
+		}
+		return null;
+	}
+
+	protected boolean reusePageParameters() {
+		return false;
+	}
+
+	private void loginUser(UserModel user) {
+		if (user != null) {
+			// Set the user into the session
+			GitBlitWebSession session = GitBlitWebSession.get();
+			// issue 62: fix session fixation vulnerability
+			session.replaceSession();
+			session.setUser(user);
+
+			// Set Cookie
+			if (app().settings().getBoolean(Keys.web.allowCookieAuthentication, false)) {
+				WebResponse response = (WebResponse) getRequestCycle().getResponse();
+				app().authentication().setCookie(response.getHttpServletResponse(), user);
+			}
+
+			if (!session.continueRequest()) {
+				PageParameters params = getPageParameters();
+				if (params == null) {
+					// redirect to this page
+					setResponsePage(getClass());
+				} else {
+					// Strip username and password and redirect to this page
+					params.remove("username");
+					params.remove("password");
+					setResponsePage(getClass(), params);
+				}
+			}
+		}
+	}
+
+	protected List<RepositoryModel> getRepositoryModels() {
+		if (repositoryModels.isEmpty()) {
+			final UserModel user = GitBlitWebSession.get().getUser();
+			List<RepositoryModel> repositories = app().repositories().getRepositoryModels(user);
+			repositoryModels.addAll(repositories);
+			Collections.sort(repositoryModels);
+		}
+		return repositoryModels;
+	}
+
+	protected void addDropDownMenus(List<NavLink> navLinks) {
+
+	}
+
+	protected List<com.gitblit.models.Menu.MenuItem> getRepositoryFilterItems(PageParameters params) {
+		final UserModel user = GitBlitWebSession.get().getUser();
+		Set<MenuItem> filters = new LinkedHashSet<MenuItem>();
+		List<RepositoryModel> repositories = getRepositoryModels();
+
+		// accessible repositories by federation set
+		Map<String, AtomicInteger> setMap = new HashMap<String, AtomicInteger>();
+		for (RepositoryModel repository : repositories) {
+			for (String set : repository.federationSets) {
+				String key = set.toLowerCase();
+				if (setMap.containsKey(key)) {
+					setMap.get(key).incrementAndGet();
+				} else {
+					setMap.put(key, new AtomicInteger(1));
+				}
+			}
+		}
+		if (setMap.size() > 0) {
+			List<String> sets = new ArrayList<String>(setMap.keySet());
+			Collections.sort(sets);
+			for (String set : sets) {
+				filters.add(new ToggleMenuItem(MessageFormat.format("{0} ({1})", set,
+						setMap.get(set).get()), "set", set, params));
+			}
+			// divider
+			filters.add(new MenuDivider());
+		}
+
+		// user's team memberships
+		if (user != null && user.teams.size() > 0) {
+			List<TeamModel> teams = new ArrayList<TeamModel>(user.teams);
+			Collections.sort(teams);
+			for (TeamModel team : teams) {
+				filters.add(new ToggleMenuItem(MessageFormat.format("{0} ({1})", team.name,
+						team.repositories.size()), "team", team.name, params));
+			}
+			// divider
+			filters.add(new MenuDivider());
+		}
+
+		// custom filters
+		String customFilters = app().settings().getString(Keys.web.customFilters, null);
+		if (!StringUtils.isEmpty(customFilters)) {
+			boolean addedExpression = false;
+			List<String> expressions = StringUtils.getStringsFromValue(customFilters, "!!!");
+			for (String expression : expressions) {
+				if (!StringUtils.isEmpty(expression)) {
+					addedExpression = true;
+					filters.add(new ToggleMenuItem(null, "x", expression, params));
+				}
+			}
+			// if we added any custom expressions, add a divider
+			if (addedExpression) {
+				filters.add(new MenuDivider());
+			}
+		}
+		return new ArrayList<MenuItem>(filters);
+	}
+
+	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);
+		if (daysBack < 1) {
+			daysBack = 7;
+		}
+		if (daysBack > maxDaysBack) {
+			daysBack = maxDaysBack;
+		}
+		PageParameters clonedParams;
+		if (params == null) {
+			clonedParams = new PageParameters();
+		} else {
+			clonedParams = new PageParameters(params);
+		}
+
+		if (!clonedParams.containsKey("db")) {
+			clonedParams.put("db",  daysBack);
+		}
+
+		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));
+		}
+		List<Integer> choices = new ArrayList<Integer>(choicesSet);
+		Collections.sort(choices);
+		String lastDaysPattern = getString("gb.lastNDays");
+		for (Integer db : choices) {
+			if (db == 1) {
+				items.add(new ParameterMenuItem(getString("gb.time.today"), "db", db.toString(), clonedParams));
+			} else {
+				String txt = MessageFormat.format(lastDaysPattern, db);
+				items.add(new ParameterMenuItem(txt, "db", db.toString(), clonedParams));
+			}
+		}
+		items.add(new MenuDivider());
+		return items;
+	}
+
+	protected List<RepositoryModel> getRepositories(PageParameters params) {
+		if (params == null) {
+			return getRepositoryModels();
+		}
+
+		boolean hasParameter = false;
+		String projectName = WicketUtils.getProjectName(params);
+		String userName = WicketUtils.getUsername(params);
+		if (StringUtils.isEmpty(projectName)) {
+			if (!StringUtils.isEmpty(userName)) {
+				projectName = ModelUtils.getPersonalPath(userName);
+			}
+		}
+		String repositoryName = WicketUtils.getRepositoryName(params);
+		String set = WicketUtils.getSet(params);
+		String regex = WicketUtils.getRegEx(params);
+		String team = WicketUtils.getTeam(params);
+		int daysBack = params.getInt("db", 0);
+		int maxDaysBack = app().settings().getInteger(Keys.web.activityDurationMaximum, 30);
+
+		List<RepositoryModel> availableModels = getRepositoryModels();
+		Set<RepositoryModel> models = new HashSet<RepositoryModel>();
+
+		if (!StringUtils.isEmpty(repositoryName)) {
+			// try named repository
+			hasParameter = true;
+			for (RepositoryModel model : availableModels) {
+				if (model.name.equalsIgnoreCase(repositoryName)) {
+					models.add(model);
+					break;
+				}
+			}
+		}
+
+		if (!StringUtils.isEmpty(projectName)) {
+			// try named project
+			hasParameter = true;
+			if (projectName.equalsIgnoreCase(app().settings().getString(Keys.web.repositoryRootGroupName, "main"))) {
+				// root project/group
+				for (RepositoryModel model : availableModels) {
+					if (model.name.indexOf('/') == -1) {
+						models.add(model);
+					}
+				}
+			} else {
+				// named project/group
+				String group = projectName.toLowerCase() + "/";
+				for (RepositoryModel model : availableModels) {
+					if (model.name.toLowerCase().startsWith(group)) {
+						models.add(model);
+					}
+				}
+			}
+		}
+
+		if (!StringUtils.isEmpty(regex)) {
+			// filter the repositories by the regex
+			hasParameter = true;
+			Pattern pattern = Pattern.compile(regex);
+			for (RepositoryModel model : availableModels) {
+				if (pattern.matcher(model.name).find()) {
+					models.add(model);
+				}
+			}
+		}
+
+		if (!StringUtils.isEmpty(set)) {
+			// filter the repositories by the specified sets
+			hasParameter = true;
+			List<String> sets = StringUtils.getStringsFromValue(set, ",");
+			for (RepositoryModel model : availableModels) {
+				for (String curr : sets) {
+					if (model.federationSets.contains(curr)) {
+						models.add(model);
+					}
+				}
+			}
+		}
+
+		if (!StringUtils.isEmpty(team)) {
+			// filter the repositories by the specified teams
+			hasParameter = true;
+			List<String> teams = StringUtils.getStringsFromValue(team, ",");
+
+			// need TeamModels first
+			List<TeamModel> teamModels = new ArrayList<TeamModel>();
+			for (String name : teams) {
+				TeamModel teamModel = app().users().getTeamModel(name);
+				if (teamModel != null) {
+					teamModels.add(teamModel);
+				}
+			}
+
+			// brute-force our way through finding the matching models
+			for (RepositoryModel repositoryModel : availableModels) {
+				for (TeamModel teamModel : teamModels) {
+					if (teamModel.hasRepositoryPermission(repositoryModel.name)) {
+						models.add(repositoryModel);
+					}
+				}
+			}
+		}
+
+		if (!hasParameter) {
+			models.addAll(availableModels);
+		}
+
+		// time-filter the list
+		if (daysBack > 0) {
+			if (maxDaysBack > 0 && daysBack > maxDaysBack) {
+				daysBack = maxDaysBack;
+			}
+			Calendar cal = Calendar.getInstance();
+			cal.set(Calendar.HOUR_OF_DAY, 0);
+			cal.set(Calendar.MINUTE, 0);
+			cal.set(Calendar.SECOND, 0);
+			cal.set(Calendar.MILLISECOND, 0);
+			cal.add(Calendar.DATE, -1 * daysBack);
+			Date threshold = cal.getTime();
+			Set<RepositoryModel> timeFiltered = new HashSet<RepositoryModel>();
+			for (RepositoryModel model : models) {
+				if (model.lastChange.after(threshold)) {
+					timeFiltered.add(model);
+				}
+			}
+			models = timeFiltered;
+		}
+
+		List<RepositoryModel> list = new ArrayList<RepositoryModel>(models);
+		Collections.sort(list);
+		return list;
+	}
+
+	/**
+	 * Inline login form.
+	 */
+	private class LoginForm extends Fragment {
+		private static final long serialVersionUID = 1L;
+
+		public LoginForm(String id, String markupId, MarkupContainer markupProvider) {
+			super(id, markupId, markupProvider);
+			setRenderBodyOnly(true);
+
+			SessionlessForm<Void> loginForm = new SessionlessForm<Void>("loginForm", RootPage.this.getClass(), getPageParameters()) {
+
+				private static final long serialVersionUID = 1L;
+
+				@Override
+				public void onSubmit() {
+					String username = RootPage.this.username.getObject();
+					char[] password = RootPage.this.password.getObject().toCharArray();
+
+					UserModel user = app().authentication().authenticate(username, password);
+					if (user == null) {
+						error(getString("gb.invalidUsernameOrPassword"));
+					} else if (user.username.equals(Constants.FEDERATION_USER)) {
+						// disallow the federation user from logging in via the
+						// web ui
+						error(getString("gb.invalidUsernameOrPassword"));
+						user = null;
+					} else {
+						loginUser(user);
+					}
+				}
+			};
+			TextField<String> unameField = new TextField<String>("username", username);
+			WicketUtils.setInputPlaceholder(unameField, markupProvider.getString("gb.username"));
+			loginForm.add(unameField);
+			PasswordTextField pwField = new PasswordTextField("password", password);
+			WicketUtils.setInputPlaceholder(pwField, markupProvider.getString("gb.password"));
+			loginForm.add(pwField);
+			add(loginForm);
+		}
+	}
+
+	/**
+	 * Menu for the authenticated user.
+	 */
+	class UserMenu extends Fragment {
+
+		private static final long serialVersionUID = 1L;
+
+		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();
+			boolean editCredentials = app().authentication().supportsCredentialChanges(user);
+			boolean standardLogin = session.authenticationType.isStandard();
+
+			if (app().settings().getBoolean(Keys.web.allowGravatar, true)) {
+				add(new GravatarImage("username", user, "navbarGravatar", 20, false));
+			} else {
+				add(new Label("username", 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));
+
+			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));
+
+				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(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;
+		}
+	}
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/TeamsPage.html b/src/main/java/com/gitblit/wicket/pages/TeamsPage.html
new file mode 100644
index 0000000..981fe5b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TeamsPage.html
@@ -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>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/TeamsPage.java b/src/main/java/com/gitblit/wicket/pages/TeamsPage.java
new file mode 100644
index 0000000..e0e7bf4
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TeamsPage.java
@@ -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));
+	}
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/UserPage.java b/src/main/java/com/gitblit/wicket/pages/UserPage.java
index a5d38d1..6cb791e 100644
--- a/src/main/java/com/gitblit/wicket/pages/UserPage.java
+++ b/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);
 	}
 }
diff --git a/src/main/java/com/gitblit/wicket/pages/UsersPage.html b/src/main/java/com/gitblit/wicket/pages/UsersPage.html
index 6eec358..a9a3939 100644
--- a/src/main/java/com/gitblit/wicket/pages/UsersPage.html
+++ b/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>
diff --git a/src/main/java/com/gitblit/wicket/pages/UsersPage.java b/src/main/java/com/gitblit/wicket/pages/UsersPage.java
index 652bdba..eab0b18 100644
--- a/src/main/java/com/gitblit/wicket/pages/UsersPage.java
+++ b/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));
 	}
diff --git a/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java b/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
index d1a632e..4e7ae54 100644
--- a/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
+++ b/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()));
 				}
 			}
 		};
diff --git a/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java b/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
index 393dd13..2bc92f4 100644
--- a/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
+++ b/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);
 	}
 }
\ No newline at end of file
diff --git a/src/site/plugins_extensions.mkd b/src/site/plugins_extensions.mkd
index 60f8b47..7bf63c1 100644
--- a/src/site/plugins_extensions.mkd
+++ b/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);
+    }
+}
+```

--
Gitblit v1.9.1