]> source.dussan.org Git - gitblit.git/commitdiff
Allow client apps to specify a minimum required access permission
authorJames Moger <james.moger@gitblit.com>
Wed, 15 May 2013 19:55:19 +0000 (15:55 -0400)
committerJames Moger <james.moger@gitblit.com>
Wed, 15 May 2013 19:55:19 +0000 (15:55 -0400)
src/main/distrib/data/clientapps.json
src/main/java/com/gitblit/GitBlit.java
src/main/java/com/gitblit/SparkleShareInviteServlet.java
src/main/java/com/gitblit/models/GitClientApplication.java
src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java

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