From 366bec6ae90ef4adadb5df0e2e9232ba7b954f8e Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 15 May 2013 15:55:19 -0400
Subject: [PATCH] Allow client apps to specify a minimum required access permission

---
 src/main/java/com/gitblit/SparkleShareInviteServlet.java        |  151 ++++--------------------
 src/main/distrib/data/clientapps.json                           |   24 +++-
 src/main/java/com/gitblit/models/GitClientApplication.java      |    2 
 src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java |  109 ++++++++---------
 src/main/java/com/gitblit/GitBlit.java                          |    3 
 src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html |   52 ++++----
 6 files changed, 128 insertions(+), 213 deletions(-)

diff --git a/src/main/distrib/data/clientapps.json b/src/main/distrib/data/clientapps.json
index 0c83d10..12d14b1 100644
--- a/src/main/distrib/data/clientapps.json
+++ b/src/main/distrib/data/clientapps.json
@@ -4,7 +4,7 @@
 		"title": "Git",
 		"description": "a fast, open-source, distributed VCS",
 		"legal": "released under the GPLv2 open source license",
-		"command": "git clone {0}",
+		"command": "git clone ${repoUrl}",
 		"productUrl": "http://git-scm.com",
 		"icon": "git-black_32x32.png",
 		"isActive": true
@@ -14,7 +14,7 @@
 		"title": "syntevo SmartGit/Hg\u2122",
 		"description": "a Git client for Windows, Mac, & Linux",
 		"legal": "\u00a9 2013 syntevo GmbH. All rights reserved.",
-		"cloneUrl": "smartgit://cloneRepo/{0}",
+		"cloneUrl": "smartgit://cloneRepo/${repoUrl}",
 		"productUrl": "http://www.syntevo.com/smartgithg",
 		"platforms": [ "windows", "macintosh", "linux" ],
 		"icon": "smartgithg_32x32.png",
@@ -25,7 +25,7 @@
 		"title": "Atlassian SourceTree\u2122",
 		"description": "a free Git client for Windows or Mac",
 		"legal": "\u00a9 2013 Atlassian. All rights reserved.",
-		"cloneUrl": "sourcetree://cloneRepo/{0}",
+		"cloneUrl": "sourcetree://cloneRepo/${repoUrl}",
 		"productUrl": "http://sourcetreeapp.com",
 		"platforms": [ "windows", "macintosh" ],
 		"icon": "sourcetree_32x32.png",
@@ -36,7 +36,7 @@
 		"title": "fournova Tower\u2122",
 		"description": "a Git client for Mac",
 		"legal": "\u00a9 2013 fournova Software GmbH. All rights reserved.",
-		"cloneUrl": "gittower://openRepo/{0}",
+		"cloneUrl": "gittower://openRepo/${repoUrl}",
 		"productUrl": "http://www.git-tower.com",
 		"platforms": [ "macintosh" ],
 		"icon": "tower_32x32.png",
@@ -47,7 +47,7 @@
 		"title": "GitHub\u2122 for Macintosh",
 		"description": "a free Git client for Mac OS X",
 		"legal": "\u00a9 2013 GitHub. All rights reserved.",
-		"cloneUrl": "github-mac://openRepo/{0}",
+		"cloneUrl": "github-mac://openRepo/${repoUrl}",
 		"productUrl": "http://mac.github.com",
 		"platforms": [ "macintosh" ],
 		"isActive": false
@@ -57,9 +57,21 @@
 		"title": "GitHub\u2122 for Windows",
 		"description": "a free Git client for Windows",
 		"legal": "\u00a9 2013 GitHub. All rights reserved.",
-		"cloneUrl": "github-windows://openRepo/{0}",
+		"cloneUrl": "github-windows://openRepo/${repoUrl}",
 		"productUrl": "http://windows.github.com",
 		"platforms": [ "windows" ],
 		"isActive": false
+	},
+	{
+		"name": "SparkleShare",
+		"title": "SparkleShare\u2122",
+		"description": "an open source collaboration and sharing tool",
+		"legal": "released under the GPLv3 open source license",
+		"cloneUrl": "sparkleshare://inviteRepo/${baseUrl}/sparkleshare/${repoUrl}.xml",
+		"productUrl": "http://sparkleshare.org",
+		"platforms": [ "windows", "macintosh", "linux" ],
+		"icon": "sparkleshare_32x32.png",
+		"minimumPermission" : "RW+",
+		"isActive": false
 	}
 ]
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index f017d21..2d3b7fd 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -128,7 +128,6 @@
 import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.WicketUtils;
 import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
 import com.google.gson.JsonIOException;
 import com.google.gson.JsonSyntaxException;
 import com.google.gson.reflect.TypeToken;
