source: josm/trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/RequestHandler.java@ 16825

Last change on this file since 16825 was 16825, checked in by simon04, 4 years ago

fix #19400 - Remote control: generate OpenAPI specification

  • Property svn:eol-style set to native
File size: 18.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.remotecontrol.handler;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.net.URI;
7import java.net.URISyntaxException;
8import java.text.MessageFormat;
9import java.util.Collections;
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.LinkedList;
13import java.util.List;
14import java.util.Map;
15import java.util.Set;
16import java.util.function.Function;
17import java.util.function.Supplier;
18import java.util.regex.Pattern;
19
20import javax.swing.JLabel;
21import javax.swing.JOptionPane;
22
23import org.openstreetmap.josm.actions.downloadtasks.DownloadParams;
24import org.openstreetmap.josm.data.osm.DownloadPolicy;
25import org.openstreetmap.josm.data.osm.UploadPolicy;
26import org.openstreetmap.josm.data.preferences.BooleanProperty;
27import org.openstreetmap.josm.gui.MainApplication;
28import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
29import org.openstreetmap.josm.spi.preferences.Config;
30import org.openstreetmap.josm.tools.Logging;
31import org.openstreetmap.josm.tools.Pair;
32import org.openstreetmap.josm.tools.Utils;
33
34/**
35 * This is the parent of all classes that handle a specific remote control command
36 *
37 * @author Bodo Meissner
38 */
39public abstract class RequestHandler {
40
41 /** preference to determine if all Remote Control actions must be confirmed manually */
42 public static final BooleanProperty GLOBAL_CONFIRMATION = new BooleanProperty("remotecontrol.always-confirm", false);
43 /** preference to determine if remote control loads data in a new layer */
44 public static final BooleanProperty LOAD_IN_NEW_LAYER = new BooleanProperty("remotecontrol.new-layer", false);
45
46 protected static final Pattern SPLITTER_COMMA = Pattern.compile(",\\s*");
47 protected static final Pattern SPLITTER_SEMIC = Pattern.compile(";\\s*");
48
49 /** past confirmations */
50 protected static final PermissionCache PERMISSIONS = new PermissionCache();
51
52 /** The GET request arguments */
53 protected Map<String, String> args;
54
55 /** The request URL without "GET". */
56 protected String request;
57
58 /** default response */
59 protected String content = "OK\r\n";
60 /** default content type */
61 protected String contentType = "text/plain";
62
63 /** will be filled with the command assigned to the subclass */
64 protected String myCommand;
65
66 /**
67 * who sent the request?
68 * the host from referer header or IP of request sender
69 */
70 protected String sender;
71
72 /**
73 * Check permission and parameters and handle request.
74 *
75 * @throws RequestHandlerForbiddenException if request is forbidden by preferences
76 * @throws RequestHandlerBadRequestException if request is invalid
77 * @throws RequestHandlerErrorException if an error occurs while processing request
78 */
79 public final void handle() throws RequestHandlerForbiddenException, RequestHandlerBadRequestException, RequestHandlerErrorException {
80 checkMandatoryParams();
81 validateRequest();
82 checkPermission();
83 handleRequest();
84 }
85
86 /**
87 * Validates the request before attempting to perform it.
88 * @throws RequestHandlerBadRequestException if request is invalid
89 * @since 5678
90 */
91 protected abstract void validateRequest() throws RequestHandlerBadRequestException;
92
93 /**
94 * Handle a specific command sent as remote control.
95 *
96 * This method of the subclass will do the real work.
97 *
98 * @throws RequestHandlerErrorException if an error occurs while processing request
99 * @throws RequestHandlerBadRequestException if request is invalid
100 */
101 protected abstract void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException;
102
103 /**
104 * Get a specific message to ask the user for permission for the operation
105 * requested via remote control.
106 *
107 * This message will be displayed to the user if the preference
108 * remotecontrol.always-confirm is true.
109 *
110 * @return the message
111 */
112 public abstract String getPermissionMessage();
113
114 /**
115 * Get a PermissionPref object containing the name of a special permission
116 * preference to individually allow the requested operation and an error
117 * message to be displayed when a disabled operation is requested.
118 *
119 * Default is not to check any special preference. Override this in a
120 * subclass to define permission preference and error message.
121 *
122 * @return the preference name and error message or null
123 */
124 public abstract PermissionPrefWithDefault getPermissionPref();
125
126 /**
127 * Returns the mandatory parameters. Both used to enforce their presence at runtime and for documentation.
128 * @return the mandatory parameters
129 */
130 public abstract String[] getMandatoryParams();
131
132 /**
133 * Returns the optional parameters. Both used to enforce their presence at runtime and for documentation.
134 * @return the optional parameters
135 */
136 public String[] getOptionalParams() {
137 return new String[0];
138 }
139
140 /**
141 * Returns usage description, for bad requests and documentation.
142 * @return usage description
143 */
144 public String getUsage() {
145 return null;
146 }
147
148 /**
149 * Returns usage examples, for bad requests and documentation.
150 * @return Usage examples
151 */
152 public String[] getUsageExamples() {
153 return new String[0];
154 }
155
156 /**
157 * Returns usage examples for the given command. To be overriden only my handlers that define several commands.
158 * @param cmd The command asked
159 * @return Usage examples for the given command
160 * @since 6332
161 */
162 public String[] getUsageExamples(String cmd) {
163 return getUsageExamples();
164 }
165
166 /**
167 * Check permissions in preferences and display error message or ask for permission.
168 *
169 * @throws RequestHandlerForbiddenException if request is forbidden by preferences
170 */
171 public final void checkPermission() throws RequestHandlerForbiddenException {
172 /*
173 * If the subclass defines a specific preference and if this is set
174 * to false, abort with an error message.
175 *
176 * Note: we use the deprecated class here for compatibility with
177 * older versions of WMSPlugin.
178 */
179 PermissionPrefWithDefault permissionPref = getPermissionPref();
180 if (permissionPref != null && permissionPref.pref != null &&
181 !Config.getPref().getBoolean(permissionPref.pref, permissionPref.defaultVal)) {
182 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by preferences", myCommand);
183 Logging.info(err);
184 throw new RequestHandlerForbiddenException(err);
185 }
186
187 /*
188 * Did the user confirm this action previously?
189 * If yes, skip the global confirmation dialog.
190 */
191 if (PERMISSIONS.isAllowed(myCommand, sender)) {
192 return;
193 }
194
195 /* Does the user want to confirm everything?
196 * If yes, display specific confirmation message.
197 */
198 if (GLOBAL_CONFIRMATION.get()) {
199 // Ensure dialog box does not exceed main window size
200 Integer maxWidth = (int) Math.max(200, MainApplication.getMainFrame().getWidth()*0.6);
201 String message = "<html><div>" + getPermissionMessage() +
202 "<br/>" + tr("Do you want to allow this?") + "</div></html>";
203 JLabel label = new JLabel(message);
204 if (label.getPreferredSize().width > maxWidth) {
205 label.setText(message.replaceFirst("<div>", "<div style=\"width:" + maxWidth + "px;\">"));
206 }
207 Object[] choices = {tr("Yes, always"), tr("Yes, once"), tr("No")};
208 int choice = JOptionPane.showOptionDialog(MainApplication.getMainFrame(), label, tr("Confirm Remote Control action"),
209 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, choices, choices[1]);
210 if (choice != JOptionPane.YES_OPTION && choice != JOptionPane.NO_OPTION) { // Yes/no refer to always/once
211 String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by user''s choice", myCommand);
212 throw new RequestHandlerForbiddenException(err);
213 } else if (choice == JOptionPane.YES_OPTION) {
214 PERMISSIONS.allow(myCommand, sender);
215 }
216 }
217 }
218
219 /**
220 * Set request URL and parse args.
221 *
222 * @param url The request URL.
223 * @throws RequestHandlerBadRequestException if request URL is invalid
224 */
225 public void setUrl(String url) throws RequestHandlerBadRequestException {
226 this.request = url;
227 try {
228 parseArgs();
229 } catch (URISyntaxException e) {
230 throw new RequestHandlerBadRequestException(e);
231 }
232 }
233
234 /**
235 * Parse the request parameters as key=value pairs.
236 * The result will be stored in {@code this.args}.
237 *
238 * Can be overridden by subclass.
239 * @throws URISyntaxException if request URL is invalid
240 */
241 protected void parseArgs() throws URISyntaxException {
242 this.args = getRequestParameter(new URI(this.request));
243 }
244
245 protected final String[] splitArg(String arg, Pattern splitter) {
246 return splitter.split(args != null ? args.get(arg) : "", -1);
247 }
248
249 /**
250 * Returns the request parameters.
251 * @param uri URI as string
252 * @return map of request parameters
253 * @see <a href="http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding">
254 * What every web developer must know about URL encoding</a>
255 */
256 static Map<String, String> getRequestParameter(URI uri) {
257 Map<String, String> r = new HashMap<>();
258 if (uri.getRawQuery() == null) {
259 return r;
260 }
261 for (String kv : uri.getRawQuery().split("&", -1)) {
262 final String[] kvs = Utils.decodeUrl(kv).split("=", 2);
263 r.put(kvs[0], kvs.length > 1 ? kvs[1] : null);
264 }
265 return r;
266 }
267
268 void checkMandatoryParams() throws RequestHandlerBadRequestException {
269 String[] mandatory = getMandatoryParams();
270 String[] optional = getOptionalParams();
271 List<String> missingKeys = new LinkedList<>();
272 boolean error = false;
273 if (mandatory != null && args != null) {
274 for (String key : mandatory) {
275 String value = args.get(key);
276 if (value == null || value.isEmpty()) {
277 error = true;
278 Logging.warn('\'' + myCommand + "' remote control request must have '" + key + "' parameter");
279 missingKeys.add(key);
280 }
281 }
282 }
283 Set<String> knownParams = new HashSet<>();
284 if (mandatory != null)
285 Collections.addAll(knownParams, mandatory);
286 if (optional != null)
287 Collections.addAll(knownParams, optional);
288 if (args != null) {
289 for (String par: args.keySet()) {
290 if (!knownParams.contains(par)) {
291 Logging.warn("Unknown remote control parameter {0}, skipping it", par);
292 }
293 }
294 }
295 if (error) {
296 throw new RequestHandlerBadRequestException(
297 tr("The following keys are mandatory, but have not been provided: {0}",
298 String.join(", ", missingKeys)));
299 }
300 }
301
302 /**
303 * Save command associated with this handler.
304 *
305 * @param command The command.
306 */
307 public void setCommand(String command) {
308 if (command.charAt(0) == '/') {
309 command = command.substring(1);
310 }
311 myCommand = command;
312 }
313
314 /**
315 * Returns the command associated with this handler.
316 * @return the command associated with this handler.
317 */
318 public String getCommand() {
319 return myCommand;
320 }
321
322 /**
323 * Returns the response content.
324 * @return the response content
325 */
326 public String getContent() {
327 return content;
328 }
329
330 /**
331 * Returns the response content type.
332 * @return the response content type
333 */
334 public String getContentType() {
335 return contentType;
336 }
337
338 private <T> T get(String key, Function<String, T> parser, Supplier<T> defaultSupplier) {
339 String val = args.get(key);
340 return val != null && !val.isEmpty() ? parser.apply(val) : defaultSupplier.get();
341 }
342
343 private boolean get(String key) {
344 return get(key, Boolean::parseBoolean, () -> Boolean.FALSE);
345 }
346
347 private boolean isLoadInNewLayer() {
348 return get("new_layer", Boolean::parseBoolean, LOAD_IN_NEW_LAYER::get);
349 }
350
351 protected DownloadParams getDownloadParams() {
352 DownloadParams result = new DownloadParams();
353 if (args != null) {
354 result = result
355 .withNewLayer(isLoadInNewLayer())
356 .withLayerName(args.get("layer_name"))
357 .withLocked(get("layer_locked"))
358 .withDownloadPolicy(get("download_policy", DownloadPolicy::of, () -> DownloadPolicy.NORMAL))
359 .withUploadPolicy(get("upload_policy", UploadPolicy::of, () -> UploadPolicy.NORMAL));
360 }
361 return result;
362 }
363
364 protected void validateDownloadParams() throws RequestHandlerBadRequestException {
365 try {
366 getDownloadParams();
367 } catch (IllegalArgumentException e) {
368 throw new RequestHandlerBadRequestException(e);
369 }
370 }
371
372 /**
373 * Sets who sent the request (the host from referer header or IP of request sender)
374 * @param sender the host from referer header or IP of request sender
375 */
376 public void setSender(String sender) {
377 this.sender = sender;
378 }
379
380 /**
381 * Base exception of remote control handler errors.
382 */
383 public static class RequestHandlerException extends Exception {
384
385 /**
386 * Constructs a new {@code RequestHandlerException}.
387 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
388 */
389 public RequestHandlerException(String message) {
390 super(message);
391 }
392
393 /**
394 * Constructs a new {@code RequestHandlerException}.
395 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
396 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
397 */
398 public RequestHandlerException(String message, Throwable cause) {
399 super(message, cause);
400 }
401
402 /**
403 * Constructs a new {@code RequestHandlerException}.
404 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
405 */
406 public RequestHandlerException(Throwable cause) {
407 super(cause);
408 }
409 }
410
411 /**
412 * Error raised when a runtime error occurred.
413 */
414 public static class RequestHandlerErrorException extends RequestHandlerException {
415
416 /**
417 * Constructs a new {@code RequestHandlerErrorException}.
418 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
419 */
420 public RequestHandlerErrorException(Throwable cause) {
421 super(cause);
422 }
423 }
424
425 /**
426 * Error raised for bad requests.
427 */
428 public static class RequestHandlerBadRequestException extends RequestHandlerException {
429
430 /**
431 * Constructs a new {@code RequestHandlerBadRequestException}.
432 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
433 */
434 public RequestHandlerBadRequestException(String message) {
435 super(message);
436 }
437
438 /**
439 * Constructs a new {@code RequestHandlerBadRequestException}.
440 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
441 */
442 public RequestHandlerBadRequestException(Throwable cause) {
443 super(cause);
444 }
445
446 /**
447 * Constructs a new {@code RequestHandlerBadRequestException}.
448 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
449 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
450 */
451 public RequestHandlerBadRequestException(String message, Throwable cause) {
452 super(message, cause);
453 }
454 }
455
456 /**
457 * Error raised for forbidden usage.
458 */
459 public static class RequestHandlerForbiddenException extends RequestHandlerException {
460
461 /**
462 * Constructs a new {@code RequestHandlerForbiddenException}.
463 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
464 */
465 public RequestHandlerForbiddenException(String message) {
466 super(message);
467 }
468 }
469
470 /**
471 * Handler that takes an URL as parameter.
472 */
473 public abstract static class RawURLParseRequestHandler extends RequestHandler {
474 @Override
475 protected void parseArgs() throws URISyntaxException {
476 Map<String, String> args = new HashMap<>();
477 if (request.indexOf('?') != -1) {
478 String query = request.substring(request.indexOf('?') + 1);
479 if (query.indexOf("url=") == 0) {
480 args.put("url", Utils.decodeUrl(query.substring(4)));
481 } else {
482 int urlIdx = query.indexOf("&url=");
483 if (urlIdx != -1) {
484 args.put("url", Utils.decodeUrl(query.substring(urlIdx + 5)));
485 query = query.substring(0, urlIdx);
486 } else if (query.indexOf('#') != -1) {
487 query = query.substring(0, query.indexOf('#'));
488 }
489 String[] params = query.split("&", -1);
490 for (String param : params) {
491 int eq = param.indexOf('=');
492 if (eq != -1) {
493 args.put(param.substring(0, eq), Utils.decodeUrl(param.substring(eq + 1)));
494 }
495 }
496 }
497 }
498 this.args = args;
499 }
500 }
501
502 static class PermissionCache {
503 private final Set<Pair<String, String>> allowed = new HashSet<>();
504
505 public void allow(String command, String sender) {
506 allowed.add(Pair.create(command, sender));
507 }
508
509 public boolean isAllowed(String command, String sender) {
510 return allowed.contains(Pair.create(command, sender));
511 }
512
513 public void clear() {
514 allowed.clear();
515 }
516 }
517}
Note: See TracBrowser for help on using the repository browser.