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