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