// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.io.remotecontrol.handler;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
import org.openstreetmap.josm.tools.Utils;
/**
* This is the parent of all classes that handle a specific remote control command
*
* @author Bodo Meissner
*/
public abstract class RequestHandler {
public static final String globalConfirmationKey = "remotecontrol.always-confirm";
public static final boolean globalConfirmationDefault = false;
public static final String loadInNewLayerKey = "remotecontrol.new-layer";
public static final boolean loadInNewLayerDefault = false;
/** The GET request arguments */
protected Map<String, String> args;
/** The request URL without "GET". */
protected String request;
/** default response */
protected String content = "OK\r\n";
/** default content type */
protected String contentType = "text/plain";
/** will be filled with the command assigned to the subclass */
protected String myCommand;
/**
* who sent the request?
* the host from referer header or IP of request sender
*/
protected String sender;
/**
* Check permission and parameters and handle request.
*
* @throws RequestHandlerForbiddenException if request is forbidden by preferences
* @throws RequestHandlerBadRequestException if request is invalid
* @throws RequestHandlerErrorException if an error occurs while processing request
*/
public final void handle() throws RequestHandlerForbiddenException, RequestHandlerBadRequestException, RequestHandlerErrorException {
checkMandatoryParams();
validateRequest();
checkPermission();
handleRequest();
}
/**
* Validates the request before attempting to perform it.
* @throws RequestHandlerBadRequestException if request is invalid
* @since 5678
*/
protected abstract void validateRequest() throws RequestHandlerBadRequestException;
/**
* Handle a specific command sent as remote control.
*
* This method of the subclass will do the real work.
*
* @throws RequestHandlerErrorException if an error occurs while processing request
* @throws RequestHandlerBadRequestException if request is invalid
*/
protected abstract void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException;
/**
* Get a specific message to ask the user for permission for the operation
* requested via remote control.
*
* This message will be displayed to the user if the preference
* remotecontrol.always-confirm is true.
*
* @return the message
*/
public abstract String getPermissionMessage();
/**
* Get a PermissionPref object containing the name of a special permission
* preference to individually allow the requested operation and an error
* message to be displayed when a disabled operation is requested.
*
* Default is not to check any special preference. Override this in a
* subclass to define permission preference and error message.
*
* @return the preference name and error message or null
*/
public abstract PermissionPrefWithDefault getPermissionPref();
public abstract String[] getMandatoryParams();
public String[] getOptionalParams() {
return new String[0];
}
public String getUsage() {
return null;
}
public String[] getUsageExamples() {
return new String[0];
}
/**
* Returns usage examples for the given command. To be overriden only my handlers that define several commands.
* @param cmd The command asked
* @return Usage examples for the given command
* @since 6332
*/
public String[] getUsageExamples(String cmd) {
return getUsageExamples();
}
/**
* Check permissions in preferences and display error message or ask for permission.
*
* @throws RequestHandlerForbiddenException if request is forbidden by preferences
*/
public final void checkPermission() throws RequestHandlerForbiddenException {
/*
* If the subclass defines a specific preference and if this is set
* to false, abort with an error message.
*
* Note: we use the deprecated class here for compatibility with
* older versions of WMSPlugin.
*/
PermissionPrefWithDefault permissionPref = getPermissionPref();
if (permissionPref != null && permissionPref.pref != null && !Main.pref.getBoolean(permissionPref.pref, permissionPref.defaultVal)) {
String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by preferences", myCommand);
Main.info(err);
throw new RequestHandlerForbiddenException(err);
}
/* Does the user want to confirm everything?
* If yes, display specific confirmation message.
*/
if (Main.pref.getBoolean(globalConfirmationKey, globalConfirmationDefault)) {
// Ensure dialog box does not exceed main window size
Integer maxWidth = (int) Math.max(200, Main.parent.getWidth()*0.6);
String message = "<html><div>" + getPermissionMessage() +
"<br/>" + tr("Do you want to allow this?") + "</div></html>";
JLabel label = new JLabel(message);
if (label.getPreferredSize().width > maxWidth) {
label.setText(message.replaceFirst("<div>", "<div style=\"width:" + maxWidth + "px;\">"));
}
if (JOptionPane.showConfirmDialog(Main.parent, label,
tr("Confirm Remote Control action"),
JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
String err = MessageFormat.format("RemoteControl: ''{0}'' forbidden by user''s choice", myCommand);
throw new RequestHandlerForbiddenException(err);
}
}
}
/**
* Set request URL and parse args.
*
* @param url The request URL.
* @throws RequestHandlerBadRequestException if request URL is invalid
*/
public void setUrl(String url) throws RequestHandlerBadRequestException {
this.request = url;
try {
parseArgs();
} catch (URISyntaxException e) {
throw new RequestHandlerBadRequestException(e);
}
}
/**
* Parse the request parameters as key=value pairs.
* The result will be stored in {@code this.args}.
*
* Can be overridden by subclass.
* @throws URISyntaxException if request URL is invalid
*/
protected void parseArgs() throws URISyntaxException {
this.args = getRequestParameter(new URI(this.request));
}
/**
* Returns the request parameters.
* @param uri URI as string
* @return map of request parameters
* @see <a href="http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding">
* What every web developer must know about URL encoding</a>
*/
static Map<String, String> getRequestParameter(URI uri) {
Map<String, String> r = new HashMap<>();
if (uri.getRawQuery() == null) {
return r;
}
for (String kv : uri.getRawQuery().split("&")) {
final String[] kvs = Utils.decodeUrl(kv).split("=", 2);
r.put(kvs[0], kvs.length > 1 ? kvs[1] : null);
}
return r;
}
void checkMandatoryParams() throws RequestHandlerBadRequestException {
String[] mandatory = getMandatoryParams();
String[] optional = getOptionalParams();
List<String> missingKeys = new LinkedList<>();
boolean error = false;
if (mandatory != null && args != null) {
for (String key : mandatory) {
String value = args.get(key);
if (value == null || value.isEmpty()) {
error = true;
Main.warn('\'' + myCommand + "' remote control request must have '" + key + "' parameter");
missingKeys.add(key);
}
}
}
Set<String> knownParams = new HashSet<>();
if (mandatory != null)
Collections.addAll(knownParams, mandatory);
if (optional != null)
Collections.addAll(knownParams, optional);
if (args != null) {
for (String par: args.keySet()) {
if (!knownParams.contains(par)) {
Main.warn("Unknown remote control parameter {0}, skipping it", par);
}
}
}
if (error) {
throw new RequestHandlerBadRequestException(
tr("The following keys are mandatory, but have not been provided: {0}",
Utils.join(", ", missingKeys)));
}
}
/**
* Save command associated with this handler.
*
* @param command The command.
*/
public void setCommand(String command) {
if (command.charAt(0) == '/') {
command = command.substring(1);
}
myCommand = command;
}
public String getContent() {
return content;
}
public String getContentType() {
return contentType;
}
protected boolean isLoadInNewLayer() {
return args.get("new_layer") != null && !args.get("new_layer").isEmpty()
? Boolean.parseBoolean(args.get("new_layer"))
: Main.pref.getBoolean(loadInNewLayerKey, loadInNewLayerDefault);
}
public void setSender(String sender) {
this.sender = sender;
}
public static class RequestHandlerException extends Exception {
/**
* Constructs a new {@code RequestHandlerException}.
* @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
*/
public RequestHandlerException(String message) {
super(message);
}
/**
* Constructs a new {@code RequestHandlerException}.
* @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public RequestHandlerException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new {@code RequestHandlerException}.
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public RequestHandlerException(Throwable cause) {
super(cause);
}
}
public static class RequestHandlerErrorException extends RequestHandlerException {
/**
* Constructs a new {@code RequestHandlerErrorException}.
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public RequestHandlerErrorException(Throwable cause) {
super(cause);
}
}
public static class RequestHandlerBadRequestException extends RequestHandlerException {
/**
* Constructs a new {@code RequestHandlerBadRequestException}.
* @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
*/
public RequestHandlerBadRequestException(String message) {
super(message);
}
/**
* Constructs a new {@code RequestHandlerBadRequestException}.
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public RequestHandlerBadRequestException(Throwable cause) {
super(cause);
}
/**
* Constructs a new {@code RequestHandlerBadRequestException}.
* @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
*/
public RequestHandlerBadRequestException(String message, Throwable cause) {
super(message, cause);
}
}
public static class RequestHandlerForbiddenException extends RequestHandlerException {
/**
* Constructs a new {@code RequestHandlerForbiddenException}.
* @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
*/
public RequestHandlerForbiddenException(String message) {
super(message);
}
}
public abstract static class RawURLParseRequestHandler extends RequestHandler {
@Override
protected void parseArgs() throws URISyntaxException {
Map<String, String> args = new HashMap<>();
if (request.indexOf('?') != -1) {
String query = request.substring(request.indexOf('?') + 1);
if (query.indexOf("url=") == 0) {
args.put("url", Utils.decodeUrl(query.substring(4)));
} else {
int urlIdx = query.indexOf("&url=");
if (urlIdx != -1) {
args.put("url", Utils.decodeUrl(query.substring(urlIdx + 5)));
query = query.substring(0, urlIdx);
} else if (query.indexOf('#') != -1) {
query = query.substring(0, query.indexOf('#'));
}
String[] params = query.split("&", -1);
for (String param : params) {
int eq = param.indexOf('=');
if (eq != -1) {
args.put(param.substring(0, eq), Utils.decodeUrl(param.substring(eq + 1)));
}
}
}
}
this.args = args;
}
}
}