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.io.UnsupportedEncodingException;
007import java.net.URLDecoder;
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;
015
016import javax.swing.JLabel;
017import javax.swing.JOptionPane;
018
019import org.openstreetmap.josm.Main;
020import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * This is the parent of all classes that handle a specific remote control command
025 *
026 * @author Bodo Meissner
027 */
028public abstract class RequestHandler {
029
030    public static final String globalConfirmationKey = "remotecontrol.always-confirm";
031    public static final boolean globalConfirmationDefault = false;
032    public static final String loadInNewLayerKey = "remotecontrol.new-layer";
033    public static final boolean loadInNewLayerDefault = false;
034
035    /** The GET request arguments */
036    protected Map<String,String> args;
037
038    /** The request URL without "GET". */
039    protected String request;
040
041    /** default response */
042    protected String content = "OK\r\n";
043    /** default content type */
044    protected String contentType = "text/plain";
045
046    /** will be filled with the command assigned to the subclass */
047    protected String myCommand;
048
049    /**
050     * who sent the request?
051     * the host from referer header or IP of request sender
052     */
053    protected String sender;
054
055    /**
056     * Check permission and parameters and handle request.
057     *
058     * @throws RequestHandlerForbiddenException
059     * @throws RequestHandlerBadRequestException
060     * @throws RequestHandlerErrorException
061     */
062    public final void handle() throws RequestHandlerForbiddenException, RequestHandlerBadRequestException, RequestHandlerErrorException {
063        checkMandatoryParams();
064        validateRequest();
065        checkPermission();
066        handleRequest();
067    }
068
069    /**
070     * Validates the request before attempting to perform it.
071     * @throws RequestHandlerBadRequestException
072     * @since 5678
073     */
074    protected abstract void validateRequest() throws RequestHandlerBadRequestException;
075
076    /**
077     * Handle a specific command sent as remote control.
078     *
079     * This method of the subclass will do the real work.
080     *
081     * @throws RequestHandlerErrorException
082     * @throws RequestHandlerBadRequestException
083     */
084    protected abstract void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException;
085
086    /**
087     * Get a specific message to ask the user for permission for the operation
088     * requested via remote control.
089     *
090     * This message will be displayed to the user if the preference
091     * remotecontrol.always-confirm is true.
092     *
093     * @return the message
094     */
095    public abstract String getPermissionMessage();
096
097    /**
098     * Get a PermissionPref object containing the name of a special permission
099     * preference to individually allow the requested operation and an error
100     * message to be displayed when a disabled operation is requested.
101     *
102     * Default is not to check any special preference. Override this in a
103     * subclass to define permission preference and error message.
104     *
105     * @return the preference name and error message or null
106     */
107    public abstract PermissionPrefWithDefault getPermissionPref();
108
109    public abstract String[] getMandatoryParams();
110
111    public String[] getOptionalParams() {
112        return null;
113    }
114
115    public String getUsage() {
116        return null;
117    }
118
119    public String[] getUsageExamples() {
120        return null;
121    }
122
123    /**
124     * Returns usage examples for the given command. To be overriden only my handlers that define several commands.
125     * @param cmd The command asked
126     * @return Usage examples for the given command
127     * @since 6332
128     */
129    public String[] getUsageExamples(String cmd) {
130        return getUsageExamples();
131    }
132
133    /**
134     * Check permissions in preferences and display error message
135     * or ask for permission.
136     *
137     * @throws RequestHandlerForbiddenException
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            String req = URLDecoder.decode(this.request, "UTF-8");
196            HashMap<String, String> args = new HashMap<>();
197            if (req.indexOf('?') != -1) {
198                String query = req.substring(req.indexOf('?') + 1);
199                if (query.indexOf('#') != -1) {
200                            query = query.substring(0, query.indexOf('#'));
201                        }
202                String[] params = query.split("&", -1);
203                for (String param : params) {
204                    int eq = param.indexOf('=');
205                    if (eq != -1) {
206                        args.put(param.substring(0, eq), param.substring(eq + 1));
207                    }
208                }
209            }
210            this.args = args;
211        } catch (UnsupportedEncodingException ex) {
212            throw new IllegalStateException(ex);
213        }
214    }
215
216    void checkMandatoryParams() throws RequestHandlerBadRequestException {
217        String[] mandatory = getMandatoryParams();
218        String[] optional = getOptionalParams();
219        List<String> missingKeys = new LinkedList<>();
220        boolean error = false;
221        if(mandatory != null) for (String key : mandatory) {
222            String value = args.get(key);
223            if ((value == null) || (value.length() == 0)) {
224                error = true;
225                Main.warn("'" + myCommand + "' remote control request must have '" + key + "' parameter");
226                missingKeys.add(key);
227            }
228        }
229        HashSet<String> knownParams = new HashSet<>();
230        if (mandatory != null) Collections.addAll(knownParams, mandatory);
231        if (optional != null) Collections.addAll(knownParams, optional);
232        for (String par: args.keySet()) {
233            if (!knownParams.contains(par)) {
234                Main.warn("Unknown remote control parameter {0}, skipping it", par);
235            }
236        }
237        if (error) {
238            throw new RequestHandlerBadRequestException(
239                    "The following keys are mandatory, but have not been provided: "
240                    + Utils.join(", ", missingKeys));
241        }
242    }
243
244    /**
245     * Save command associated with this handler.
246     *
247     * @param command The command.
248     */
249    public void setCommand(String command)
250    {
251        if (command.charAt(0) == '/') {
252            command = command.substring(1);
253        }
254        myCommand = command;
255    }
256
257    public String getContent() {
258        return content;
259    }
260
261    public String getContentType() {
262        return contentType;
263    }
264
265    protected boolean isLoadInNewLayer() {
266        return args.get("new_layer") != null && !args.get("new_layer").isEmpty()
267                ? Boolean.parseBoolean(args.get("new_layer"))
268                : Main.pref.getBoolean(loadInNewLayerKey, loadInNewLayerDefault);
269    }
270
271    protected final String decodeParam(String param) {
272        try {
273            return URLDecoder.decode(param, "UTF-8");
274        } catch (UnsupportedEncodingException e) {
275            throw new RuntimeException(e);
276        }
277    }
278
279    public void setSender(String sender) {
280        this.sender = sender;
281    }
282
283    public static class RequestHandlerException extends Exception {
284
285        public RequestHandlerException(String message) {
286            super(message);
287        }
288        public RequestHandlerException(String message, Throwable cause) {
289            super(message, cause);
290        }
291        public RequestHandlerException(Throwable cause) {
292            super(cause);
293        }
294        public RequestHandlerException() {
295        }
296    }
297
298    public static class RequestHandlerErrorException extends RequestHandlerException {
299        public RequestHandlerErrorException(Throwable cause) {
300            super(cause);
301        }
302    }
303
304    public static class RequestHandlerBadRequestException extends RequestHandlerException {
305
306        public RequestHandlerBadRequestException(String message) {
307            super(message);
308        }
309        public RequestHandlerBadRequestException(String message, Throwable cause) {
310            super(message, cause);
311        }
312    }
313
314    public static class RequestHandlerForbiddenException extends RequestHandlerException {
315        private static final long serialVersionUID = 2263904699747115423L;
316
317        public RequestHandlerForbiddenException(String message) {
318            super(message);
319        }
320    }
321}