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) {
149            if (!Main.pref.getBoolean(permissionPref.pref, permissionPref.defaultVal)) {
150                String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by preferences", myCommand);
151                Main.info(err);
152                throw new RequestHandlerForbiddenException(err);
153            }
154        }
155
156        /* Does the user want to confirm everything?
157         * If yes, display specific confirmation message.
158         */
159        if (Main.pref.getBoolean(globalConfirmationKey, globalConfirmationDefault)) {
160            // Ensure dialog box does not exceed main window size
161            Integer maxWidth = (int) Math.max(200, Main.parent.getWidth()*0.6);
162            String message = "<html><div>" + getPermissionMessage() +
163                    "<br/>" + tr("Do you want to allow this?") + "</div></html>";
164            JLabel label = new JLabel(message);
165            if (label.getPreferredSize().width > maxWidth) {
166                label.setText(message.replaceFirst("<div>", "<div style=\"width:" + maxWidth + "px;\">"));
167            }
168            if (JOptionPane.showConfirmDialog(Main.parent, label,
169                tr("Confirm Remote Control action"),
170                JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
171                    String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by user''s choice", myCommand);
172                    throw new RequestHandlerForbiddenException(err);
173            }
174        }
175    }
176
177    /**
178     * Set request URL and parse args.
179     *
180     * @param url The request URL.
181     */
182    public void setUrl(String url) {
183        this.request = url;
184        parseArgs();
185    }
186
187    /**
188     * Parse the request parameters as key=value pairs.
189     * The result will be stored in {@code this.args}.
190     *
191     * Can be overridden by subclass.
192     */
193    protected void parseArgs() {
194        try {
195            this.args = getRequestParameter(new URI(this.request));
196        } catch (URISyntaxException ex) {
197            throw new RuntimeException(ex);
198        }
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                    "The following keys are mandatory, but have not been provided: "
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        public RequestHandlerErrorException(Throwable cause) {
314            super(cause);
315        }
316    }
317
318    public static class RequestHandlerBadRequestException extends RequestHandlerException {
319
320        public RequestHandlerBadRequestException(String message) {
321            super(message);
322        }
323
324        public RequestHandlerBadRequestException(String message, Throwable cause) {
325            super(message, cause);
326        }
327    }
328
329    public static class RequestHandlerForbiddenException extends RequestHandlerException {
330        private static final long serialVersionUID = 2263904699747115423L;
331
332        public RequestHandlerForbiddenException(String message) {
333            super(message);
334        }
335    }
336
337    public abstract static class RawURLParseRequestHandler extends RequestHandler {
338        @Override
339        protected void parseArgs() {
340            Map<String, String> args = new HashMap<>();
341            if (request.indexOf('?') != -1) {
342                String query = request.substring(request.indexOf('?') + 1);
343                if (query.indexOf("url=") == 0) {
344                    args.put("url", Utils.decodeUrl(query.substring(4)));
345                } else {
346                    int urlIdx = query.indexOf("&url=");
347                    if (urlIdx != -1) {
348                        args.put("url", Utils.decodeUrl(query.substring(urlIdx + 5)));
349                        query = query.substring(0, urlIdx);
350                    } else if (query.indexOf('#') != -1) {
351                        query = query.substring(0, query.indexOf('#'));
352                    }
353                    String[] params = query.split("&", -1);
354                    for (String param : params) {
355                        int eq = param.indexOf('=');
356                        if (eq != -1) {
357                            args.put(param.substring(0, eq), Utils.decodeUrl(param.substring(eq + 1)));
358                        }
359                    }
360                }
361            }
362            this.args = args;
363        }
364    }
365}