001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools.bugreport; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagConstraints; 007import java.awt.GridBagLayout; 008import java.io.IOException; 009import java.io.InputStream; 010import java.net.URL; 011import java.net.URLEncoder; 012import java.nio.charset.StandardCharsets; 013import java.util.Base64; 014 015import javax.swing.JOptionPane; 016import javax.swing.JPanel; 017import javax.swing.SwingUtilities; 018import javax.xml.parsers.ParserConfigurationException; 019import javax.xml.xpath.XPath; 020import javax.xml.xpath.XPathConstants; 021import javax.xml.xpath.XPathExpressionException; 022import javax.xml.xpath.XPathFactory; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 026import org.openstreetmap.josm.gui.widgets.UrlLabel; 027import org.openstreetmap.josm.tools.GBC; 028import org.openstreetmap.josm.tools.HttpClient; 029import org.openstreetmap.josm.tools.HttpClient.Response; 030import org.openstreetmap.josm.tools.OpenBrowser; 031import org.openstreetmap.josm.tools.Utils; 032import org.w3c.dom.Document; 033import org.xml.sax.SAXException; 034 035/** 036 * This class handles sending the bug report to JOSM website. 037 * <p> 038 * Currently, we try to open a browser window for the user that displays the bug report. 039 * 040 * @author Michael Zangl 041 * @since 10055 042 */ 043public class BugReportSender extends Thread { 044 045 private final String statusText; 046 private String errorMessage; 047 048 /** 049 * Creates a new sender. 050 * @param statusText The status text to send. 051 */ 052 protected BugReportSender(String statusText) { 053 super("Bug report sender"); 054 this.statusText = statusText; 055 } 056 057 @Override 058 public void run() { 059 try { 060 // first, send the debug text using post. 061 String debugTextPasteId = pasteDebugText(); 062 063 // then open a browser to display the pasted text. 064 String openBrowserError = OpenBrowser.displayUrl(getJOSMTicketURL() + "?pdata_stored=" + debugTextPasteId); 065 if (openBrowserError != null) { 066 Main.warn(openBrowserError); 067 failed(openBrowserError); 068 } 069 } catch (BugReportSenderException e) { 070 Main.warn(e); 071 failed(e.getMessage()); 072 } 073 } 074 075 /** 076 * Sends the debug text to the server. 077 * @return The token which was returned by the server. We need to pass this on to the ticket system. 078 * @throws BugReportSenderException if sending the report failed. 079 */ 080 private String pasteDebugText() throws BugReportSenderException { 081 try { 082 String text = Utils.strip(statusText); 083 String pdata = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)); 084 String postQuery = "pdata=" + URLEncoder.encode(pdata, "UTF-8"); 085 HttpClient client = HttpClient.create(new URL(getJOSMTicketURL()), "POST") 086 .setHeader("Content-Type", "application/x-www-form-urlencoded") 087 .setRequestBody(postQuery.getBytes(StandardCharsets.UTF_8)); 088 089 Response connection = client.connect(); 090 091 if (connection.getResponseCode() >= 500) { 092 throw new BugReportSenderException("Internal server error."); 093 } 094 095 try (InputStream in = connection.getContent()) { 096 return retrieveDebugToken(Utils.parseSafeDOM(in)); 097 } 098 } catch (IOException | SAXException | ParserConfigurationException | XPathExpressionException t) { 099 throw new BugReportSenderException(t); 100 } 101 } 102 103 private static String getJOSMTicketURL() { 104 return Main.getJOSMWebsite() + "/josmticket"; 105 } 106 107 private static String retrieveDebugToken(Document document) throws XPathExpressionException, BugReportSenderException { 108 XPathFactory factory = XPathFactory.newInstance(); 109 XPath xpath = factory.newXPath(); 110 String status = (String) xpath.compile("/josmticket/@status").evaluate(document, XPathConstants.STRING); 111 if (!"ok".equals(status)) { 112 String message = (String) xpath.compile("/josmticket/error/text()").evaluate(document, 113 XPathConstants.STRING); 114 if (message.isEmpty()) { 115 message = "Error in server response but server did not tell us what happened."; 116 } 117 throw new BugReportSenderException(message); 118 } 119 120 String token = (String) xpath.compile("/josmticket/preparedid/text()") 121 .evaluate(document, XPathConstants.STRING); 122 if (token.isEmpty()) { 123 throw new BugReportSenderException("Server did not respond with a prepared id."); 124 } 125 return token; 126 } 127 128 private void failed(String string) { 129 errorMessage = string; 130 SwingUtilities.invokeLater(() -> { 131 JPanel errorPanel = new JPanel(new GridBagLayout()); 132 errorPanel.add(new JMultilineLabel( 133 tr("Opening the bug report failed. Please report manually using this website:")), 134 GBC.eol().fill(GridBagConstraints.HORIZONTAL)); 135 errorPanel.add(new UrlLabel(Main.getJOSMWebsite() + "/newticket", 2), GBC.eop().insets(8, 0, 0, 0)); 136 errorPanel.add(new DebugTextDisplay(statusText)); 137 138 JOptionPane.showMessageDialog(Main.parent, errorPanel, tr("You have encountered a bug in JOSM"), 139 JOptionPane.ERROR_MESSAGE); 140 }); 141 } 142 143 /** 144 * Returns the error message that could have occured during bug sending. 145 * @return the error message, or {@code null} if successful 146 */ 147 public final String getErrorMessage() { 148 return errorMessage; 149 } 150 151 private static class BugReportSenderException extends Exception { 152 BugReportSenderException(String message) { 153 super(message); 154 } 155 156 BugReportSenderException(Throwable cause) { 157 super(cause); 158 } 159 } 160 161 /** 162 * Opens the bug report window on the JOSM server. 163 * @param statusText The status text to send along to the server. 164 * @return bug report sender started thread 165 */ 166 public static BugReportSender reportBug(String statusText) { 167 BugReportSender sender = new BugReportSender(statusText); 168 sender.start(); 169 return sender; 170 } 171}