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