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