@@ -615,7 +614,7 @@
 			Type type = new TypeToken<Collection<GitClientApplication>>() {
 			}.getType();
 			InputStreamReader reader = new InputStreamReader(is);
-			Gson gson = new GsonBuilder().create();
+			Gson gson = JsonUtils.gson();
 			Collection<GitClientApplication> links = gson.fromJson(reader, type);
 			return links;
 		} catch (JsonIOException e) {
diff --git a/src/main/java/com/gitblit/SparkleShareInviteServlet.java b/src/main/java/com/gitblit/SparkleShareInviteServlet.java
index 3cabb41..14d281a 100644
--- a/src/main/java/com/gitblit/SparkleShareInviteServlet.java
+++ b/src/main/java/com/gitblit/SparkleShareInviteServlet.java
@@ -17,14 +17,12 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
-import java.util.List;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.StringUtils;
@@ -41,27 +39,6 @@
 
 	public SparkleShareInviteServlet() {
 		super();
-	}
-	
-	/**
-	 * Returns an Sparkleshare invite url to this servlet for the repository.
-	 * https://github.com/hbons/SparkleShare/wiki/Invites
-	 * 
-	 * @param baseURL
-	 * @param repository
-	 * @param username
-	 * @return an url
-	 */
-	public static String asLink(String baseURL, String repository, String username) {
-		if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
-			baseURL = baseURL.substring(0, baseURL.length() - 1);
-		}
-		String url = baseURL + Constants.SPARKLESHARE_INVITE_PATH
-				+ ((StringUtils.isEmpty(username) ? "" : (username + "@")))
-				+ repository + ".xml";
-		url = url.replace("https://", "sparkleshare://");
-		url = url.replace("http://", "sparkleshare-unsafe://");
-		return url;
 	}
 	
 	@Override
