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 send th request?
051     * the host from refrerer 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    {
064        checkMandatoryParams();
065        validateRequest();
066        checkPermission();
067        handleRequest();
068    }
069
070    /**
071     * Validates the request before attempting to perform it.
072     * @throws RequestHandlerBadRequestException
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
083     * @throws RequestHandlerBadRequestException
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    abstract public 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    abstract public PermissionPrefWithDefault getPermissionPref();
109
110    abstract public String[] getMandatoryParams();
111    
112    public String[] getOptionalParams() {
113        return null;
114    }
115     
116    public String[] getUsageExamples() {
117        return null;
118    }
119
120    /**
121     * Returns usage examples for the given command. To be overriden only my handlers that define several commands.
122     * @param cmd The command asked
123     * @return Usage examples for the given command
124     * @since 6332
125     */
126    public String[] getUsageExamples(String cmd) {
127        return getUsageExamples();
128    }
129
130    /**
131     * Check permissions in preferences and display error message
132     * or ask for permission.
133     *
134     * @throws RequestHandlerForbiddenException
135     */
136    final public void checkPermission() throws RequestHandlerForbiddenException
137    {
138        /*
139         * If the subclass defines a specific preference and if this is set
140         * to false, abort with an error message.
141         *
142         * Note: we use the deprecated class here for compatibility with
143         * older versions of WMSPlugin.
144         */
145        PermissionPrefWithDefault permissionPref = getPermissionPref();
146        if((permissionPref != null) && (permissionPref.pref != null))
147        {
148            if (!Main.pref.getBoolean(permissionPref.pref, permissionPref.defaultVal)) {
149                String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by preferences", myCommand);
150                Main.info(err);
151                throw new RequestHandlerForbiddenException(err);
152            }
153        }
154
155        /* Does the user want to confirm everything?
156         * If yes, display specific confirmation message.
157         */
158        if (Main.pref.getBoolean(globalConfirmationKey, globalConfirmationDefault)) {
159            // Ensure dialog box does not exceed main window size
160            Integer maxWidth = (int) Math.max(200, Main.parent.getWidth()*0.6);
161            String message = "<html><div>" + getPermissionMessage() +
162                    "<br/>" + tr("Do you want to allow this?") + "</div></html>";
163            JLabel label = new JLabel(message);
164            if (label.getPreferredSize().width > maxWidth) {
165                label.setText(message.replaceFirst("<div>", "<div style=\"width:" + maxWidth + "px;\">"));
166            }
167            if (JOptionPane.showConfirmDialog(Main.parent, label,
168                tr("Confirm Remote Control action"),
169                JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
170                    String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by user''s choice", myCommand);
171                    throw new RequestHandlerForbiddenException(err);
172            }
173        }
174    }
175
176    /**
177     * Set request URL and parse args.
178     *
179     * @param url The request URL.
180     */
181    public void setUrl(String url) {
182        this.request = url;
183        parseArgs();
184    }
185
186    /**
187     * Parse the request parameters as key=value pairs.
188     * The result will be stored in {@code this.args}.
189     *
190     * Can be overridden by subclass.
191     */
192    protected void parseArgs() {
193        try {
194            String req = URLDecoder.decode(this.request, "UTF-8");
195            HashMap<String, String> args = new HashMap<String, String>();
196            if (req.indexOf('?') != -1) {
197                String query = req.substring(req.indexOf('?') + 1);
198                if (query.indexOf('#') != -1) {
199                            query = query.substring(0, query.indexOf('#'));
200                        }
201                String[] params = query.split("&", -1);
202                for (String param : params) {
203                    int eq = param.indexOf('=');
204                    if (eq != -1) {
205                        args.put(param.substring(0, eq), param.substring(eq + 1));
206                    }
207                }
208            }
209            this.args = args;
210        } catch (UnsupportedEncodingException ex) {
211            throw new IllegalStateException(ex);
212        }
213    }
214
215    void checkMandatoryParams() throws RequestHandlerBadRequestException {
216        String[] mandatory = getMandatoryParams();
217        String[] optional = getOptionalParams();
218        List<String> missingKeys = new LinkedList<String>();
219        boolean error = false;
220        if(mandatory != null) for (String key : mandatory) {
221            String value = args.get(key);
222            if ((value == null) || (value.length() == 0)) {
223                error = true;
224                Main.warn("'" + myCommand + "' remote control request must have '" + key + "' parameter");
225                missingKeys.add(key);
226            }
227        }
228        HashSet<String> knownParams = new HashSet<String>();
229        if (mandatory != null) Collections.addAll(knownParams, mandatory);
230        if (optional != null) Collections.addAll(knownParams, optional);
231        for (String par: args.keySet()) {
232            if (!knownParams.contains(par)) {
233                Main.warn("Unknown remote control parameter {0}, skipping it", par);
234            }
235        }
236        if (error) {
237            throw new RequestHandlerBadRequestException(
238                    "The following keys are mandatory, but have not been provided: "
239                    + Utils.join(", ", missingKeys));
240        }
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();
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
289        public RequestHandlerException() {
290        }
291    }
292
293    public static class RequestHandlerErrorException extends RequestHandlerException {
294    }
295
296    public static class RequestHandlerBadRequestException extends RequestHandlerException {
297
298        public RequestHandlerBadRequestException(String message) {
299            super(message);
300        }
301    }
302
303    public static class RequestHandlerForbiddenException extends RequestHandlerException {
304        private static final long serialVersionUID = 2263904699747115423L;
305
306        public RequestHandlerForbiddenException(String message) {
307            super(message);
308        }
309    }
310}