Allow projects to specify themes

Gerrit administrators can put per-project themes in
$site_path/themes/{project-name}; the same header/footer/CSS filenames
are supported as for sitewide themes. These are inherited and cached
per-project and exposed via GET /projects/X/config.

Themes must be provided by a site admin rather than arbitrary project
admins, so the site admins can be responsible for making sure themes
do not introduce XSS vulnerabilities.

Change-Id: I065d9e6d4df9275b963bb142ec11f66b5604678b
This commit is contained in:
Dave Borowitz
2013-04-09 12:14:57 -07:00
parent 9779c416d5
commit 1e49e143ec
11 changed files with 130 additions and 15 deletions

View File

@@ -24,6 +24,10 @@ import java.io.IOException;
/** Important paths within a {@link SitePath}. */
@Singleton
public final class SitePaths {
public static final String CSS_FILENAME = "GerritSite.css";
public static final String HEADER_FILENAME = "GerritSiteHeader.html";
public static final String FOOTER_FILENAME = "GerritSiteFooter.html";
public final File site_path;
public final File bin_dir;
public final File etc_dir;
@@ -35,6 +39,7 @@ public final class SitePaths {
public final File mail_dir;
public final File hooks_dir;
public final File static_dir;
public final File themes_dir;
public final File gerrit_sh;
public final File gerrit_war;
@@ -71,6 +76,7 @@ public final class SitePaths {
mail_dir = new File(etc_dir, "mail");
hooks_dir = new File(site_path, "hooks");
static_dir = new File(site_path, "static");
themes_dir = new File(site_path, "themes");
gerrit_sh = new File(bin_dir, "gerrit.sh");
gerrit_war = new File(bin_dir, "gerrit.war");
@@ -85,9 +91,9 @@ public final class SitePaths {
ssh_dsa = new File(etc_dir, "ssh_host_dsa_key");
peer_keys = new File(etc_dir, "peer_keys");
site_css = new File(etc_dir, "GerritSite.css");
site_header = new File(etc_dir, "GerritSiteHeader.html");
site_footer = new File(etc_dir, "GerritSiteFooter.html");
site_css = new File(etc_dir, CSS_FILENAME);
site_header = new File(etc_dir, HEADER_FILENAME);
site_footer = new File(etc_dir, FOOTER_FILENAME);
site_gitweb = new File(etc_dir, "gitweb_config.perl");
if (site_path.exists()) {

View File

@@ -30,6 +30,7 @@ public class GetConfig implements RestReadView<ProjectResource> {
public Boolean requireChangeId;
public Map<String, CommentLinkInfo> commentlinks;
public ThemeInfo theme;
}
@Override
@@ -51,6 +52,9 @@ public class GetConfig implements RestReadView<ProjectResource> {
for (CommentLinkInfo cl : project.getCommentLinks()) {
result.commentlinks.put(cl.name, cl);
}
// Themes are visible to anyone, as they are rendered client-side.
result.theme = project.getTheme();
return result;
}
}

View File

@@ -14,12 +14,14 @@
package com.google.gerrit.server.project;
import com.google.common.base.Charsets;
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;
import com.google.common.io.Files;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.LabelType;
@@ -35,6 +37,7 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.inject.Inject;
@@ -45,7 +48,10 @@ import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -59,11 +65,15 @@ import java.util.Set;
/** Cached information on a project. */
public class ProjectState {
private static final Logger log =
LoggerFactory.getLogger(ProjectState.class);
public interface Factory {
ProjectState create(ProjectConfig config);
}
private final boolean isAllProjects;
private final SitePaths sitePaths;
private final AllProjectsName allProjectsName;
private final ProjectCache projectCache;
private final ProjectControl.AssistedFactory projectControlFactory;
@@ -84,11 +94,15 @@ public class ProjectState {
/** Local access sections, wrapped in SectionMatchers for faster evaluation. */
private volatile List<SectionMatcher> localAccessSections;
/** Theme information loaded from site_path/themes. */
private volatile ThemeInfo theme;
/** If this is all projects, the capabilities used by the server. */
private final CapabilityCollection capabilities;
@Inject
public ProjectState(
final SitePaths sitePaths,
final ProjectCache projectCache,
final AllProjectsName allProjectsName,
final ProjectControl.AssistedFactory projectControlFactory,
@@ -97,6 +111,7 @@ public class ProjectState {
final RulesCache rulesCache,
final List<CommentLinkInfo> commentLinks,
@Assisted final ProjectConfig config) {
this.sitePaths = sitePaths;
this.projectCache = projectCache;
this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
this.allProjectsName = allProjectsName;
@@ -397,6 +412,47 @@ public class ProjectState {
return ImmutableList.copyOf(cls.values());
}
public ThemeInfo getTheme() {
ThemeInfo theme = this.theme;
if (theme == null) {
synchronized (this) {
theme = this.theme;
if (theme == null) {
theme = loadTheme();
this.theme = theme;
}
}
}
if (theme == ThemeInfo.INHERIT) {
ProjectState parent = Iterables.getFirst(parents(), null);
return parent != null ? parent.getTheme() : null;
}
return theme;
}
private ThemeInfo loadTheme() {
String name = getConfig().getProject().getName();
File dir = new File(sitePaths.themes_dir, name);
if (!dir.exists()) {
return ThemeInfo.INHERIT;
} else if (!dir.isDirectory()) {
log.warn("Bad theme for {}: not a directory", name);
return ThemeInfo.INHERIT;
}
try {
return new ThemeInfo(readFile(new File(dir, SitePaths.CSS_FILENAME)),
readFile(new File(dir, SitePaths.HEADER_FILENAME)),
readFile(new File(dir, SitePaths.FOOTER_FILENAME)));
} catch (IOException e) {
log.error("Error reading theme for " + name, e);
return ThemeInfo.INHERIT;
}
}
private String readFile(File f) throws IOException {
return f.exists() ? Files.toString(f, Charsets.UTF_8) : null;
}
private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
for (ProjectState s : tree()) {
switch (func.apply(s.getProject())) {

View File

@@ -0,0 +1,29 @@
// Copyright (C) 2013 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.git;
package com.google.gerrit.server.project;
public class ThemeInfo {
static final ThemeInfo INHERIT = new ThemeInfo(null, null, null);
public final String css;
public final String header;
public final String footer;
ThemeInfo(String css, String header, String footer) {
this.css = css;
this.header = header;
this.footer = footer;
}
}