001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io.remotecontrol.handler; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.UnsupportedEncodingException; 007import java.net.URLDecoder; 008import java.text.MessageFormat; 009import java.util.Collections; 010import java.util.HashMap; 011import java.util.HashSet; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Map; 015 016import javax.swing.JLabel; 017import javax.swing.JOptionPane; 018 019import org.openstreetmap.josm.Main; 020import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault; 021import org.openstreetmap.josm.tools.Utils; 022 023/** 024 * This is the parent of all classes that handle a specific remote control command 025 * 026 * @author Bodo Meissner 027 */ 028public abstract class RequestHandler { 029 030 public static final String globalConfirmationKey = "remotecontrol.always-confirm"; 031 public static final boolean globalConfirmationDefault = false; 032 public static final String loadInNewLayerKey = "remotecontrol.new-layer"; 033 public static final boolean loadInNewLayerDefault = false; 034 035 /** The GET request arguments */ 036 protected Map<String,String> args; 037 038 /** The request URL without "GET". */ 039 protected String request; 040 041 /** default response */ 042 protected String content = "OK\r\n"; 043 /** default content type */ 044 protected String contentType = "text/plain"; 045 046 /** will be filled with the command assigned to the subclass */ 047 protected String myCommand; 048 049 /** 050 * who sent the request? 051 * the host from referer header or IP of request sender 052 */ 053 protected String sender; 054 055 /** 056 * Check permission and parameters and handle request. 057 * 058 * @throws RequestHandlerForbiddenException 059 * @throws RequestHandlerBadRequestException 060 * @throws RequestHandlerErrorException 061 */ 062 public final void handle() throws RequestHandlerForbiddenException, RequestHandlerBadRequestException, RequestHandlerErrorException { 063 checkMandatoryParams(); 064 validateRequest(); 065 checkPermission(); 066 handleRequest(); 067 } 068 069 /** 070 * Validates the request before attempting to perform it. 071 * @throws RequestHandlerBadRequestException 072 * @since 5678 073 */ 074 protected abstract void validateRequest() throws RequestHandlerBadRequestException; 075 076 /** 077 * Handle a specific command sent as remote control. 078 * 079 * This method of the subclass will do the real work. 080 * 081 * @throws RequestHandlerErrorException 082 * @throws RequestHandlerBadRequestException 083 */ 084 protected abstract void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException; 085 086 /** 087 * Get a specific message to ask the user for permission for the operation 088 * requested via remote control. 089 * 090 * This message will be displayed to the user if the preference 091 * remotecontrol.always-confirm is true. 092 * 093 * @return the message 094 */ 095 public abstract String getPermissionMessage(); 096 097 /** 098 * Get a PermissionPref object containing the name of a special permission 099 * preference to individually allow the requested operation and an error 100 * message to be displayed when a disabled operation is requested. 101 * 102 * Default is not to check any special preference. Override this in a 103 * subclass to define permission preference and error message. 104 * 105 * @return the preference name and error message or null 106 */ 107 public abstract PermissionPrefWithDefault getPermissionPref(); 108 109 public abstract String[] getMandatoryParams(); 110 111 public String[] getOptionalParams() { 112 return null; 113 } 114 115 public String getUsage() { 116 return null; 117 } 118 119 public String[] getUsageExamples() { 120 return null; 121 } 122 123 /** 124 * Returns usage examples for the given command. To be overriden only my handlers that define several commands. 125 * @param cmd The command asked 126 * @return Usage examples for the given command 127 * @since 6332 128 */ 129 public String[] getUsageExamples(String cmd) { 130 return getUsageExamples(); 131 } 132 133 /** 134 * Check permissions in preferences and display error message 135 * or ask for permission. 136 * 137 * @throws RequestHandlerForbiddenException 138 */ 139 public final void checkPermission() throws RequestHandlerForbiddenException { 140 /* 141 * If the subclass defines a specific preference and if this is set 142 * to false, abort with an error message. 143 * 144 * Note: we use the deprecated class here for compatibility with 145 * older versions of WMSPlugin. 146 */ 147 PermissionPrefWithDefault permissionPref = getPermissionPref(); 148 if (permissionPref != null && permissionPref.pref != null) { 149 if (!Main.pref.getBoolean(permissionPref.pref, permissionPref.defaultVal)) { 150 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by preferences", myCommand); 151 Main.info(err); 152 throw new RequestHandlerForbiddenException(err); 153 } 154 } 155 156 /* Does the user want to confirm everything? 157 * If yes, display specific confirmation message. 158 */ 159 if (Main.pref.getBoolean(globalConfirmationKey, globalConfirmationDefault)) { 160 // Ensure dialog box does not exceed main window size 161 Integer maxWidth = (int) Math.max(200, Main.parent.getWidth()*0.6); 162 String message = "<html><div>" + getPermissionMessage() + 163 "<br/>" + tr("Do you want to allow this?") + "</div></html>"; 164 JLabel label = new JLabel(message); 165 if (label.getPreferredSize().width > maxWidth) { 166 label.setText(message.replaceFirst("<div>", "<div style=\"width:" + maxWidth + "px;\">")); 167 } 168 if (JOptionPane.showConfirmDialog(Main.parent, label, 169 tr("Confirm Remote Control action"), 170 JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { 171 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by user''s choice", myCommand); 172 throw new RequestHandlerForbiddenException(err); 173 } 174 } 175 } 176 177 /** 178 * Set request URL and parse args. 179 * 180 * @param url The request URL. 181 */ 182 public void setUrl(String url) { 183 this.request = url; 184 parseArgs(); 185 } 186 187 /** 188 * Parse the request parameters as key=value pairs. 189 * The result will be stored in {@code this.args}. 190 * 191 * Can be overridden by subclass. 192 */ 193 protected void parseArgs() { 194 try { 195 String req = URLDecoder.decode(this.request, "UTF-8"); 196 HashMap<String, String> args = new HashMap<>(); 197 if (req.indexOf('?') != -1) { 198 String query = req.substring(req.indexOf('?') + 1); 199 if (query.indexOf('#') != -1) { 200 query = query.substring(0, query.indexOf('#')); 201 } 202 String[] params = query.split("&", -1); 203 for (String param : params) { 204 int eq = param.indexOf('='); 205 if (eq != -1) { 206 args.put(param.substring(0, eq), param.substring(eq + 1)); 207 } 208 } 209 } 210 this.args = args; 211 } catch (UnsupportedEncodingException ex) { 212 throw new IllegalStateException(ex); 213 } 214 } 215 216 void checkMandatoryParams() throws RequestHandlerBadRequestException { 217 String[] mandatory = getMandatoryParams(); 218 String[] optional = getOptionalParams(); 219 List<String> missingKeys = new LinkedList<>(); 220 boolean error = false; 221 if(mandatory != null) for (String key : mandatory) { 222 String value = args.get(key); 223 if ((value == null) || (value.length() == 0)) { 224 error = true; 225 Main.warn("'" + myCommand + "' remote control request must have '" + key + "' parameter"); 226 missingKeys.add(key); 227 } 228 } 229 HashSet<String> knownParams = new HashSet<>(); 230 if (mandatory != null) Collections.addAll(knownParams, mandatory); 231 if (optional != null) Collections.addAll(knownParams, optional); 232 for (String par: args.keySet()) { 233 if (!knownParams.contains(par)) { 234 Main.warn("Unknown remote control parameter {0}, skipping it", par); 235 } 236 } 237 if (error) { 238 throw new RequestHandlerBadRequestException( 239 "The following keys are mandatory, but have not been provided: " 240 + Utils.join(", ", missingKeys)); 241 } 242 } 243 244 /** 245 * Save command associated with this handler. 246 * 247 * @param command The command. 248 */ 249 public void setCommand(String command) 250 { 251 if (command.charAt(0) == '/') { 252 command = command.substring(1); 253 } 254 myCommand = command; 255 } 256 257 public String getContent() { 258 return content; 259 } 260 261 public String getContentType() { 262 return contentType; 263 } 264 265 protected boolean isLoadInNewLayer() { 266 return args.get("new_layer") != null && !args.get("new_layer").isEmpty() 267 ? Boolean.parseBoolean(args.get("new_layer")) 268 : Main.pref.getBoolean(loadInNewLayerKey, loadInNewLayerDefault); 269 } 270 271 protected final String decodeParam(String param) { 272 try { 273 return URLDecoder.decode(param, "UTF-8"); 274 } catch (UnsupportedEncodingException e) { 275 throw new RuntimeException(e); 276 } 277 } 278 279 public void setSender(String sender) { 280 this.sender = sender; 281 } 282 283 public static class RequestHandlerException extends Exception { 284 285 public RequestHandlerException(String message) { 286 super(message); 287 } 288 public RequestHandlerException(String message, Throwable cause) { 289 super(message, cause); 290 } 291 public RequestHandlerException(Throwable cause) { 292 super(cause); 293 } 294 public RequestHandlerException() { 295 } 296 } 297 298 public static class RequestHandlerErrorException extends RequestHandlerException { 299 public RequestHandlerErrorException(Throwable cause) { 300 super(cause); 301 } 302 } 303 304 public static class RequestHandlerBadRequestException extends RequestHandlerException { 305 306 public RequestHandlerBadRequestException(String message) { 307 super(message); 308 } 309 public RequestHandlerBadRequestException(String message, Throwable cause) { 310 super(message, cause); 311 } 312 } 313 314 public static class RequestHandlerForbiddenException extends RequestHandlerException { 315 private static final long serialVersionUID = 2263904699747115423L; 316 317 public RequestHandlerForbiddenException(String message) { 318 super(message); 319 } 320 } 321}