@@ -81,22 +58,22 @@
 			java.io.IOException {		
 		
 		// extract repo name from request
-		String path = request.getPathInfo();
-		if (path != null && path.length() > 1) {
-			if (path.charAt(0) == '/') {
-				path = path.substring(1);
-			}
-		}
+		String repoUrl = request.getPathInfo().substring(1);
+
 		// trim trailing .xml
-		if (path.endsWith(".xml")) {
-			path = path.substring(0, path.length() - 4);
+		if (repoUrl.endsWith(".xml")) {
+			repoUrl = repoUrl.substring(0, repoUrl.length() - 4);
 		}
 		
+		String servletPath =  Constants.GIT_PATH;
+		
+		int schemeIndex = repoUrl.indexOf("://") + 3;
+		String host = repoUrl.substring(0, repoUrl.indexOf('/', schemeIndex));				
+		String path = repoUrl.substring(repoUrl.indexOf(servletPath) + servletPath.length());
 		String username = null;
-		int fetch = path.indexOf('@');
-		if (fetch > -1) {
-			username = path.substring(0, fetch);
-			path = path.substring(fetch + 1);
+		int fetchIndex = repoUrl.indexOf('@');
+		if (fetchIndex > -1) {
+			username = repoUrl.substring(schemeIndex, fetchIndex);
 		}
 		UserModel user;
 		if (StringUtils.isEmpty(username)) {
@@ -109,102 +86,28 @@
 			username = "";
 		}
 		
-		// ensure that the requested repository exists and is sparkleshared
+		// ensure that the requested repository exists
 		RepositoryModel model = GitBlit.self().getRepositoryModel(path);
 		if (model == null) {
 			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
 			response.getWriter().append(MessageFormat.format("Repository \"{0}\" not found!", path));
 			return;
-		} else if (!model.isSparkleshared()) {
-			response.setStatus(HttpServletResponse.SC_FORBIDDEN);
-			response.getWriter().append(MessageFormat.format("Repository \"{0}\" is not sparkleshared!", path));
-			return;
 		}
 		
-		if (GitBlit.getBoolean(Keys.git.enableGitServlet, true)
-				|| GitBlit.getInteger(Keys.git.daemonPort, 0) > 0) {
-			// Gitblit as server
-			// determine username for repository url
-			if (model.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
-				if (!user.canRewindRef(model)) {
-					response.setStatus(HttpServletResponse.SC_FORBIDDEN);
-					response.getWriter().append(MessageFormat.format("\"{0}\" does not have RW+ permissions for {1}!", user.username, path));
-					return;
-				}
-			}
-			
-			if (model.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
-				username = user.username + "@";
-			} else {
-				username = "";
-			}
-
-			String serverPort = "";
-			if (request.getScheme().equals("https")) {
-				if (request.getServerPort() != 443) {
-					serverPort = ":" + request.getServerPort();
-				}
-			} else if (request.getScheme().equals("http")) {
-				if (request.getServerPort() != 80) {
-					serverPort = ":" + request.getServerPort();
-				}
-			}
-
-			// assume http/https serving
-			String scheme = request.getScheme();
-			String servletPath = Constants.GIT_PATH;
-
-			// try to switch to git://, if git servlet disabled and repo has no restrictions
-			if (!GitBlit.getBoolean(Keys.git.enableGitServlet, true)
-					&& (GitBlit.getInteger(Keys.git.daemonPort, 0) > 0)
-					&& AccessRestrictionType.NONE == model.accessRestriction) {
-				scheme = "git";
-				servletPath = "/";
-				serverPort = GitBlit.getString(Keys.git.daemonPort, "");
-			}
-
-			// construct Sparkleshare invite
-			StringBuilder sb = new StringBuilder();		
-			sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
-			sb.append("<sparkleshare><invite>\n");
-			sb.append(MessageFormat.format("<address>{0}://{1}{2}{3}{4}</address>\n", scheme, username, request.getServerName(), serverPort, request.getContextPath()));
-			sb.append(MessageFormat.format("<remote_path>{0}{1}</remote_path>\n", servletPath, model.name));
-			if (GitBlit.getInteger(Keys.fanout.port, 0) > 0) {
-				// Gitblit is running it's own fanout service for pubsub notifications
-				sb.append(MessageFormat.format("<announcements_url>tcp://{0}:{1}</announcements_url>\n", request.getServerName(), GitBlit.getString(Keys.fanout.port, "")));
-			}
-			sb.append("</invite></sparkleshare>\n");
-
-			// write invite to client
-			response.setContentType("application/xml");
-			response.setContentLength(sb.length());
-			response.getWriter().append(sb.toString());
-		} else {
-			// Gitblit as viewer, repository access handled externally so
-			// assume RW+ permission
-			List<String> others = GitBlit.getStrings(Keys.web.otherUrls);
-			if (others.size() == 0) {
-				return;
-			}
-			
-			String address = MessageFormat.format(others.get(0), "", username);
-			
-			StringBuilder sb = new StringBuilder();		
-			sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
-			sb.append("<sparkleshare><invite>\n");
-			
-			sb.append(MessageFormat.format("<address>{0}</address>\n", address));
-			sb.append(MessageFormat.format("<remote_path>{0}</remote_path>\n", model.name));
-			if (GitBlit.getInteger(Keys.fanout.port, 0) > 0) {
-				// Gitblit is running it's own fanout service for pubsub notifications
-				sb.append(MessageFormat.format("<announcements_url>tcp://{0}:{1}</announcements_url>\n", request.getServerName(), GitBlit.getString(Keys.fanout.port, "")));
-			}
-			sb.append("</invite></sparkleshare>\n");
-
-			// write invite to client
-			response.setContentType("application/xml");
-			response.setContentLength(sb.length());
-			response.getWriter().append(sb.toString());
+		StringBuilder sb = new StringBuilder();		
+		sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+		sb.append("<sparkleshare><invite>\n");
+		sb.append(MessageFormat.format("<address>{0}</address>\n", host));
+		sb.append(MessageFormat.format("<remote_path>{0}{1}</remote_path>\n", servletPath, model.name));
+		if (GitBlit.getInteger(Keys.fanout.port, 0) > 0) {
+			// Gitblit is running it's own fanout service for pubsub notifications
+			sb.append(MessageFormat.format("<announcements_url>tcp://{0}:{1}</announcements_url>\n", request.getServerName(), GitBlit.getString(Keys.fanout.port, "")));
 		}
+		sb.append("</invite></sparkleshare>\n");
+
+		// write invite to client
+		response.setContentType("application/xml");
+		response.setContentLength(sb.length());
+		response.getWriter().append(sb.toString());
 	}
 }
diff --git a/src/main/java/com/gitblit/models/GitClientApplication.java b/src/main/java/com/gitblit/models/GitClientApplication.java
index fd53059..8225da4 100644
--- a/src/main/java/com/gitblit/models/GitClientApplication.java
+++ b/src/main/java/com/gitblit/models/GitClientApplication.java
@@ -17,6 +17,7 @@
 
 import java.io.Serializable;
 
+import com.gitblit.Constants.AccessPermission;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.StringUtils;
 
@@ -39,6 +40,7 @@
 	public String command;
 	public String productUrl;
 	public String[] platforms;
+	public AccessPermission minimumPermission;
 	public boolean isActive;
 
 	public boolean allowsPlatform(String p) {
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
index 2663f88..c3b13fa 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
@@ -15,10 +15,10 @@
 			<div class="btn-group repositoryUrlContainer">
 				<img style="vertical-align: middle;padding: 0px 0px 1px 3px;" wicket:id="accessRestrictionIcon"></img>
 				<span wicket:id="menu"></span>
-   				<span class="repositoryUrl">
+   				<div class="repositoryUrl">
    					<span wicket:id="primaryUrl">[repository primary url]</span>
    					<span class="hidden-phone hidden-tablet" wicket:id="copyFunction"></span>
-   				</span>
+   				</div>
    				<span class="hidden-phone hidden-tablet repositoryUrlRightCap" wicket:id="primaryUrlPermission">[repository primary url permission]</span>
    			</div>
 		</div>
@@ -27,32 +27,36 @@
 	<wicket:fragment wicket:id="applicationMenusFragment">
 		<div class="btn-toolbar" style="margin: 4px 0px 0px 0px;">
 			<div class="btn-group" wicket:id="appMenus">
-   				<a class="btn btn-mini btn-appmenu" data-toggle="dropdown" href="#">   				
-	    			<span wicket:id="applicationName"></span>
-    				<span class="caret"></span>
-   				</a>
-   				<ul class="dropdown-menu applicationMenu">
-   					<li>
-   						<div class="applicationHeaderMenuItem">
-   							<div style="float:right">
-   								<img style="padding-right: 5px;vertical-align: middle;" wicket:id="applicationIcon"></img>
-   							</div>
-   							<span class="applicationTitle" wicket:id="applicationTitle"></span>
-   						</div>
-   					</li>
-	   				<li><div class="applicationHeaderMenuItem"><span wicket:id="applicationDescription"></span></div></li>
-   					<li><div class="applicationLegalMenuItem"><span wicket:id="applicationLegal"></span></div></li>
-   					
-   					<li class="divider" style="margin: 5px 1px 0px 1px;clear:both;" ></li>
-   				
-   					<li class="action" wicket:id="actionItems">
-   						<span wicket:id="actionItem"></span>
-   					</li>
-   				</ul>
+				<span wicket:id="appMenu"></span>
    			</div>
 		</div>
 	</wicket:fragment>
 	
+	<wicket:fragment wicket:id="appMenuFragment">
+		<a class="btn btn-mini btn-appmenu" data-toggle="dropdown" href="#">   				
+	    	<span wicket:id="applicationName"></span>
+    		<span class="caret"></span>
+   		</a>
+   		<ul class="dropdown-menu applicationMenu">
+   			<li>
+   				<div class="applicationHeaderMenuItem">
+   					<div style="float:right">
+   						<img style="padding-right: 5px;vertical-align: middle;" wicket:id="applicationIcon"></img>
+   					</div>
+   					<span class="applicationTitle" wicket:id="applicationTitle"></span>
+   				</div>
+   			</li>
+	   		<li><div class="applicationHeaderMenuItem"><span wicket:id="applicationDescription"></span></div></li>
+   			<li><div class="applicationLegalMenuItem"><span wicket:id="applicationLegal"></span></div></li>
+   			
+   			<li class="divider" style="margin: 5px 1px 0px 1px;clear:both;" ></li>
+   		
+   			<li class="action" wicket:id="actionItems">
+   				<span wicket:id="actionItem"></span>
+   			</li>
+   		</ul>
+	</wicket:fragment>
+	
 	<wicket:fragment wicket:id="urlProtocolMenuFragment">
 		<a class="" data-toggle="dropdown" href="#">   				
     		<span class="repositoryUrlLeftCap" wicket:id="menuText">URLs</span>
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
index 942f8d5..7f43d63 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
@@ -38,12 +38,12 @@
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
-import com.gitblit.SparkleShareInviteServlet;
 import com.gitblit.models.GitClientApplication;
 import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.RepositoryUrl;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.ExternalImage;
 import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.WicketUtils;
 
@@ -200,7 +200,7 @@
 		return urlPanel;
 	}
 	
-	protected Fragment createApplicationMenus(String wicketId, UserModel user, final RepositoryModel repository, List<RepositoryUrl> repositoryUrls) {
+	protected Fragment createApplicationMenus(String wicketId, UserModel user, final RepositoryModel repository, final List<RepositoryUrl> repositoryUrls) {
 		final List<GitClientApplication> displayedApps = new ArrayList<GitClientApplication>();
 		final String userAgent = ((WebClientInfo) GitBlitWebSession.get().getClientInfo()).getUserAgent();
 		
@@ -210,14 +210,9 @@
 					displayedApps.add(app);
 				}
 			}
-
-			GitClientApplication sparkleshare = getSparkleShareAppMenu(user, repository);
-			if (sparkleshare != null) {
-				displayedApps.add(sparkleshare);
-			}
 		}
 
-		final ListDataProvider<RepositoryUrl> urlsDp = new ListDataProvider<RepositoryUrl>(repositoryUrls);
+		final String baseURL = WicketUtils.getGitblitURL(RequestCycle.get().getRequest());
 		ListDataProvider<GitClientApplication> displayedAppsDp = new ListDataProvider<GitClientApplication>(displayedApps);
 		DataView<GitClientApplication> appMenus = new DataView<GitClientApplication>("appMenus", displayedAppsDp) {
 			private static final long serialVersionUID = 1L;
@@ -225,58 +220,92 @@
 			public void populateItem(final Item<GitClientApplication> item) {
 				final GitClientApplication clientApp = item.getModelObject();
 
+				// filter the urls for the client app
+				List<RepositoryUrl> urls;
+				if (clientApp.minimumPermission == null) {
+					// client app does not specify minimum access permission
+					urls = repositoryUrls;
+				} else {
+					urls = new ArrayList<RepositoryUrl>();
+					for (RepositoryUrl repoUrl : repositoryUrls) {
+						if (repoUrl.permission == null) {
+							// external permissions, assume it is satisfactory
+							urls.add(repoUrl);
+						} else if (repoUrl.permission.atLeast(clientApp.minimumPermission)) {
+							// repo url meets minimum permission requirement
+							urls.add(repoUrl);
+						}
+					}
+				}
+				
+				if (urls.size() == 0) {
+					// do not show this app menu because there are no urls
+					item.add(new Label("appMenu").setVisible(false));
+					return;
+				}
+				
+				Fragment appMenu = new Fragment("appMenu", "appMenuFragment", this);
+				appMenu.setRenderBodyOnly(true);
+				item.add(appMenu);
+				
 				// menu button
-				item.add(new Label("applicationName", clientApp.name));
+				appMenu.add(new Label("applicationName", clientApp.name));
 				
 				// application icon
 				Component img;
 				if (StringUtils.isEmpty(clientApp.icon)) {
 					img = WicketUtils.newClearPixel("applicationIcon").setVisible(false);	
 				} else {
-					img = WicketUtils.newImage("applicationIcon", clientApp.icon);	
+					if (clientApp.icon.contains("://")) {
+						// external image
+						img = new ExternalImage("applicationIcon", clientApp.icon);
+					} else {
+						// context image
+						img = WicketUtils.newImage("applicationIcon", clientApp.icon);
+					}
 				}				
-				item.add(img);
+				appMenu.add(img);
 				
 				// application menu title, may be a link
 				if (StringUtils.isEmpty(clientApp.productUrl)) {
-					item.add(new Label("applicationTitle", clientApp.toString()));
+					appMenu.add(new Label("applicationTitle", clientApp.toString()));
 				} else {
-					item.add(new LinkPanel("applicationTitle", null, clientApp.toString(), clientApp.productUrl, true));
+					appMenu.add(new LinkPanel("applicationTitle", null, clientApp.toString(), clientApp.productUrl, true));
 				}
 				
 				// brief application description
 				if (StringUtils.isEmpty(clientApp.description)) {
-					item.add(new Label("applicationDescription").setVisible(false));
+					appMenu.add(new Label("applicationDescription").setVisible(false));
 				} else {
-					item.add(new Label("applicationDescription", clientApp.description));
+					appMenu.add(new Label("applicationDescription", clientApp.description));
 				}
 				
 				// brief application legal info, copyright, license, etc
 				if (StringUtils.isEmpty(clientApp.legal)) {
-					item.add(new Label("applicationLegal").setVisible(false));
+					appMenu.add(new Label("applicationLegal").setVisible(false));
 				} else {
-					item.add(new Label("applicationLegal", clientApp.legal));
+					appMenu.add(new Label("applicationLegal", clientApp.legal));
 				}
 				
 				// a nested repeater for all action items
+				ListDataProvider<RepositoryUrl> urlsDp = new ListDataProvider<RepositoryUrl>(urls);
 				DataView<RepositoryUrl> actionItems = new DataView<RepositoryUrl>("actionItems", urlsDp) {
 					private static final long serialVersionUID = 1L;
 
 					public void populateItem(final Item<RepositoryUrl> repoLinkItem) {
 						RepositoryUrl repoUrl = repoLinkItem.getModelObject();
-						
 						Fragment fragment = new Fragment("actionItem", "actionFragment", this);
 						fragment.add(createPermissionBadge("permission", repoUrl));
 
 						if (!StringUtils.isEmpty(clientApp.cloneUrl)) {
 							// custom registered url
-							String url = MessageFormat.format(clientApp.cloneUrl, repoUrl);
+							String url = substitute(clientApp.cloneUrl, repoUrl.url, baseURL);
 							fragment.add(new LinkPanel("content", "applicationMenuItem", getString("gb.clone") + " " + repoUrl.url, url));
 							repoLinkItem.add(fragment);
 							fragment.add(new Label("copyFunction").setVisible(false));
 						} else if (!StringUtils.isEmpty(clientApp.command)) {
 							// command-line
-							String command = MessageFormat.format(clientApp.command, repoUrl);
+							String command = substitute(clientApp.command, repoUrl.url, baseURL);
 							Label content = new Label("content", command);
 							WicketUtils.setCssClass(content, "commandMenuItem");
 							fragment.add(content);
@@ -286,7 +315,7 @@
 							fragment.add(createCopyFragment(command));
 						}
 					}};
-					item.add(actionItems);
+					appMenu.add(actionItems);
 			}
 		};
 		
@@ -295,42 +324,8 @@
 		return applicationMenus;
 	}
 	
