package io.gitlab.jfronny.gitea.helpdesk.web; import io.gitlab.jfronny.commons.StringFormatter; import io.gitlab.jfronny.gitea.helpdesk.Config; import io.gitlab.jfronny.gitea.helpdesk.Main; import io.gitlab.jfronny.gitea.helpdesk.db.DBInterface; import io.gitlab.jfronny.gitea.helpdesk.gitea.GiteaInterface; import net.freeutils.httpserver.HTTPServer; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.Map; public class WebInterface implements AutoCloseable { public static final String THEME = Main.getResource("/web/theme.css"); private static final String TEMPLATE = Main.getResource("/web/template.html"); private static final String ISSUE_MISSING_REPO = Main.getResource("/web/issue/missing_repo.html"); private static final String ISSUE_SUCCESS = Main.getResource("/web/issue/success.html"); private static final String REPLY_MISSING_REPO = Main.getResource("/web/reply/missing_repo.html"); private static final String REPLY_SUCCESS = Main.getResource("/web/reply/success.html"); private static final String UNSUBSCRIBE_MISSING_ID = Main.getResource("/web/unsubscribe/missing_id.html"); private static final String UNSUBSCRIBE_FAILURE = Main.getResource("/web/unsubscribe/failure.html"); private static final String UNSUBSCRIBE_SUCCESS = Main.getResource("/web/unsubscribe/success.html"); private static final String CLOSE_MISSING_ID = Main.getResource("/web/close/missing_id.html"); private static final String CLOSE_FAILURE = Main.getResource("/web/close/failure.html"); private static final String CLOSE_NOT_CREATOR = Main.getResource("/web/close/not_creator.html"); private static final String CLOSE_SUCCESS = Main.getResource("/web/close/success.html"); private String template(String body) { return TEMPLATE.replace("<%template-content%>", body); } private final HTTPServer server; private final Config.Web web; public WebInterface(Config.Web web, String address, DBInterface db, GiteaInterface gitea) throws IOException { server = new HTTPServer(web.port); this.web = web; HTTPServer.VirtualHost host = server.getVirtualHost(null); host.addContext("/unsubscribe", (req, resp) -> { String id = req.getParams().get("id"); if (id == null) { resp.send(404, template(UNSUBSCRIBE_MISSING_ID)); } else { try { db.removeSubscription(id); resp.send(202, template(UNSUBSCRIBE_SUCCESS)); } catch (Throwable e) { resp.send(500, template(UNSUBSCRIBE_FAILURE.formatted(escapeHTML(StringFormatter.toString(e))))); } } return 0; }); host.addContext("/close", ((req, resp) -> { String id = req.getParams().get("id"); if (id == null) { resp.send(404, template(CLOSE_MISSING_ID)); } else { try { db.getSubscription(id).ifPresentOrElse(sub -> { try { if (sub.creator()) { gitea.closeIssue(sub.repoOwner(), sub.repo(), sub.issue()); resp.send(202, template(CLOSE_SUCCESS)); } else { resp.send(403, template(CLOSE_NOT_CREATOR)); } } catch (IOException e) { throw new UncheckedIOException(e); } catch (URISyntaxException e) { throw new RuntimeException(e); } }, () -> { try { resp.send(404, template(CLOSE_MISSING_ID)); } catch (IOException e) { throw new UncheckedIOException(e); } }); } catch (Throwable e) { resp.send(500, template(CLOSE_FAILURE.formatted(escapeHTML(StringFormatter.toString(e))))); } } return 0; })); String[] addr = address.split("@"); host.addContext("/issue", (req, resp) -> { Map params = req.getParams(); String owner = params.get("owner"); String repo = params.get("repo"); if (owner == null || repo == null || !Main.PATH_SEGMENT_PATTERN.matcher(owner).matches() || !Main.PATH_SEGMENT_PATTERN.matcher(repo).matches()) { resp.send(404, template(ISSUE_MISSING_REPO)); } else { String mail = "mailto:" + addr[0] + "+create+" + owner + '+' + repo + '@' + addr[1]; resp.send(200, template(ISSUE_SUCCESS.formatted(escapeHTML(mail)))); } return 0; }); host.addContext("/comment", (req, resp) -> { Map params = req.getParams(); String owner = params.get("owner"); String repo = params.get("repo"); String issueId = params.get("id"); if (owner == null || repo == null || !Main.PATH_SEGMENT_PATTERN.matcher(owner).matches() || !Main.PATH_SEGMENT_PATTERN.matcher(repo).matches() || !Main.NUMBER_PATTERN.matcher(issueId).matches()) { resp.send(404, template(REPLY_MISSING_REPO)); } else { String mail = "mailto:" + addr[0] + "+comment+" + owner + '+' + repo + '+' + issueId + '@' + addr[1]; resp.send(200, template(REPLY_SUCCESS.formatted(escapeHTML(mail)))); } return 0; }); host.addContext("/theme.css", (req, resp) -> { byte[] content = THEME.getBytes(StandardCharsets.UTF_8); resp.sendHeaders(200, content.length, -1, "W/\"" + Integer.toHexString(THEME.hashCode()) + "\"", "text/css; charset=utf-8", null); resp.getBody().write(content); return 0; }); server.start(); } @Override public void close() { server.stop(); } public static String escapeHTML(String s) { StringBuilder out = new StringBuilder(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') { out.append("&#"); out.append((int) c); out.append(';'); } else if (c == '\n') { out.append("
\n"); } else { out.append(c); } } return out.toString(); } public String getAddress() { return web.publicAddress; } }