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 = {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                            String.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}