-	protected GitClientApplication getSparkleShareAppMenu(UserModel user, RepositoryModel repository) {
-		String url = null;
-		if (repository.isBare && repository.isSparkleshared()) {
-			String username = null;
-			if (UserModel.ANONYMOUS != user) {
-				username = user.username;
-			}
-			if (isGitblitServingRepositories()) {
-				// Gitblit as server
-				// ensure user can rewind
-				if (user.canRewindRef(repository)) {
-					String baseURL = WicketUtils.getGitblitURL(RequestCycle.get().getRequest());
-					url = SparkleShareInviteServlet.asLink(baseURL, repository.name, username);
-				}
-			} else {
-				// Gitblit as viewer, assume RW+ permission
-				String baseURL = WicketUtils.getGitblitURL(RequestCycle.get().getRequest());
-				url = SparkleShareInviteServlet.asLink(baseURL, repository.name, username);
-			}
-		}
-
-		// sparkleshare invite url
-		if (!StringUtils.isEmpty(url)) {
-			GitClientApplication app = new GitClientApplication();
-			app.name = "SparkleShare";
-			app.title = "SparkleShare\u2122";
-			app.description = "an open source collaboration and sharing tool";
-			app.legal = "released under the GPLv3 open source license";
-			app.cloneUrl = url;
-			app.platforms = new String [] { "windows", "macintosh", "linux" };
-			app.productUrl = "http://sparkleshare.org";
-			app.icon = "sparkleshare_32x32.png";
-			app.isActive = true;
-			return app;
-		}
-		return null;
+	protected String substitute(String pattern, String repoUrl, String baseUrl) {
+		return pattern.replace("${repoUrl}", repoUrl).replace("${baseUrl}", baseUrl);
 	}
 	
 	protected boolean isGitblitServingRepositories() {

--
Gitblit v1.9.1