diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index f9138a9e1e..98ac07ade6 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -741,6 +741,11 @@ how the replacement is displayed to the user. html = $1$2 ---- +Comment links can also be specified in `project.config` and sections in +children override those in parents. The only restriction is that to +avoid injecting arbitrary user-supplied HTML in the page, comment links +defined in `project.config` may only supply `link`, not `html`. + [[commentlink.name.match]]commentlink..match:: + A JavaScript regular expression to match positions to be replaced diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java index bd4fafb335..43413eacb7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java @@ -14,12 +14,14 @@ package com.google.gerrit.server.git; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.gerrit.common.data.Permission.isPermission; import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; @@ -66,6 +68,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; public class ProjectConfig extends VersionedMetaData { public static final String COMMENTLINK = "commentlink"; @@ -138,6 +141,7 @@ public class ProjectConfig extends VersionedMetaData { private Map contributorAgreements; private Map notifySections; private Map labelSections; + private List commentLinkSections; private List validationErrors; private ObjectId rulesId; @@ -155,8 +159,8 @@ public class ProjectConfig extends VersionedMetaData { return r; } - public static CommentLinkInfo buildCommentLink(Config cfg, String name) - throws IllegalArgumentException { + public static CommentLinkInfo buildCommentLink(Config cfg, String name, + boolean allowRaw) throws IllegalArgumentException { String match = cfg.getString(COMMENTLINK, name, KEY_MATCH); // Unfortunately this validation isn't entirely complete. Clients @@ -168,6 +172,8 @@ public class ProjectConfig extends VersionedMetaData { String link = cfg.getString(COMMENTLINK, name, KEY_LINK); String html = cfg.getString(COMMENTLINK, name, KEY_HTML); + checkArgument(allowRaw || Strings.isNullOrEmpty(html), + "Raw html replacement not allowed"); return new CommentLinkInfo(name, match, link, html); } @@ -256,6 +262,10 @@ public class ProjectConfig extends VersionedMetaData { return labelSections; } + public Collection getCommentLinkSections() { + return commentLinkSections; + } + public GroupReference resolve(AccountGroup group) { return resolve(GroupReference.forGroup(group)); } @@ -356,6 +366,7 @@ public class ProjectConfig extends VersionedMetaData { loadAccessSections(rc, groupsByName); loadNotifySections(rc, groupsByName); loadLabelSections(rc); + loadCommentLinkSections(rc); } private void loadAccountsSection( @@ -613,6 +624,25 @@ public class ProjectConfig extends VersionedMetaData { } } + private void loadCommentLinkSections(Config rc) { + Set subsections = rc.getSubsections(COMMENTLINK); + commentLinkSections = Lists.newArrayListWithCapacity(subsections.size()); + for (String name : subsections) { + try { + commentLinkSections.add(buildCommentLink(rc, name, false)); + } catch (PatternSyntaxException e) { + error(new ValidationError(PROJECT_CONFIG, String.format( + "Invalid pattern \"%s\" in commentlink.%s.match: %s", + rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage()))); + } catch (IllegalArgumentException e) { + error(new ValidationError(PROJECT_CONFIG, String.format( + "Error in pattern \"%s\" in commentlink.%s.match: %s", + rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage()))); + } + } + commentLinkSections = ImmutableList.copyOf(commentLinkSections); + } + private Map readGroupList() throws IOException { groupsByUUID = new HashMap(); Map groupsByName = diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java index 747ccfe420..3d54003ca3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java @@ -40,7 +40,7 @@ public class CommentLinkProvider implements Provider> { List cls = Lists.newArrayListWithCapacity(subsections.size()); for (String name : subsections) { - cls.add(ProjectConfig.buildCommentLink(cfg, name)); + cls.add(ProjectConfig.buildCommentLink(cfg, name, true)); } return ImmutableList.copyOf(cls); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java index ec00e3fd31..3d5e1b8db2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java @@ -16,6 +16,7 @@ package com.google.gerrit.server.project; import com.google.common.base.Function; import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -295,6 +296,16 @@ public class ProjectState { }; } + /** + * @return an iterable that walks in-order from All-Projects through the + * project hierarchy to this project. + */ + public Iterable treeInOrder() { + List projects = Lists.newArrayList(tree()); + Collections.reverse(projects); + return projects; + } + /** * @return an iterable that walks through the parents of this project. Starts * from the immediate parent of this project and progresses up the @@ -346,9 +357,7 @@ public class ProjectState { public LabelTypes getLabelTypes() { Map types = Maps.newLinkedHashMap(); - List projects = Lists.newArrayList(tree()); - Collections.reverse(projects); - for (ProjectState s : projects) { + for (ProjectState s : treeInOrder()) { for (LabelType type : s.getConfig().getLabelSections().values()) { String lower = type.getName().toLowerCase(); LabelType old = types.get(lower); @@ -367,7 +376,16 @@ public class ProjectState { } public List getCommentLinks() { - return commentLinks; + Map cls = Maps.newLinkedHashMap(); + for (CommentLinkInfo cl : commentLinks) { + cls.put(cl.name.toLowerCase(), cl); + } + for (ProjectState s : treeInOrder()) { + for (CommentLinkInfo cl : s.getConfig().getCommentLinkSections()) { + cls.put(cl.name.toLowerCase(), cl); + } + } + return ImmutableList.copyOf(cls.values()); } private boolean getInheritableBoolean(Function func) {