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.net.URI; 007import java.net.URISyntaxException; 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; 015import java.util.Set; 016 017import javax.swing.JLabel; 018import javax.swing.JOptionPane; 019 020import org.openstreetmap.josm.Main; 021import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault; 022import org.openstreetmap.josm.spi.preferences.Config; 023import org.openstreetmap.josm.tools.Logging; 024import org.openstreetmap.josm.tools.Pair; 025import org.openstreetmap.josm.tools.Utils; 026 027/** 028 * This is the parent of all classes that handle a specific remote control command 029 * 030 * @author Bodo Meissner 031 */ 032public abstract class RequestHandler { 033 034 public static final String globalConfirmationKey = "remotecontrol.always-confirm"; 035 public static final boolean globalConfirmationDefault = false; 036 public static final String loadInNewLayerKey = "remotecontrol.new-layer"; 037 public static final boolean loadInNewLayerDefault = false; 038 039 /** past confirmations */ 040 protected static final PermissionCache PERMISSIONS = new PermissionCache(); 041 042 /** The GET request arguments */ 043 protected Map<String, String> args; 044 045 /** The request URL without "GET". */ 046 protected String request; 047 048 /** default response */ 049 protected String content = "OK\r\n"; 050 /** default content type */ 051 protected String contentType = "text/plain"; 052 053 /** will be filled with the command assigned to the subclass */ 054 protected String myCommand; 055 056 /** 057 * who sent the request? 058 * the host from referer header or IP of request sender 059 */ 060 protected String sender; 061 062 /** 063 * Check permission and parameters and handle request. 064 * 065 * @throws RequestHandlerForbiddenException if request is forbidden by preferences 066 * @throws RequestHandlerBadRequestException if request is invalid 067 * @throws RequestHandlerErrorException if an error occurs while processing request 068 */ 069 public final void handle() throws RequestHandlerForbiddenException, RequestHandlerBadRequestException, RequestHandlerErrorException { 070 checkMandatoryParams(); 071 validateRequest(); 072 checkPermission(); 073 handleRequest(); 074 } 075 076 /** 077 * Validates the request before attempting to perform it. 078 * @throws RequestHandlerBadRequestException if request is invalid 079 * @since 5678 080 */ 081 protected abstract void validateRequest() throws RequestHandlerBadRequestException; 082 083 /** 084 * Handle a specific command sent as remote control. 085 * 086 * This method of the subclass will do the real work. 087 * 088 * @throws RequestHandlerErrorException if an error occurs while processing request 089 * @throws RequestHandlerBadRequestException if request is invalid 090 */ 091 protected abstract void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException; 092 093 /** 094 * Get a specific message to ask the user for permission for the operation 095 * requested via remote control. 096 * 097 * This message will be displayed to the user if the preference 098 * remotecontrol.always-confirm is true. 099 * 100 * @return the message 101 */ 102 public abstract String getPermissionMessage(); 103 104 /** 105 * Get a PermissionPref object containing the name of a special permission 106 * preference to individually allow the requested operation and an error 107 * message to be displayed when a disabled operation is requested. 108 * 109 * Default is not to check any special preference. Override this in a 110 * subclass to define permission preference and error message. 111 * 112 * @return the preference name and error message or null 113 */ 114 public abstract PermissionPrefWithDefault getPermissionPref(); 115 116 public abstract String[] getMandatoryParams(); 117 118 public String[] getOptionalParams() { 119 return new String[0]; 120 } 121 122 public String getUsage() { 123 return null; 124 } 125 126 public String[] getUsageExamples() { 127 return new String[0]; 128 } 129 130 /** 131 * Returns usage examples for the given command. To be overriden only my handlers that define several commands. 132 * @param cmd The command asked 133 * @return Usage examples for the given command 134 * @since 6332 135 */ 136 public String[] getUsageExamples(String cmd) { 137 return getUsageExamples(); 138 } 139 140 /** 141 * Check permissions in preferences and display error message or ask for permission. 142 * 143 * @throws RequestHandlerForbiddenException if request is forbidden by preferences 144 */ 145 public final void checkPermission() throws RequestHandlerForbiddenException { 146 /* 147 * If the subclass defines a specific preference and if this is set 148 * to false, abort with an error message. 149 * 150 * Note: we use the deprecated class here for compatibility with 151 * older versions of WMSPlugin. 152 */ 153 PermissionPrefWithDefault permissionPref = getPermissionPref(); 154 if (permissionPref != null && permissionPref.pref != null && 155 !Config.getPref().getBoolean(permissionPref.pref, permissionPref.defaultVal)) { 156 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by preferences", myCommand); 157 Logging.info(err); 158 throw new RequestHandlerForbiddenException(err); 159 } 160 161 /* 162 * Did the user confirm this action previously? 163 * If yes, skip the global confirmation dialog. 164 */ 165 if (PERMISSIONS.isAllowed(myCommand, sender)) { 166 return; 167 } 168 169 /* Does the user want to confirm everything? 170 * If yes, display specific confirmation message. 171 */ 172 if (Config.getPref().getBoolean(globalConfirmationKey, globalConfirmationDefault)) { 173 // Ensure dialog box does not exceed main window size 174 Integer maxWidth = (int) Math.max(200, Main.parent.getWidth()*0.6); 175 String message = "<html><div>" + getPermissionMessage() + 176 "<br/>" + tr("Do you want to allow this?") + "</div></html>"; 177 JLabel label = new JLabel(message); 178 if (label.getPreferredSize().width > maxWidth) { 179 label.setText(message.replaceFirst("<div>", "<div style=\"width:" + maxWidth + "px;\">")); 180 } 181 Object[] choices = new Object[] {tr("Yes, always"), tr("Yes, once"), tr("No")}; 182 int choice = JOptionPane.showOptionDialog(Main.parent, label, tr("Confirm Remote Control action"), 183 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, choices, choices[1]); 184 if (choice != JOptionPane.YES_OPTION && choice != JOptionPane.NO_OPTION) { // Yes/no refer to always/once 185 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by user''s choice", myCommand); 186 throw new RequestHandlerForbiddenException(err); 187 } else if (choice == JOptionPane.YES_OPTION) { 188 PERMISSIONS.allow(myCommand, sender); 189 } 190 } 191 } 192 193 /** 194 * Set request URL and parse args. 195 * 196 * @param url The request URL. 197 * @throws RequestHandlerBadRequestException if request URL is invalid 198 */ 199 public void setUrl(String url) throws RequestHandlerBadRequestException { 200 this.request = url; 201 try { 202 parseArgs(); 203 } catch (URISyntaxException e) { 204 throw new RequestHandlerBadRequestException(e); 205 } 206 } 207 208 /** 209 * Parse the request parameters as key=value pairs. 210 * The result will be stored in {@code this.args}. 211 * 212 * Can be overridden by subclass. 213 * @throws URISyntaxException if request URL is invalid 214 */ 215 protected void parseArgs() throws URISyntaxException { 216 this.args = getRequestParameter(new URI(this.request)); 217 } 218 219 /** 220 * Returns the request parameters. 221 * @param uri URI as string 222 * @return map of request parameters 223 * @see <a href="http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding"> 224 * What every web developer must know about URL encoding</a> 225 */ 226 static Map<String, String> getRequestParameter(URI uri) { 227 Map<String, String> r = new HashMap<>(); 228 if (uri.getRawQuery() == null) { 229 return r; 230 } 231 for (String kv : uri.getRawQuery().split("&")) { 232 final String[] kvs = Utils.decodeUrl(kv).split("=", 2); 233 r.put(kvs[0], kvs.length > 1 ? kvs[1] : null); 234 } 235 return r; 236 } 237 238 void checkMandatoryParams() throws RequestHandlerBadRequestException { 239 String[] mandatory = getMandatoryParams(); 240 String[] optional = getOptionalParams(); 241 List<String> missingKeys = new LinkedList<>(); 242 boolean error = false; 243 if (mandatory != null && args != null) { 244 for (String key : mandatory) { 245 String value = args.get(key); 246 if (value == null || value.isEmpty()) { 247 error = true; 248 Logging.warn('\'' + myCommand + "' remote control request must have '" + key + "' parameter"); 249 missingKeys.add(key); 250 } 251 } 252 } 253 Set<String> knownParams = new HashSet<>(); 254 if (mandatory != null) 255 Collections.addAll(knownParams, mandatory); 256 if (optional != null) 257 Collections.addAll(knownParams, optional); 258 if (args != null) { 259 for (String par: args.keySet()) { 260 if (!knownParams.contains(par)) { 261 Logging.warn("Unknown remote control parameter {0}, skipping it", par); 262 } 263 } 264 } 265 if (error) { 266 throw new RequestHandlerBadRequestException( 267 tr("The following keys are mandatory, but have not been provided: {0}", 268 Utils.join(", ", missingKeys))); 269 } 270 } 271 272 /** 273 * Save command associated with this handler. 274 * 275 * @param command The command. 276 */ 277 public void setCommand(String command) { 278 if (command.charAt(0) == '/') { 279 command = command.substring(1); 280 } 281 myCommand = command; 282 } 283 284 public String getContent() { 285 return content; 286 } 287 288 public String getContentType() { 289 return contentType; 290 } 291 292 protected boolean isLoadInNewLayer() { 293 return args.get("new_layer") != null && !args.get("new_layer").isEmpty() 294 ? Boolean.parseBoolean(args.get("new_layer")) 295 : Config.getPref().getBoolean(loadInNewLayerKey, loadInNewLayerDefault); 296 } 297 298 public void setSender(String sender) { 299 this.sender = sender; 300 } 301 302 public static class RequestHandlerException extends Exception { 303 304 /** 305 * Constructs a new {@code RequestHandlerException}. 306 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 307 */ 308 public RequestHandlerException(String message) { 309 super(message); 310 } 311 312 /** 313 * Constructs a new {@code RequestHandlerException}. 314 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 315 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 316 */ 317 public RequestHandlerException(String message, Throwable cause) { 318 super(message, cause); 319 } 320 321 /** 322 * Constructs a new {@code RequestHandlerException}. 323 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 324 */ 325 public RequestHandlerException(Throwable cause) { 326 super(cause); 327 } 328 } 329 330 public static class RequestHandlerErrorException extends RequestHandlerException { 331 332 /** 333 * Constructs a new {@code RequestHandlerErrorException}. 334 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 335 */ 336 public RequestHandlerErrorException(Throwable cause) { 337 super(cause); 338 } 339 } 340 341 public static class RequestHandlerBadRequestException extends RequestHandlerException { 342 343 /** 344 * Constructs a new {@code RequestHandlerBadRequestException}. 345 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 346 */ 347 public RequestHandlerBadRequestException(String message) { 348 super(message); 349 } 350 351 /** 352 * Constructs a new {@code RequestHandlerBadRequestException}. 353 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 354 */ 355 public RequestHandlerBadRequestException(Throwable cause) { 356 super(cause); 357 } 358 359 /** 360 * Constructs a new {@code RequestHandlerBadRequestException}. 361 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 362 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 363 */ 364 public RequestHandlerBadRequestException(String message, Throwable cause) { 365 super(message, cause); 366 } 367 } 368 369 public static class RequestHandlerForbiddenException extends RequestHandlerException { 370 371 /** 372 * Constructs a new {@code RequestHandlerForbiddenException}. 373 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 374 */ 375 public RequestHandlerForbiddenException(String message) { 376 super(message); 377 } 378 } 379 380 public abstract static class RawURLParseRequestHandler extends RequestHandler { 381 @Override 382 protected void parseArgs() throws URISyntaxException { 383 Map<String, String> args = new HashMap<>(); 384 if (request.indexOf('?') != -1) { 385 String query = request.substring(request.indexOf('?') + 1); 386 if (query.indexOf("url=") == 0) { 387 args.put("url", Utils.decodeUrl(query.substring(4))); 388 } else { 389 int urlIdx = query.indexOf("&url="); 390 if (urlIdx != -1) { 391 args.put("url", Utils.decodeUrl(query.substring(urlIdx + 5))); 392 query = query.substring(0, urlIdx); 393 } else if (query.indexOf('#') != -1) { 394 query = query.substring(0, query.indexOf('#')); 395 } 396 String[] params = query.split("&", -1); 397 for (String param : params) { 398 int eq = param.indexOf('='); 399 if (eq != -1) { 400 args.put(param.substring(0, eq), Utils.decodeUrl(param.substring(eq + 1))); 401 } 402 } 403 } 404 } 405 this.args = args; 406 } 407 } 408 409 static class PermissionCache { 410 private final Set<Pair<String, String>> allowed = new HashSet<>(); 411 412 public void allow(String command, String sender) { 413 allowed.add(Pair.create(command, sender)); 414 } 415 416 public boolean isAllowed(String command, String sender) { 417 return allowed.contains(Pair.create(command, sender)); 418 } 419 420 public void clear() { 421 allowed.clear(); 422 } 423 } 424}