// Copyright (C) 2009 The Android Open Source Project // // 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.google.gerrit.server.config; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Strings.emptyToNull; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Strings.nullToEmpty; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.GitwebType; import com.google.gerrit.common.data.ParameterizedString; import com.google.gerrit.extensions.common.WebLinkInfo; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.extensions.webui.BranchWebLink; import com.google.gerrit.extensions.webui.FileHistoryWebLink; import com.google.gerrit.extensions.webui.FileWebLink; import com.google.gerrit.extensions.webui.ParentWebLink; import com.google.gerrit.extensions.webui.PatchSetWebLink; import com.google.gerrit.extensions.webui.ProjectWebLink; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class GitwebConfig { private static final Logger log = LoggerFactory.getLogger(GitwebConfig.class); public static boolean isDisabled(Config cfg) { return isEmptyString(cfg, "gitweb", null, "url") || isEmptyString(cfg, "gitweb", null, "cgi") || "disabled".equals(cfg.getString("gitweb", null, "type")); } public static class LegacyModule extends AbstractModule { private final Config cfg; public LegacyModule(Config cfg) { this.cfg = cfg; } @Override protected void configure() { GitwebType type = typeFromConfig(cfg); if (type != null) { bind(GitwebType.class).toInstance(type); if (!isNullOrEmpty(type.getBranch())) { DynamicSet.bind(binder(), BranchWebLink.class).to(GitwebLinks.class); } if (!isNullOrEmpty(type.getFile()) || !isNullOrEmpty(type.getRootTree())) { DynamicSet.bind(binder(), FileWebLink.class).to(GitwebLinks.class); } if (!isNullOrEmpty(type.getFileHistory())) { DynamicSet.bind(binder(), FileHistoryWebLink.class).to(GitwebLinks.class); } if (!isNullOrEmpty(type.getRevision())) { DynamicSet.bind(binder(), PatchSetWebLink.class).to(GitwebLinks.class); DynamicSet.bind(binder(), ParentWebLink.class).to(GitwebLinks.class); } if (!isNullOrEmpty(type.getProject())) { DynamicSet.bind(binder(), ProjectWebLink.class).to(GitwebLinks.class); } } } } private static boolean isEmptyString(Config cfg, String section, String subsection, String name) { // This is currently the only way to check for the empty string in a JGit // config. Fun! String[] values = cfg.getStringList(section, subsection, name); return values.length > 0 && isNullOrEmpty(values[0]); } private static GitwebType typeFromConfig(Config cfg) { GitwebType defaultType = defaultType(cfg.getString("gitweb", null, "type")); if (defaultType == null) { return null; } GitwebType type = new GitwebType(); type.setLinkName( firstNonNull(cfg.getString("gitweb", null, "linkname"), defaultType.getLinkName())); type.setBranch(firstNonNull(cfg.getString("gitweb", null, "branch"), defaultType.getBranch())); type.setProject( firstNonNull(cfg.getString("gitweb", null, "project"), defaultType.getProject())); type.setRevision( firstNonNull(cfg.getString("gitweb", null, "revision"), defaultType.getRevision())); type.setRootTree( firstNonNull(cfg.getString("gitweb", null, "roottree"), defaultType.getRootTree())); type.setFile(firstNonNull(cfg.getString("gitweb", null, "file"), defaultType.getFile())); type.setFileHistory( firstNonNull(cfg.getString("gitweb", null, "filehistory"), defaultType.getFileHistory())); type.setUrlEncode(cfg.getBoolean("gitweb", null, "urlencode", defaultType.getUrlEncode())); String pathSeparator = cfg.getString("gitweb", null, "pathSeparator"); if (pathSeparator != null) { if (pathSeparator.length() == 1) { char c = pathSeparator.charAt(0); if (isValidPathSeparator(c)) { type.setPathSeparator(firstNonNull(c, defaultType.getPathSeparator())); } else { log.warn("Invalid gitweb.pathSeparator: " + c); } } else { log.warn("gitweb.pathSeparator is not a single character: " + pathSeparator); } } return type; } private static GitwebType defaultType(String typeName) { GitwebType type = new GitwebType(); switch (nullToEmpty(typeName)) { case "gitweb": type.setLinkName("gitweb"); type.setProject("?p=${project}.git;a=summary"); type.setRevision("?p=${project}.git;a=commit;h=${commit}"); type.setBranch("?p=${project}.git;a=shortlog;h=${branch}"); type.setRootTree("?p=${project}.git;a=tree;hb=${commit}"); type.setFile("?p=${project}.git;hb=${commit};f=${file}"); type.setFileHistory("?p=${project}.git;a=history;hb=${branch};f=${file}"); break; case "cgit": type.setLinkName("cgit"); type.setProject("${project}.git/summary"); type.setRevision("${project}.git/commit/?id=${commit}"); type.setBranch("${project}.git/log/?h=${branch}"); type.setRootTree("${project}.git/tree/?h=${commit}"); type.setFile("${project}.git/tree/${file}?h=${commit}"); type.setFileHistory("${project}.git/log/${file}?h=${branch}"); break; case "custom": // For a custom type with no explicit link name, just reuse "gitweb". type.setLinkName("gitweb"); type.setProject(""); type.setRevision(""); type.setBranch(""); type.setRootTree(""); type.setFile(""); type.setFileHistory(""); break; case "": case "disabled": default: return null; } return type; } private final String url; private final GitwebType type; @Inject GitwebConfig(GitwebCgiConfig cgiConfig, @GerritServerConfig Config cfg) { if (isDisabled(cfg)) { type = null; url = null; } else { String cfgUrl = cfg.getString("gitweb", null, "url"); type = typeFromConfig(cfg); if (type == null) { url = null; } else if (cgiConfig.getGitwebCgi() == null) { // Use an externally managed gitweb instance, and not an internal one. url = cfgUrl; } else { url = firstNonNull(cfgUrl, "gitweb"); } } } /** @return GitwebType for gitweb viewer. */ @Nullable public GitwebType getGitwebType() { return type; } /** * @return URL of the entry point into gitweb. This URL may be relative to our context if gitweb * is hosted by ourselves; or absolute if its hosted elsewhere; or null if gitweb has not been * configured. */ public String getUrl() { return url; } /** * Determines if a given character can be used unencoded in an URL as a replacement for the path * separator '/'. * * <p>Reasoning: http://www.ietf.org/rfc/rfc1738.txt ยง 2.2: * * <p>... only alphanumerics, the special characters "$-_.+!*'(),", and reserved characters used * for their reserved purposes may be used unencoded within a URL. * * <p>The following characters might occur in file names, however: * * <p>alphanumeric characters, * * <p>"$-_.+!'," */ static boolean isValidPathSeparator(char c) { switch (c) { case '*': case '(': case ')': return true; default: return false; } } @Singleton static class GitwebLinks implements BranchWebLink, FileHistoryWebLink, FileWebLink, PatchSetWebLink, ParentWebLink, ProjectWebLink { private final String url; private final GitwebType type; private final ParameterizedString branch; private final ParameterizedString file; private final ParameterizedString fileHistory; private final ParameterizedString project; private final ParameterizedString revision; @Inject GitwebLinks(GitwebConfig config, GitwebType type) { this.url = config.getUrl(); this.type = type; this.branch = parse(type.getBranch()); this.file = parse(firstNonNull(emptyToNull(type.getFile()), nullToEmpty(type.getRootTree()))); this.fileHistory = parse(type.getFileHistory()); this.project = parse(type.getProject()); this.revision = parse(type.getRevision()); } @Override public WebLinkInfo getBranchWebLink(String projectName, String branchName) { if (branch != null) { return link( branch .replace("project", encode(projectName)) .replace("branch", encode(branchName)) .toString()); } return null; } @Override public WebLinkInfo getFileHistoryWebLink(String projectName, String revision, String fileName) { if (fileHistory != null) { return link( fileHistory .replace("project", encode(projectName)) .replace("branch", encode(revision)) .replace("file", encode(fileName)) .toString()); } return null; } @Override public WebLinkInfo getFileWebLink(String projectName, String revision, String fileName) { if (file != null) { return link( file.replace("project", encode(projectName)) .replace("commit", encode(revision)) .replace("file", encode(fileName)) .toString()); } return null; } @Override public WebLinkInfo getPatchSetWebLink(String projectName, String commit) { if (revision != null) { return link( revision .replace("project", encode(projectName)) .replace("commit", encode(commit)) .toString()); } return null; } @Override public WebLinkInfo getParentWebLink(String projectName, String commit) { // For Gitweb treat parent revision links the same as patch set links return getPatchSetWebLink(projectName, commit); } @Override public WebLinkInfo getProjectWeblink(String projectName) { if (project != null) { return link(project.replace("project", encode(projectName)).toString()); } return null; } private String encode(String val) { if (type.getUrlEncode()) { return Url.encode(type.replacePathSeparator(val)); } return val; } private WebLinkInfo link(String rest) { return new WebLinkInfo(type.getLinkName(), null, url + rest, null); } private static ParameterizedString parse(String pattern) { if (!isNullOrEmpty(pattern)) { return new ParameterizedString(pattern); } return null; } } }