LCOV - code coverage report
Current view: top level - httpd/plugins - HttpPluginServlet.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 103 383 26.9 %
Date: 2022-11-19 15:00:39 Functions: 16 33 48.5 %

          Line data    Source code
       1             : // Copyright (C) 2012 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.httpd.plugins;
      16             : 
      17             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
      18             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
      19             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
      20             : import static com.google.common.net.HttpHeaders.ORIGIN;
      21             : import static com.google.common.net.HttpHeaders.VARY;
      22             : import static com.google.gerrit.common.FileUtil.lastModified;
      23             : import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CHARACTER_ENCODING;
      24             : import static com.google.gerrit.server.plugins.PluginEntry.ATTR_CONTENT_TYPE;
      25             : import static java.nio.charset.StandardCharsets.UTF_8;
      26             : import static java.util.stream.Collectors.toList;
      27             : 
      28             : import com.google.common.base.CharMatcher;
      29             : import com.google.common.base.Joiner;
      30             : import com.google.common.base.Splitter;
      31             : import com.google.common.base.Strings;
      32             : import com.google.common.cache.Cache;
      33             : import com.google.common.collect.Lists;
      34             : import com.google.common.collect.Maps;
      35             : import com.google.common.flogger.FluentLogger;
      36             : import com.google.common.io.ByteStreams;
      37             : import com.google.common.net.HttpHeaders;
      38             : import com.google.gerrit.common.Nullable;
      39             : import com.google.gerrit.httpd.resources.Resource;
      40             : import com.google.gerrit.httpd.resources.ResourceKey;
      41             : import com.google.gerrit.httpd.resources.SmallResource;
      42             : import com.google.gerrit.httpd.restapi.RestApiServlet;
      43             : import com.google.gerrit.server.config.CanonicalWebUrl;
      44             : import com.google.gerrit.server.config.GerritServerConfig;
      45             : import com.google.gerrit.server.documentation.MarkdownFormatter;
      46             : import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry;
      47             : import com.google.gerrit.server.plugins.Plugin;
      48             : import com.google.gerrit.server.plugins.Plugin.ApiType;
      49             : import com.google.gerrit.server.plugins.PluginContentScanner;
      50             : import com.google.gerrit.server.plugins.PluginEntry;
      51             : import com.google.gerrit.server.plugins.PluginsCollection;
      52             : import com.google.gerrit.server.plugins.ReloadPluginListener;
      53             : import com.google.gerrit.server.plugins.StartPluginListener;
      54             : import com.google.gerrit.server.ssh.SshInfo;
      55             : import com.google.gerrit.util.http.CacheHeaders;
      56             : import com.google.gerrit.util.http.RequestUtil;
      57             : import com.google.inject.Inject;
      58             : import com.google.inject.Provider;
      59             : import com.google.inject.Singleton;
      60             : import com.google.inject.name.Named;
      61             : import com.google.inject.servlet.GuiceFilter;
      62             : import java.io.BufferedReader;
      63             : import java.io.IOException;
      64             : import java.io.InputStream;
      65             : import java.io.InputStreamReader;
      66             : import java.io.OutputStream;
      67             : import java.io.UnsupportedEncodingException;
      68             : import java.nio.charset.Charset;
      69             : import java.nio.file.Files;
      70             : import java.nio.file.Path;
      71             : import java.util.ArrayList;
      72             : import java.util.HashMap;
      73             : import java.util.List;
      74             : import java.util.Locale;
      75             : import java.util.Map;
      76             : import java.util.Optional;
      77             : import java.util.concurrent.ConcurrentMap;
      78             : import java.util.function.Predicate;
      79             : import java.util.jar.Attributes;
      80             : import java.util.regex.Matcher;
      81             : import java.util.regex.Pattern;
      82             : import javax.servlet.FilterChain;
      83             : import javax.servlet.ServletConfig;
      84             : import javax.servlet.ServletContext;
      85             : import javax.servlet.ServletException;
      86             : import javax.servlet.http.HttpServlet;
      87             : import javax.servlet.http.HttpServletRequest;
      88             : import javax.servlet.http.HttpServletResponse;
      89             : import org.apache.commons.lang3.StringUtils;
      90             : import org.eclipse.jgit.lib.Config;
      91             : import org.eclipse.jgit.util.IO;
      92             : import org.eclipse.jgit.util.RawParseUtils;
      93             : 
      94             : @Singleton
      95             : class HttpPluginServlet extends HttpServlet implements StartPluginListener, ReloadPluginListener {
      96          99 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      97             : 
      98             :   private static final int SMALL_RESOURCE = 128 * 1024;
      99             :   private static final long serialVersionUID = 1L;
     100             : 
     101             :   private final MimeUtilFileTypeRegistry mimeUtil;
     102             :   private final Provider<String> webUrl;
     103             :   private final Cache<ResourceKey, Resource> resourceCache;
     104             :   private final String sshHost;
     105             :   private final int sshPort;
     106             :   private final RestApiServlet managerApi;
     107             : 
     108          99 :   private List<Plugin> pending = new ArrayList<>();
     109             :   private ContextMapper wrapper;
     110          99 :   private final ConcurrentMap<String, PluginHolder> plugins = Maps.newConcurrentMap();
     111             :   private final Pattern allowOrigin;
     112             : 
     113             :   @Inject
     114             :   HttpPluginServlet(
     115             :       MimeUtilFileTypeRegistry mimeUtil,
     116             :       @CanonicalWebUrl Provider<String> webUrl,
     117             :       @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
     118             :       SshInfo sshInfo,
     119             :       RestApiServlet.Globals globals,
     120             :       PluginsCollection plugins,
     121          99 :       @GerritServerConfig Config cfg) {
     122          99 :     this.mimeUtil = mimeUtil;
     123          99 :     this.webUrl = webUrl;
     124          99 :     this.resourceCache = cache;
     125          99 :     this.managerApi = new RestApiServlet(globals, plugins);
     126             : 
     127          99 :     String sshHost = "review.example.com";
     128          99 :     int sshPort = 29418;
     129          99 :     if (!sshInfo.getHostKeys().isEmpty()) {
     130          11 :       String host = sshInfo.getHostKeys().get(0).getHost();
     131          11 :       int c = host.lastIndexOf(':');
     132          11 :       if (0 <= c) {
     133          11 :         sshHost = host.substring(0, c);
     134          11 :         sshPort = Integer.parseInt(host.substring(c + 1));
     135             :       } else {
     136           0 :         sshHost = host;
     137           0 :         sshPort = 22;
     138             :       }
     139             :     }
     140          99 :     this.sshHost = sshHost;
     141          99 :     this.sshPort = sshPort;
     142          99 :     this.allowOrigin = makeAllowOrigin(cfg);
     143          99 :   }
     144             : 
     145             :   @Override
     146             :   public synchronized void init(ServletConfig config) throws ServletException {
     147          99 :     super.init(config);
     148             : 
     149          99 :     wrapper = new ContextMapper(config.getServletContext().getContextPath());
     150          99 :     for (Plugin plugin : pending) {
     151           0 :       install(plugin);
     152           0 :     }
     153          99 :     pending = null;
     154          99 :   }
     155             : 
     156             :   @Override
     157             :   public synchronized void onStartPlugin(Plugin plugin) {
     158           9 :     if (pending != null) {
     159           0 :       pending.add(plugin);
     160             :     } else {
     161           9 :       install(plugin);
     162             :     }
     163           9 :   }
     164             : 
     165             :   @Override
     166             :   public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
     167           1 :     install(newPlugin);
     168           1 :   }
     169             : 
     170             :   private void install(Plugin plugin) {
     171           9 :     GuiceFilter filter = load(plugin);
     172           9 :     final String name = plugin.getName();
     173           9 :     final PluginHolder holder = new PluginHolder(plugin, filter);
     174           9 :     plugin.add(() -> plugins.remove(name, holder));
     175           9 :     plugins.put(name, holder);
     176           9 :   }
     177             : 
     178             :   @Nullable
     179             :   private GuiceFilter load(Plugin plugin) {
     180           9 :     if (plugin.getHttpInjector() != null) {
     181           1 :       final String name = plugin.getName();
     182             :       final GuiceFilter filter;
     183             :       try {
     184           1 :         filter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
     185           0 :       } catch (RuntimeException e) {
     186           0 :         logger.atWarning().withCause(e).log("Plugin %s cannot load GuiceFilter", name);
     187           0 :         return null;
     188           1 :       }
     189             : 
     190             :       try {
     191           1 :         ServletContext ctx = PluginServletContext.create(plugin, wrapper.getFullPath(name));
     192           1 :         filter.init(new WrappedFilterConfig(ctx));
     193           0 :       } catch (ServletException e) {
     194           0 :         logger.atWarning().withCause(e).log("Plugin %s failed to initialize HTTP", name);
     195           0 :         return null;
     196           1 :       }
     197             : 
     198           1 :       plugin.add(filter::destroy);
     199           1 :       return filter;
     200             :     }
     201           9 :     return null;
     202             :   }
     203             : 
     204             :   @Override
     205             :   public void service(HttpServletRequest req, HttpServletResponse res)
     206             :       throws IOException, ServletException {
     207           1 :     List<String> parts =
     208           1 :         Lists.newArrayList(
     209           1 :             Splitter.on('/')
     210           1 :                 .limit(3)
     211           1 :                 .omitEmptyStrings()
     212           1 :                 .split(Strings.nullToEmpty(RequestUtil.getEncodedPathInfo(req))));
     213             : 
     214           1 :     if (isApiCall(req, parts)) {
     215           1 :       managerApi.service(req, res);
     216           1 :       return;
     217             :     }
     218             : 
     219           1 :     String name = parts.get(0);
     220           1 :     final PluginHolder holder = plugins.get(name);
     221           1 :     if (holder == null) {
     222           0 :       CacheHeaders.setNotCacheable(res);
     223           0 :       res.sendError(HttpServletResponse.SC_NOT_FOUND);
     224           0 :       return;
     225             :     }
     226             : 
     227           1 :     HttpServletRequest wr = wrapper.create(req, name);
     228           1 :     FilterChain chain =
     229           1 :         (sreq, sres) -> onDefault(holder, (HttpServletRequest) sreq, (HttpServletResponse) sres);
     230           1 :     if (holder.filter != null) {
     231           1 :       holder.filter.doFilter(wr, res, chain);
     232             :     } else {
     233           0 :       chain.doFilter(wr, res);
     234             :     }
     235           1 :   }
     236             : 
     237             :   private static boolean isApiCall(HttpServletRequest req, List<String> parts) {
     238           1 :     String method = req.getMethod();
     239           1 :     int cnt = parts.size();
     240           1 :     return cnt == 0
     241           1 :         || (cnt == 1 && ("PUT".equals(method) || "DELETE".equals(method)))
     242           1 :         || (cnt == 2 && parts.get(1).startsWith("gerrit~"));
     243             :   }
     244             : 
     245             :   private void onDefault(PluginHolder holder, HttpServletRequest req, HttpServletResponse res)
     246             :       throws IOException {
     247           1 :     if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
     248           0 :       CacheHeaders.setNotCacheable(res);
     249           0 :       res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
     250           0 :       return;
     251             :     }
     252             : 
     253           1 :     String pathInfo = RequestUtil.getEncodedPathInfo(req);
     254           1 :     if (pathInfo.length() < 1) {
     255           0 :       Resource.NOT_FOUND.send(req, res);
     256           0 :       return;
     257             :     }
     258             : 
     259           1 :     checkCors(req, res);
     260             : 
     261           1 :     String file = pathInfo.substring(1);
     262           1 :     PluginResourceKey key = PluginResourceKey.create(holder.plugin, file);
     263           1 :     Resource rsc = resourceCache.getIfPresent(key);
     264           1 :     if (rsc != null && req.getHeader(HttpHeaders.IF_MODIFIED_SINCE) == null) {
     265           0 :       rsc.send(req, res);
     266           0 :       return;
     267             :     }
     268             : 
     269           1 :     String uri = req.getRequestURI();
     270           1 :     if ("".equals(file)) {
     271           0 :       res.sendRedirect(uri + holder.docPrefix + "index.html");
     272           0 :       return;
     273             :     }
     274             : 
     275           1 :     if (file.startsWith(holder.staticPrefix)) {
     276           0 :       if (holder.plugin.getApiType() == ApiType.JS) {
     277           0 :         sendJsPlugin(holder.plugin, key, req, res);
     278             :       } else {
     279           0 :         PluginContentScanner scanner = holder.plugin.getContentScanner();
     280           0 :         Optional<PluginEntry> entry = scanner.getEntry(file);
     281           0 :         if (entry.isPresent()) {
     282           0 :           if (hasUpToDateCachedResource(rsc, entry.get().getTime())) {
     283           0 :             rsc.send(req, res);
     284             :           } else {
     285           0 :             sendResource(scanner, entry.get(), key, res);
     286             :           }
     287             :         } else {
     288           0 :           resourceCache.put(key, Resource.NOT_FOUND);
     289           0 :           Resource.NOT_FOUND.send(req, res);
     290             :         }
     291           0 :       }
     292           1 :     } else if (file.equals(holder.docPrefix.substring(0, holder.docPrefix.length() - 1))) {
     293           0 :       res.sendRedirect(uri + "/index.html");
     294           1 :     } else if (file.startsWith(holder.docPrefix) && file.endsWith("/")) {
     295           0 :       res.sendRedirect(uri + "index.html");
     296           1 :     } else if (file.startsWith(holder.docPrefix)) {
     297           0 :       PluginContentScanner scanner = holder.plugin.getContentScanner();
     298           0 :       Optional<PluginEntry> entry = scanner.getEntry(file);
     299           0 :       if (!entry.isPresent()) {
     300           0 :         entry = findSource(scanner, file);
     301             :       }
     302           0 :       if (!entry.isPresent() && file.endsWith("/index.html")) {
     303           0 :         String pfx = file.substring(0, file.length() - "index.html".length());
     304           0 :         long pluginLastModified = lastModified(holder.plugin.getSrcFile());
     305           0 :         if (hasUpToDateCachedResource(rsc, pluginLastModified)) {
     306           0 :           rsc.send(req, res);
     307             :         } else {
     308           0 :           sendAutoIndex(scanner, pfx, holder.plugin.getName(), key, res, pluginLastModified);
     309             :         }
     310           0 :       } else if (entry.isPresent() && entry.get().getName().endsWith(".md")) {
     311           0 :         if (hasUpToDateCachedResource(rsc, entry.get().getTime())) {
     312           0 :           rsc.send(req, res);
     313             :         } else {
     314           0 :           sendMarkdownAsHtml(scanner, entry.get(), holder.plugin.getName(), key, res);
     315             :         }
     316           0 :       } else if (entry.isPresent()) {
     317           0 :         if (hasUpToDateCachedResource(rsc, entry.get().getTime())) {
     318           0 :           rsc.send(req, res);
     319             :         } else {
     320           0 :           sendResource(scanner, entry.get(), key, res);
     321             :         }
     322             :       } else {
     323           0 :         resourceCache.put(key, Resource.NOT_FOUND);
     324           0 :         Resource.NOT_FOUND.send(req, res);
     325             :       }
     326           0 :     } else {
     327           1 :       resourceCache.put(key, Resource.NOT_FOUND);
     328           1 :       Resource.NOT_FOUND.send(req, res);
     329             :     }
     330           1 :   }
     331             : 
     332             :   @Nullable
     333             :   private static Pattern makeAllowOrigin(Config cfg) {
     334          99 :     String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
     335          99 :     if (allow.length > 0) {
     336           1 :       return Pattern.compile(Joiner.on('|').join(allow));
     337             :     }
     338          99 :     return null;
     339             :   }
     340             : 
     341             :   private void checkCors(HttpServletRequest req, HttpServletResponse res) {
     342           1 :     String origin = req.getHeader(ORIGIN);
     343           1 :     if (!Strings.isNullOrEmpty(origin) && isOriginAllowed(origin)) {
     344           0 :       res.addHeader(VARY, ORIGIN);
     345           0 :       setCorsHeaders(res, origin);
     346             :     }
     347           1 :   }
     348             : 
     349             :   private void setCorsHeaders(HttpServletResponse res, String origin) {
     350           0 :     res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
     351           0 :     res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
     352           0 :     res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, HEAD");
     353           0 :   }
     354             : 
     355             :   private boolean isOriginAllowed(String origin) {
     356           0 :     return allowOrigin == null || allowOrigin.matcher(origin).matches();
     357             :   }
     358             : 
     359             :   private boolean hasUpToDateCachedResource(Resource cachedResource, long lastUpdateTime) {
     360           0 :     return cachedResource != null && cachedResource.isUnchanged(lastUpdateTime);
     361             :   }
     362             : 
     363             :   private void appendPageAsSection(
     364             :       PluginContentScanner scanner, PluginEntry pluginEntry, String sectionTitle, StringBuilder md)
     365             :       throws IOException {
     366           0 :     InputStreamReader isr = new InputStreamReader(scanner.getInputStream(pluginEntry), UTF_8);
     367           0 :     StringBuilder content = new StringBuilder();
     368           0 :     try (BufferedReader reader = new BufferedReader(isr)) {
     369             :       String line;
     370           0 :       while ((line = reader.readLine()) != null) {
     371           0 :         line = StringUtils.stripEnd(line, null);
     372           0 :         if (line.isEmpty()) {
     373           0 :           content.append("\n");
     374             :         } else {
     375           0 :           content.append(line).append("\n");
     376             :         }
     377             :       }
     378             :     }
     379             : 
     380             :     // Only append the section if there was anything in it
     381           0 :     if (content.toString().trim().length() > 0) {
     382           0 :       md.append("## ");
     383           0 :       md.append(sectionTitle);
     384           0 :       md.append(" ##\n");
     385           0 :       md.append("\n").append(content);
     386           0 :       md.append("\n");
     387             :     }
     388           0 :   }
     389             : 
     390             :   private void appendEntriesSection(
     391             :       PluginContentScanner scanner,
     392             :       List<PluginEntry> entries,
     393             :       String sectionTitle,
     394             :       StringBuilder md,
     395             :       String prefix,
     396             :       int nameOffset)
     397             :       throws IOException {
     398           0 :     if (!entries.isEmpty()) {
     399           0 :       md.append("## ").append(sectionTitle).append(" ##\n");
     400           0 :       for (PluginEntry entry : entries) {
     401           0 :         String rsrc = entry.getName().substring(prefix.length());
     402             :         String entryTitle;
     403           0 :         if (rsrc.endsWith(".html")) {
     404           0 :           entryTitle = rsrc.substring(nameOffset, rsrc.length() - 5).replace('-', ' ');
     405           0 :         } else if (rsrc.endsWith(".md")) {
     406           0 :           entryTitle = extractTitleFromMarkdown(scanner, entry);
     407           0 :           if (Strings.isNullOrEmpty(entryTitle)) {
     408           0 :             entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' ');
     409             :           }
     410             :         } else {
     411           0 :           entryTitle = rsrc.substring(nameOffset).replace('-', ' ');
     412             :         }
     413           0 :         md.append(String.format("* [%s](%s)\n", entryTitle, rsrc));
     414           0 :       }
     415           0 :       md.append("\n");
     416             :     }
     417           0 :   }
     418             : 
     419             :   private void sendAutoIndex(
     420             :       PluginContentScanner scanner,
     421             :       final String prefix,
     422             :       final String pluginName,
     423             :       PluginResourceKey cacheKey,
     424             :       HttpServletResponse res,
     425             :       long lastModifiedTime)
     426             :       throws IOException {
     427           0 :     List<PluginEntry> cmds = new ArrayList<>();
     428           0 :     List<PluginEntry> servlets = new ArrayList<>();
     429           0 :     List<PluginEntry> restApis = new ArrayList<>();
     430           0 :     List<PluginEntry> docs = new ArrayList<>();
     431           0 :     PluginEntry about = null;
     432           0 :     PluginEntry toc = null;
     433             : 
     434           0 :     Predicate<PluginEntry> filter =
     435             :         entry -> {
     436           0 :           String name = entry.getName();
     437           0 :           Optional<Long> size = entry.getSize();
     438           0 :           if (name.startsWith(prefix)
     439           0 :               && (name.endsWith(".md") || name.endsWith(".html"))
     440           0 :               && size.isPresent()) {
     441           0 :             if (size.get() <= 0 || size.get() > SMALL_RESOURCE) {
     442           0 :               logger.atWarning().log(
     443             :                   "Plugin %s: %s omitted from document index. " + "Size %d out of range (0,%d).",
     444           0 :                   pluginName, name.substring(prefix.length()), size.get(), SMALL_RESOURCE);
     445           0 :               return false;
     446             :             }
     447           0 :             return true;
     448             :           }
     449           0 :           return false;
     450             :         };
     451             : 
     452           0 :     List<PluginEntry> entries = scanner.entries().filter(filter).collect(toList());
     453           0 :     for (PluginEntry entry : entries) {
     454           0 :       String name = entry.getName().substring(prefix.length());
     455           0 :       if (name.startsWith("cmd-")) {
     456           0 :         cmds.add(entry);
     457           0 :       } else if (name.startsWith("servlet-")) {
     458           0 :         servlets.add(entry);
     459           0 :       } else if (name.startsWith("rest-api-")) {
     460           0 :         restApis.add(entry);
     461           0 :       } else if (name.startsWith("about.")) {
     462           0 :         if (about == null) {
     463           0 :           about = entry;
     464             :         } else {
     465           0 :           logger.atWarning().log(
     466             :               "Plugin %s: Multiple 'about' documents found; using %s",
     467           0 :               pluginName, about.getName().substring(prefix.length()));
     468             :         }
     469           0 :       } else if (name.startsWith("toc.")) {
     470           0 :         if (toc == null) {
     471           0 :           toc = entry;
     472             :         } else {
     473           0 :           logger.atWarning().log(
     474             :               "Plugin %s: Multiple 'toc' documents found; using %s",
     475           0 :               pluginName, toc.getName().substring(prefix.length()));
     476             :         }
     477             :       } else {
     478           0 :         docs.add(entry);
     479             :       }
     480           0 :     }
     481             : 
     482           0 :     cmds.sort(PluginEntry.COMPARATOR_BY_NAME);
     483           0 :     docs.sort(PluginEntry.COMPARATOR_BY_NAME);
     484             : 
     485           0 :     StringBuilder md = new StringBuilder();
     486           0 :     md.append(String.format("# Plugin %s #\n", pluginName));
     487           0 :     md.append("\n");
     488           0 :     appendPluginInfoTable(md, scanner.getManifest().getMainAttributes());
     489             : 
     490           0 :     if (about != null) {
     491           0 :       appendPageAsSection(scanner, about, "About", md);
     492             :     }
     493             : 
     494           0 :     if (toc != null) {
     495           0 :       appendPageAsSection(scanner, toc, "Documentation", md);
     496             :     } else {
     497           0 :       appendEntriesSection(scanner, docs, "Documentation", md, prefix, 0);
     498           0 :       appendEntriesSection(scanner, servlets, "Servlets", md, prefix, "servlet-".length());
     499           0 :       appendEntriesSection(scanner, restApis, "REST APIs", md, prefix, "rest-api-".length());
     500           0 :       appendEntriesSection(scanner, cmds, "Commands", md, prefix, "cmd-".length());
     501             :     }
     502             : 
     503           0 :     sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res, lastModifiedTime);
     504           0 :   }
     505             : 
     506             :   private void sendMarkdownAsHtml(
     507             :       String md,
     508             :       String pluginName,
     509             :       PluginResourceKey cacheKey,
     510             :       HttpServletResponse res,
     511             :       long lastModifiedTime)
     512             :       throws UnsupportedEncodingException, IOException {
     513           0 :     Map<String, String> macros = new HashMap<>();
     514           0 :     macros.put("PLUGIN", pluginName);
     515           0 :     macros.put("SSH_HOST", sshHost);
     516           0 :     macros.put("SSH_PORT", "" + sshPort);
     517           0 :     String url = webUrl.get();
     518           0 :     if (Strings.isNullOrEmpty(url)) {
     519           0 :       url = "http://review.example.com/";
     520             :     }
     521           0 :     macros.put("URL", url);
     522             : 
     523           0 :     Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(md);
     524           0 :     StringBuilder sb = new StringBuilder();
     525           0 :     while (m.find()) {
     526           0 :       String key = m.group(2);
     527           0 :       String val = macros.get(key);
     528           0 :       if (m.group(1) != null) {
     529           0 :         m.appendReplacement(sb, "@" + key + "@");
     530           0 :       } else if (val != null) {
     531           0 :         m.appendReplacement(sb, val);
     532             :       } else {
     533           0 :         m.appendReplacement(sb, "@" + key + "@");
     534             :       }
     535           0 :     }
     536           0 :     m.appendTail(sb);
     537             : 
     538           0 :     byte[] html = new MarkdownFormatter().markdownToDocHtml(sb.toString(), UTF_8.name());
     539           0 :     resourceCache.put(
     540             :         cacheKey,
     541             :         new SmallResource(html)
     542           0 :             .setContentType("text/html")
     543           0 :             .setCharacterEncoding(UTF_8.name())
     544           0 :             .setLastModified(lastModifiedTime));
     545           0 :     res.setContentType("text/html");
     546           0 :     res.setCharacterEncoding(UTF_8.name());
     547           0 :     res.setContentLength(html.length);
     548           0 :     res.setDateHeader("Last-Modified", lastModifiedTime);
     549           0 :     res.getOutputStream().write(html);
     550           0 :   }
     551             : 
     552             :   private static void appendPluginInfoTable(StringBuilder html, Attributes main) {
     553           0 :     if (main != null) {
     554           0 :       String t = main.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
     555           0 :       String n = main.getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
     556           0 :       String v = main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
     557           0 :       String a = main.getValue("Gerrit-ApiVersion");
     558             : 
     559           0 :       html.append("<table class=\"plugin_info\">");
     560           0 :       if (!Strings.isNullOrEmpty(t)) {
     561           0 :         html.append("<tr><th>Name</th><td>").append(t).append("</td></tr>\n");
     562             :       }
     563           0 :       if (!Strings.isNullOrEmpty(n)) {
     564           0 :         html.append("<tr><th>Vendor</th><td>").append(n).append("</td></tr>\n");
     565             :       }
     566           0 :       if (!Strings.isNullOrEmpty(v)) {
     567           0 :         html.append("<tr><th>Version</th><td>").append(v).append("</td></tr>\n");
     568             :       }
     569           0 :       if (!Strings.isNullOrEmpty(a)) {
     570           0 :         html.append("<tr><th>API Version</th><td>").append(a).append("</td></tr>\n");
     571             :       }
     572           0 :       html.append("</table>\n");
     573             :     }
     574           0 :   }
     575             : 
     576             :   private static String extractTitleFromMarkdown(PluginContentScanner scanner, PluginEntry entry)
     577             :       throws IOException {
     578           0 :     String charEnc = null;
     579           0 :     Map<Object, String> atts = entry.getAttrs();
     580           0 :     if (atts != null) {
     581           0 :       charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
     582             :     }
     583           0 :     if (charEnc == null) {
     584           0 :       charEnc = UTF_8.name();
     585             :     }
     586           0 :     return new MarkdownFormatter()
     587           0 :         .extractTitleFromMarkdown(readWholeEntry(scanner, entry), charEnc);
     588             :   }
     589             : 
     590             :   private static Optional<PluginEntry> findSource(PluginContentScanner scanner, String file)
     591             :       throws IOException {
     592           0 :     if (file.endsWith(".html")) {
     593           0 :       int d = file.lastIndexOf('.');
     594           0 :       return scanner.getEntry(file.substring(0, d) + ".md");
     595             :     }
     596           0 :     return Optional.empty();
     597             :   }
     598             : 
     599             :   private void sendMarkdownAsHtml(
     600             :       PluginContentScanner scanner,
     601             :       PluginEntry entry,
     602             :       String pluginName,
     603             :       PluginResourceKey key,
     604             :       HttpServletResponse res)
     605             :       throws IOException {
     606           0 :     byte[] rawmd = readWholeEntry(scanner, entry);
     607           0 :     String encoding = null;
     608           0 :     Map<Object, String> atts = entry.getAttrs();
     609           0 :     if (atts != null) {
     610           0 :       encoding = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
     611             :     }
     612             : 
     613             :     String txtmd =
     614           0 :         RawParseUtils.decode(Charset.forName(encoding != null ? encoding : UTF_8.name()), rawmd);
     615           0 :     long time = entry.getTime();
     616           0 :     if (0 < time) {
     617           0 :       res.setDateHeader("Last-Modified", time);
     618             :     }
     619           0 :     sendMarkdownAsHtml(txtmd, pluginName, key, res, time);
     620           0 :   }
     621             : 
     622             :   private void sendResource(
     623             :       PluginContentScanner scanner,
     624             :       PluginEntry entry,
     625             :       PluginResourceKey key,
     626             :       HttpServletResponse res)
     627             :       throws IOException {
     628           0 :     byte[] data = null;
     629           0 :     Optional<Long> size = entry.getSize();
     630           0 :     if (size.isPresent() && size.get() <= SMALL_RESOURCE) {
     631           0 :       data = readWholeEntry(scanner, entry);
     632             :     }
     633             : 
     634           0 :     String contentType = null;
     635           0 :     String charEnc = null;
     636           0 :     Map<Object, String> atts = entry.getAttrs();
     637           0 :     if (atts != null) {
     638           0 :       contentType = Strings.emptyToNull(atts.get(ATTR_CONTENT_TYPE));
     639           0 :       charEnc = Strings.emptyToNull(atts.get(ATTR_CHARACTER_ENCODING));
     640             :     }
     641           0 :     if (contentType == null) {
     642           0 :       contentType = mimeUtil.getMimeType(entry.getName(), data).toString();
     643           0 :       if ("application/octet-stream".equals(contentType) && entry.getName().endsWith(".js")) {
     644           0 :         contentType = "application/javascript";
     645           0 :       } else if ("application/x-pointplus".equals(contentType)
     646           0 :           && entry.getName().endsWith(".css")) {
     647           0 :         contentType = "text/css";
     648             :       }
     649             :     }
     650             : 
     651           0 :     long time = entry.getTime();
     652           0 :     if (0 < time) {
     653           0 :       res.setDateHeader("Last-Modified", time);
     654             :     }
     655           0 :     if (size.isPresent()) {
     656           0 :       res.setHeader("Content-Length", size.get().toString());
     657             :     }
     658           0 :     res.setContentType(contentType);
     659           0 :     if (charEnc != null) {
     660           0 :       res.setCharacterEncoding(charEnc);
     661             :     }
     662           0 :     if (data != null) {
     663           0 :       resourceCache.put(
     664             :           key,
     665             :           new SmallResource(data)
     666           0 :               .setContentType(contentType)
     667           0 :               .setCharacterEncoding(charEnc)
     668           0 :               .setLastModified(time));
     669           0 :       res.getOutputStream().write(data);
     670             :     } else {
     671           0 :       writeToResponse(res, scanner.getInputStream(entry));
     672             :     }
     673           0 :   }
     674             : 
     675             :   private void sendJsPlugin(
     676             :       Plugin plugin, PluginResourceKey key, HttpServletRequest req, HttpServletResponse res)
     677             :       throws IOException {
     678           0 :     Path path = plugin.getSrcFile();
     679           0 :     if (req.getRequestURI().endsWith(getJsPluginPath(plugin)) && Files.exists(path)) {
     680           0 :       res.setHeader("Content-Length", Long.toString(Files.size(path)));
     681           0 :       if (path.toString().toLowerCase(Locale.US).endsWith(".html")) {
     682           0 :         res.setContentType("text/html");
     683             :       } else {
     684           0 :         res.setContentType("application/javascript");
     685             :       }
     686           0 :       writeToResponse(res, Files.newInputStream(path));
     687             :     } else {
     688           0 :       resourceCache.put(key, Resource.NOT_FOUND);
     689           0 :       Resource.NOT_FOUND.send(req, res);
     690             :     }
     691           0 :   }
     692             : 
     693             :   private static String getJsPluginPath(Plugin plugin) {
     694           0 :     return String.format(
     695           0 :         "/plugins/%s/static/%s", plugin.getName(), plugin.getSrcFile().getFileName());
     696             :   }
     697             : 
     698             :   private void writeToResponse(HttpServletResponse res, InputStream inputStream)
     699             :       throws IOException {
     700           0 :     try (InputStream in = inputStream;
     701           0 :         OutputStream out = res.getOutputStream()) {
     702           0 :       ByteStreams.copy(in, out);
     703             :     }
     704           0 :   }
     705             : 
     706             :   private static byte[] readWholeEntry(PluginContentScanner scanner, PluginEntry entry)
     707             :       throws IOException {
     708           0 :     try (InputStream in = scanner.getInputStream(entry)) {
     709           0 :       return IO.readWholeStream(in, entry.getSize().get().intValue()).array();
     710             :     }
     711             :   }
     712             : 
     713             :   private static class PluginHolder {
     714             :     final Plugin plugin;
     715             :     final GuiceFilter filter;
     716             :     final String staticPrefix;
     717             :     final String docPrefix;
     718             : 
     719           9 :     PluginHolder(Plugin plugin, GuiceFilter filter) {
     720           9 :       this.plugin = plugin;
     721           9 :       this.filter = filter;
     722           9 :       this.staticPrefix = getPrefix(plugin, "Gerrit-HttpStaticPrefix", "static/");
     723           9 :       this.docPrefix = getPrefix(plugin, "Gerrit-HttpDocumentationPrefix", "Documentation/");
     724           9 :     }
     725             : 
     726             :     @Nullable
     727             :     private static String getPrefix(Plugin plugin, String attr, String def) {
     728           9 :       Path path = plugin.getSrcFile();
     729           9 :       PluginContentScanner scanner = plugin.getContentScanner();
     730           9 :       if (path == null || scanner == PluginContentScanner.EMPTY) {
     731           9 :         return def;
     732             :       }
     733             :       try {
     734           0 :         String prefix = scanner.getManifest().getMainAttributes().getValue(attr);
     735           0 :         if (prefix != null) {
     736           0 :           return CharMatcher.is('/').trimFrom(prefix) + "/";
     737             :         }
     738           0 :         return def;
     739           0 :       } catch (IOException e) {
     740           0 :         logger.atWarning().withCause(e).log(
     741           0 :             "Error getting %s for plugin %s, using default", attr, plugin.getName());
     742           0 :         return null;
     743             :       }
     744             :     }
     745             :   }
     746             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750