001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools.bugreport; 003 004import java.io.IOException; 005import java.io.InputStream; 006import java.net.URL; 007import java.net.URLEncoder; 008import java.nio.charset.StandardCharsets; 009import java.util.Base64; 010import java.util.Objects; 011 012import javax.xml.parsers.ParserConfigurationException; 013import javax.xml.xpath.XPath; 014import javax.xml.xpath.XPathConstants; 015import javax.xml.xpath.XPathExpressionException; 016import javax.xml.xpath.XPathFactory; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.tools.HttpClient; 020import org.openstreetmap.josm.tools.HttpClient.Response; 021import org.openstreetmap.josm.tools.Logging; 022import org.openstreetmap.josm.tools.OpenBrowser; 023import org.openstreetmap.josm.tools.Utils; 024import org.openstreetmap.josm.tools.XmlUtils; 025import org.w3c.dom.Document; 026import org.xml.sax.SAXException; 027 028/** 029 * This class handles sending the bug report to JOSM website. 030 * <p> 031 * Currently, we try to open a browser window for the user that displays the bug report. 032 * 033 * @author Michael Zangl 034 * @since 10055 035 */ 036public class BugReportSender extends Thread { 037 038 /** 039 * Called during bug submission to JOSM bugtracker. Completes the bug report submission and handles errors. 040 * @since 12790 041 */ 042 public interface BugReportSendingHandler { 043 /** 044 * Called when a bug is sent to JOSM bugtracker. 045 * @param bugUrl URL to visit to effectively submit the bug report to JOSM website 046 * @param statusText the status text being sent 047 * @return <code>null</code> for success or a string in case of an error 048 */ 049 String sendingBugReport(String bugUrl, String statusText); 050 051 /** 052 * Called when a bug failed to be sent to JOSM bugtracker. 053 * @param errorMessage the error message 054 * @param statusText the status text being sent 055 */ 056 void failed(String errorMessage, String statusText); 057 } 058 059 /** 060 * The fallback bug report sending handler if none is set. 061 * @since 12790 062 */ 063 public static final BugReportSendingHandler FALLBACK_BUGREPORT_SENDING_HANDLER = new BugReportSendingHandler() { 064 @Override 065 public String sendingBugReport(String bugUrl, String statusText) { 066 return OpenBrowser.displayUrl(bugUrl); 067 } 068 069 @Override 070 public void failed(String errorMessage, String statusText) { 071 Logging.error("Unable to send bug report: {0}\n{1}", errorMessage, statusText); 072 } 073 }; 074 075 private static volatile BugReportSendingHandler handler = FALLBACK_BUGREPORT_SENDING_HANDLER; 076 077 private final String statusText; 078 private String errorMessage; 079 080 /** 081 * Creates a new sender. 082 * @param statusText The status text to send. 083 */ 084 protected BugReportSender(String statusText) { 085 super("Bug report sender"); 086 this.statusText = statusText; 087 } 088 089 @Override 090 public void run() { 091 try { 092 // first, send the debug text using post. 093 String debugTextPasteId = pasteDebugText(); 094 String bugUrl = getJOSMTicketURL() + "?pdata_stored=" + debugTextPasteId; 095 096 // then notify handler 097 errorMessage = handler.sendingBugReport(bugUrl, statusText); 098 if (errorMessage != null) { 099 Logging.warn(errorMessage); 100 handler.failed(errorMessage, statusText); 101 } 102 } catch (BugReportSenderException e) { 103 Logging.warn(e); 104 errorMessage = e.getMessage(); 105 handler.failed(errorMessage, statusText); 106 } 107 } 108 109 /** 110 * Sends the debug text to the server. 111 * @return The token which was returned by the server. We need to pass this on to the ticket system. 112 * @throws BugReportSenderException if sending the report failed. 113 */ 114 private String pasteDebugText() throws BugReportSenderException { 115 try { 116 String text = Utils.strip(statusText); 117 String pdata = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)); 118 String postQuery = "pdata=" + URLEncoder.encode(pdata, "UTF-8"); 119 HttpClient client = HttpClient.create(new URL(getJOSMTicketURL()), "POST") 120 .setHeader("Content-Type", "application/x-www-form-urlencoded") 121 .setRequestBody(postQuery.getBytes(StandardCharsets.UTF_8)); 122 123 Response connection = client.connect(); 124 125 if (connection.getResponseCode() >= 500) { 126 throw new BugReportSenderException("Internal server error."); 127 } 128 129 try (InputStream in = connection.getContent()) { 130 return retrieveDebugToken(XmlUtils.parseSafeDOM(in)); 131 } 132 } catch (IOException | SAXException | ParserConfigurationException | XPathExpressionException t) { 133 throw new BugReportSenderException(t); 134 } 135 } 136 137 private static String getJOSMTicketURL() { 138 return Main.getJOSMWebsite() + "/josmticket"; 139 } 140 141 private static String retrieveDebugToken(Document document) throws XPathExpressionException, BugReportSenderException { 142 XPathFactory factory = XPathFactory.newInstance(); 143 XPath xpath = factory.newXPath(); 144 String status = (String) xpath.compile("/josmticket/@status").evaluate(document, XPathConstants.STRING); 145 if (!"ok".equals(status)) { 146 String message = (String) xpath.compile("/josmticket/error/text()").evaluate(document, 147 XPathConstants.STRING); 148 if (message.isEmpty()) { 149 message = "Error in server response but server did not tell us what happened."; 150 } 151 throw new BugReportSenderException(message); 152 } 153 154 String token = (String) xpath.compile("/josmticket/preparedid/text()") 155 .evaluate(document, XPathConstants.STRING); 156 if (token.isEmpty()) { 157 throw new BugReportSenderException("Server did not respond with a prepared id."); 158 } 159 return token; 160 } 161 162 /** 163 * Returns the error message that could have occured during bug sending. 164 * @return the error message, or {@code null} if successful 165 */ 166 public final String getErrorMessage() { 167 return errorMessage; 168 } 169 170 private static class BugReportSenderException extends Exception { 171 BugReportSenderException(String message) { 172 super(message); 173 } 174 175 BugReportSenderException(Throwable cause) { 176 super(cause); 177 } 178 } 179 180 /** 181 * Opens the bug report window on the JOSM server. 182 * @param statusText The status text to send along to the server. 183 * @return bug report sender started thread 184 */ 185 public static BugReportSender reportBug(String statusText) { 186 BugReportSender sender = new BugReportSender(statusText); 187 sender.start(); 188 return sender; 189 } 190 191 /** 192 * Sets the {@link BugReportSendingHandler} for bug report sender. 193 * @param bugReportSendingHandler the handler in charge of completing the bug report submission and handle errors. Must not be null 194 * @since 12790 195 */ 196 public static void setBugReportSendingHandler(BugReportSendingHandler bugReportSendingHandler) { 197 handler = Objects.requireNonNull(bugReportSendingHandler, "bugReportSendingHandler"); 198 } 199}