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