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 : }
|