// Copyright (C) 2008 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.httpd.raw; import static com.google.gerrit.common.FileUtil.lastModified; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Strings; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.google.common.primitives.Bytes; import com.google.gerrit.common.Version; import com.google.gerrit.common.data.HostPageData; import com.google.gerrit.extensions.client.DiffPreferencesInfo; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.systemstatus.MessageOfTheDay; import com.google.gerrit.extensions.webui.WebUiPlugin; import com.google.gerrit.httpd.HtmlDomUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.GetDiffPreferences; import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtjsonrpc.server.JsonServlet; import com.google.gwtjsonrpc.server.RPCServletUtils; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringWriter; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; /** Sends the Gerrit host page to clients. */ @SuppressWarnings("serial") @Singleton public class HostPageServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(HostPageServlet.class); private static final String HPD_ID = "gerrit_hostpagedata"; private static final int DEFAULT_JS_LOAD_TIMEOUT = 5000; private final Provider<CurrentUser> currentUser; private final DynamicSet<WebUiPlugin> plugins; private final DynamicSet<MessageOfTheDay> messages; private final HostPageData.Theme signedOutTheme; private final HostPageData.Theme signedInTheme; private final SitePaths site; private final Document template; private final String noCacheName; private final boolean refreshHeaderFooter; private final SiteStaticDirectoryServlet staticServlet; private final boolean isNoteDbEnabled; private final Integer pluginsLoadTimeout; private final boolean canLoadInIFrame; private final GetDiffPreferences getDiff; private volatile Page page; @Inject HostPageServlet( Provider<CurrentUser> cu, SitePaths sp, ThemeFactory themeFactory, ServletContext servletContext, DynamicSet<WebUiPlugin> webUiPlugins, DynamicSet<MessageOfTheDay> motd, @GerritServerConfig Config cfg, SiteStaticDirectoryServlet ss, NotesMigration migration, GetDiffPreferences diffPref) throws IOException, ServletException { currentUser = cu; plugins = webUiPlugins; messages = motd; signedOutTheme = themeFactory.getSignedOutTheme(); signedInTheme = themeFactory.getSignedInTheme(); site = sp; refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true); staticServlet = ss; isNoteDbEnabled = migration.readChanges(); pluginsLoadTimeout = getPluginsLoadTimeout(cfg); canLoadInIFrame = cfg.getBoolean("gerrit", "canLoadInIFrame", false); getDiff = diffPref; String pageName = "HostPage.html"; template = HtmlDomUtil.parseFile(getClass(), pageName); if (template == null) { throw new FileNotFoundException("No " + pageName + " in webapp"); } if (HtmlDomUtil.find(template, "gerrit_module") == null) { throw new ServletException("No gerrit_module in " + pageName); } if (HtmlDomUtil.find(template, HPD_ID) == null) { throw new ServletException("No " + HPD_ID + " in " + pageName); } String src = "gerrit_ui/gerrit_ui.nocache.js"; try (InputStream in = servletContext.getResourceAsStream("/" + src)) { if (in != null) { Hasher md = Hashing.md5().newHasher(); byte[] buf = new byte[1024]; int n; while ((n = in.read(buf)) > 0) { md.putBytes(buf, 0, n); } src += "?content=" + md.hash().toString(); } else { log.debug("No " + src + " in webapp root; keeping noncache.js URL"); } } catch (IOException e) { throw new IOException("Failed reading " + src, e); } noCacheName = src; page = new Page(); } private static int getPluginsLoadTimeout(Config cfg) { long cfgValue = ConfigUtil.getTimeUnit( cfg, "plugins", null, "jsLoadTimeout", DEFAULT_JS_LOAD_TIMEOUT, TimeUnit.MILLISECONDS); if (cfgValue < 0) { return 0; } return (int) cfgValue; } private void json(Object data, StringWriter w) { JsonServlet.defaultGsonBuilder().create().toJson(data, w); } private Page get() { Page p = page; try { if (refreshHeaderFooter && p.isStale()) { p = new Page(); page = p; } } catch (IOException e) { log.error("Cannot refresh site header/footer", e); } return p; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { Page.Content page = select(req); StringWriter w = new StringWriter(); CurrentUser user = currentUser.get(); if (user.isIdentifiedUser()) { w.write(HPD_ID + ".accountDiffPref="); json(getDiffPreferences(user.asIdentifiedUser()), w); w.write(";"); w.write(HPD_ID + ".theme="); json(signedInTheme, w); w.write(";"); } else { w.write(HPD_ID + ".theme="); json(signedOutTheme, w); w.write(";"); } plugins(w); messages(w); byte[] hpd = w.toString().getBytes(UTF_8); byte[] raw = Bytes.concat(page.part1, hpd, page.part2); byte[] tosend; if (RPCServletUtils.acceptsGzipEncoding(req)) { rsp.setHeader("Content-Encoding", "gzip"); tosend = HtmlDomUtil.compress(raw); } else { tosend = raw; } CacheHeaders.setNotCacheable(rsp); rsp.setContentType("text/html"); rsp.setCharacterEncoding(HtmlDomUtil.ENC.name()); rsp.setContentLength(tosend.length); try (OutputStream out = rsp.getOutputStream()) { out.write(tosend); } } private DiffPreferencesInfo getDiffPreferences(IdentifiedUser user) { try { return getDiff.apply(new AccountResource(user)); } catch (AuthException | ConfigInvalidException | IOException | PermissionBackendException e) { log.warn("Cannot query account diff preferences", e); } return DiffPreferencesInfo.defaults(); } private void plugins(StringWriter w) { List<String> urls = new ArrayList<>(); for (WebUiPlugin u : plugins) { urls.add(String.format("plugins/%s/%s", u.getPluginName(), u.getJavaScriptResourcePath())); } if (!urls.isEmpty()) { w.write(HPD_ID + ".plugins="); json(urls, w); w.write(";"); } } private void messages(StringWriter w) { List<HostPageData.Message> list = new ArrayList<>(2); for (MessageOfTheDay motd : messages) { String html = motd.getHtmlMessage(); if (!Strings.isNullOrEmpty(html)) { HostPageData.Message m = new HostPageData.Message(); m.id = motd.getMessageId(); m.redisplay = motd.getRedisplay(); m.html = html; list.add(m); } } if (!list.isEmpty()) { w.write(HPD_ID + ".messages="); json(list, w); w.write(";"); } } private Page.Content select(HttpServletRequest req) { Page pg = get(); if ("1".equals(req.getParameter("dbg"))) { return pg.debug; } return pg.opt; } private void insertETags(Element e) { if ("img".equalsIgnoreCase(e.getTagName()) || "script".equalsIgnoreCase(e.getTagName())) { String src = e.getAttribute("src"); if (src != null && src.startsWith("static/")) { String name = src.substring("static/".length()); ResourceServlet.Resource r = staticServlet.getResource(name); if (r != null) { e.setAttribute("src", src + "?e=" + r.etag); } } } for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling()) { if (n instanceof Element) { insertETags((Element) n); } } } private static class FileInfo { private final Path path; private final long time; FileInfo(Path p) { path = p; time = lastModified(path); } boolean isStale() { return time != lastModified(path); } } private class Page { private final FileInfo css; private final FileInfo header; private final FileInfo footer; private final Content opt; private final Content debug; Page() throws IOException { Document hostDoc = HtmlDomUtil.clone(template); css = injectCssFile(hostDoc, "gerrit_sitecss", site.site_css); header = injectXmlFile(hostDoc, "gerrit_header", site.site_header); footer = injectXmlFile(hostDoc, "gerrit_footer", site.site_footer); HostPageData pageData = new HostPageData(); pageData.version = Version.getVersion(); pageData.isNoteDbEnabled = isNoteDbEnabled; pageData.pluginsLoadTimeout = pluginsLoadTimeout; pageData.canLoadInIFrame = canLoadInIFrame; StringWriter w = new StringWriter(); w.write("var " + HPD_ID + "="); json(pageData, w); w.write(";"); Element data = HtmlDomUtil.find(hostDoc, HPD_ID); asScript(data); data.appendChild(hostDoc.createTextNode(w.toString())); data.appendChild(hostDoc.createComment(HPD_ID)); Element nocache = HtmlDomUtil.find(hostDoc, "gerrit_module"); asScript(nocache); nocache.removeAttribute("id"); nocache.setAttribute("src", noCacheName); opt = new Content(hostDoc); nocache.setAttribute("src", "gerrit_ui/dbg_gerrit_ui.nocache.js"); debug = new Content(hostDoc); } boolean isStale() { return css.isStale() || header.isStale() || footer.isStale(); } private void asScript(Element scriptNode) { scriptNode.setAttribute("type", "text/javascript"); scriptNode.setAttribute("language", "javascript"); } class Content { final byte[] part1; final byte[] part2; Content(Document hostDoc) throws IOException { String raw = HtmlDomUtil.toString(hostDoc); int p = raw.indexOf("<!--" + HPD_ID); if (p < 0) { throw new IOException("No tag in transformed host page HTML"); } part1 = raw.substring(0, p).getBytes(UTF_8); part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes(UTF_8); } } private FileInfo injectCssFile(Document hostDoc, String id, Path src) throws IOException { FileInfo info = new FileInfo(src); Element banner = HtmlDomUtil.find(hostDoc, id); if (banner == null) { return info; } while (banner.getFirstChild() != null) { banner.removeChild(banner.getFirstChild()); } String css = HtmlDomUtil.readFile(src.getParent(), src.getFileName().toString()); if (css == null) { return info; } banner.appendChild(hostDoc.createCDATASection("\n" + css + "\n")); return info; } private FileInfo injectXmlFile(Document hostDoc, String id, Path src) throws IOException { FileInfo info = new FileInfo(src); Element banner = HtmlDomUtil.find(hostDoc, id); if (banner == null) { return info; } while (banner.getFirstChild() != null) { banner.removeChild(banner.getFirstChild()); } Document html = HtmlDomUtil.parseFile(src); if (html == null) { return info; } Element content = html.getDocumentElement(); insertETags(content); banner.appendChild(hostDoc.importNode(content, true)); return info; } } }