diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/AbstractMapTokenizer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/AbstractMapTokenizer.java
index 0decb6201bc..ee8b9ebfff7 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/AbstractMapTokenizer.java
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/AbstractMapTokenizer.java
@@ -18,6 +18,26 @@ public abstract class AbstractMapTokenizer
implements PlaceToke
@Override
public P getPlace(String token) {
+ /*
+ * See bug 6088; the token delivered here as parameter has undergone encoding/decoding using URL.encodeURI; this
+ * leaves encoded characters with special meaning in the URL untouched, such as '/', '&' or '?', meaning that
+ * their encoding such as %2F for the '/' character will remain unchanged in the token and will not be replaced
+ * by '/'. Other characters, such as the '%' character, encoded as '%25', will be decoded.
+ *
+ * But we do need to be able to encode special characters in tokens, e.g., for regatta names which may contain
+ * any UTF character. The problem, though, is that what we receive here as the "token" parameter is already a
+ * "projection" that cannot be inverted to the original string. For example, when we see "%2F" as the "token"
+ * value, we cannot tell whether the original URL hash/fragment was "%252F" or just "%2F" as the latter would
+ * have been returned unchanged.
+ *
+ * Alternatively, we could look at Window.Location.getHash() and silently assume that that would be what was
+ * decoded and then passed to this method; interestingly, the call to here from SwitchingEntryPoint.onModuleLoad()
+ * passes an non-decoded hash/fragment taken straight from Window.Location.getHash().
+ *
+ * Yet alternatively, we could find an encoding of values in tokens that is guaranteed to never produce the
+ * offending characters such as '%', '/' or the like. As an extreme case, we could use a Base64 encoding
+ * of all values and then run a Base64 decoding to obtain the original token again.
+ */
return getPlaceFromParameters(toParameterMap(token));
}
@@ -32,7 +52,6 @@ public abstract class AbstractMapTokenizer
implements PlaceToke
private String toToken(Map> parameters) {
StringBuilder stringBuilder = new StringBuilder();
-
for (Map.Entry> entry : parameters.entrySet()) {
if (entry.getValue() == null || entry.getValue().isEmpty()) {
continue;
@@ -41,31 +60,34 @@ public abstract class AbstractMapTokenizer implements PlaceToke
if (stringBuilder.length() > 0) {
stringBuilder.append("&");
}
- stringBuilder.append(entry.getKey() + "=" + val);
+ stringBuilder.append(entry.getKey() + "=" + val); // see bug6088; values must contain only URL-compliant characters, so any encoding must be done by the caller
}
}
-
return stringBuilder.toString();
}
private Map> toParameterMap(String placeParametersAsToken) {
- List list = Arrays.asList(placeParametersAsToken.split("&"));
-
+ final Map> result;
+ final List list = Arrays.asList(placeParametersAsToken.split("&"));
if (list == null || list.size() < 1) {
logger.warning("Token empty, no-op");
- Collections.emptyMap();
- }
-
- Map> result = new HashMap<>();
- for (String listItem : list) {
- String[] nvPair = listItem.split("=");
- if (nvPair == null || nvPair.length != 2) {
- logger.warning("Invalid parameters");
- continue;
+ result = Collections.emptyMap();
+ } else {
+ result = new HashMap<>();
+ for (String listItem : list) {
+ final int indexOfEquals = listItem.indexOf('='); // don't use "split" because value may contain '=' characters
+ final String key;
+ final String value;
+ if (indexOfEquals < 0) {
+ key = listItem;
+ value = null;
+ } else {
+ key = listItem.substring(0, indexOfEquals);
+ value = listItem.substring(indexOfEquals+1);
+ }
+ Util.addToValueSet(result, key, value); // see bug6088; values are expected to travel safely through URLs; encoding is the caller's responsibility
}
- Util.addToValueSet(result, nvPair[0], nvPair[1]);
}
return result;
}
-
}
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/NavigatorUtil.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/NavigatorUtil.java
index ef82171a6d5..a7e8bd1407c 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/NavigatorUtil.java
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/NavigatorUtil.java
@@ -7,39 +7,44 @@ import com.sap.sse.gwt.client.Notification.NotificationType;
public class NavigatorUtil {
/**
- * Note that this method will check whether the browser does support the copyToClipboard feature first
- * and will display a warning notification if it does not, to prevent JS error messages in the console.
- * Please use {@link clientHasNavigatorCopyToClipboardSupport} to check for browser support,
- * if case specific handling is required.
- * On a successful copy an info notification is displayed.
- * @param String text: Text to share.
+ * Note that this method will check whether the browser does support the copyToClipboard feature first and will
+ * display a warning notification if it does not, to prevent JS error messages in the console. Please use
+ * {@link clientHasNavigatorCopyToClipboardSupport} to check for browser support, if case specific handling is
+ * required. On a successful copy an info notification is displayed.
+ *
+ * @param String
+ * text: Text to share.
*/
public static void copyToClipboard(String text) {
- if(nativeClientHasNavigatorCopyToClipboardSupport()) {
+ if (nativeClientHasNavigatorCopyToClipboardSupport()) {
nativeCopyToClipboard(text);
Notification.notify(StringMessages.INSTANCE.sharingLinkCopied(), NotificationType.INFO);
- }else {
+ } else {
GWT.log("This browser does not support copying to clipboard");
- Notification.notify(StringMessages.INSTANCE.browserDoesNotSupportCopyToClipboard(), NotificationType.WARNING);
+ Notification.notify(StringMessages.INSTANCE.browserDoesNotSupportCopyToClipboard(),
+ NotificationType.WARNING);
}
}
/**
- * Note that this method will check whether the browser does support the sharing feature first
- * and will display a warning notification if it does not, to prevent JS error messages in the console.
- * Please use {@link clientHasNavigatorShareSupport} to check for browser support, if case specific handling is required.
- * @param String url: URL to share.
- * @param String text: Optional text to share.
+ * Note that this method will check whether the browser does support the sharing feature first and will display a
+ * warning notification if it does not, to prevent JS error messages in the console. Please use
+ * {@link clientHasNavigatorShareSupport} to check for browser support, if case specific handling is required.
+ *
+ * @param String
+ * url: URL to share.
+ * @param String
+ * text: Optional text to share.
*/
public static void shareUrl(String url, String text) {
- if(nativeClientHasNavigatorShareSupport()) {
- if(text != null) {
+ if (nativeClientHasNavigatorShareSupport()) {
+ if (text != null) {
nativeShareUrlAndText(url, text);
- }else {
+ } else {
nativeShareUrl(url);
}
- }else {
+ } else {
GWT.log("This browser does not support native sharing");
Notification.notify(StringMessages.INSTANCE.browserDoesNotSupportNativeSharing(), NotificationType.WARNING);
}
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/event/AbstractEventActivityProxy.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/event/AbstractEventActivityProxy.java
index 7be9079e3ec..1da6a830671 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/event/AbstractEventActivityProxy.java
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/event/AbstractEventActivityProxy.java
@@ -1,6 +1,7 @@
package com.sap.sailing.gwt.home.shared.places.event;
import java.util.UUID;
+import java.util.logging.Logger;
import com.google.gwt.activity.shared.Activity;
import com.sap.sailing.gwt.home.communication.event.GetEventViewAction;
@@ -21,7 +22,8 @@ import com.sap.sse.gwt.client.mvp.AbstractActivityProxy;
import com.sap.sse.gwt.client.mvp.ClientFactory;
public abstract class AbstractEventActivityProxy extends AbstractActivityProxy implements ProvidesNavigationPath {
-
+ private static final Logger logger = Logger.getLogger(AbstractEventActivityProxy.class.getName());
+
private final C clientFactory;
private AbstractEventPlace place;
private NavigationPathDisplay navigationPathDisplay;
@@ -55,7 +57,7 @@ public abstract class AbstractEventActivityProxy extends AbstractMapTokenizer {
+ /**
+ * See also {@code TokenizedHomePlaceUrlBuilder.EVENT_ID_PARAM}
+ */
private final static String PARAM_EVENTID = "eventId";
+
+ /**
+ * See also {@code TokenizedHomePlaceUrlBuilder.REGATTA_ID_PARAM}; expected to be a Base64 string that has been URL-encoded,
+ * so that the optional trailing '=' characters are properly encoded as %3D, for example.
+ */
private final static String PARAM_REGATTAID = "regattaId";
protected PLACE getPlaceFromParameters(Map> parameters) {
+ final String encodedRegattaId = extractSingleParameter(parameters, PARAM_REGATTAID);
+ String decodedRegattaId;
+ try {
+ decodedRegattaId = encodedRegattaId==null?null:new String(Base64Utils.fromBase64(encodedRegattaId));
+ } catch (Throwable e) {
+ logger.warning("Error trying to decode regatta ID "+encodedRegattaId+"; trying to use URL decoding to obtain regatta name");
+ decodedRegattaId = URL.decodeQueryString(encodedRegattaId);
+ }
+ // see bug 6088: regatta names may contain any UTF character and therefore need encoding
return getRealPlace(new EventContext().withId(extractSingleParameter(parameters, PARAM_EVENTID))
- .withRegattaId(extractSingleParameter(parameters, PARAM_REGATTAID)), parameters);
+ .withRegattaId(decodedRegattaId), parameters);
}
private String extractSingleParameter(Map> parameters, String key) {
@@ -63,9 +85,9 @@ public abstract class AbstractEventPlace extends Place implements HasLocationTit
Map> parameters = new HashMap<>();
EventContext context = place.getCtx();
Util.addToValueSet(parameters, PARAM_EVENTID, context.getEventId());
- String regattaId = context.getRegattaId();
+ String regattaId = context.getRegattaId(); // bug 6088: we assume that a regatta id/name can contain any UTF character and therefore needs encoding
if (regattaId != null && !regattaId.isEmpty()) {
- Util.addToValueSet(parameters, PARAM_REGATTAID, context.getRegattaId());
+ Util.addToValueSet(parameters, PARAM_REGATTAID, Base64Utils.toBase64(context.getRegattaId().getBytes()));
}
return parameters;
}
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/event/EventContext.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/event/EventContext.java
index 4c10ca45d17..e179004f2ac 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/event/EventContext.java
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/event/EventContext.java
@@ -53,13 +53,13 @@ public class EventContext implements ShareablePlaceContext {
@Override
public String getContextAsPathParameters() {
- if(eventId != null) {
+ if (eventId != null) {
String path = "/events/" + eventId;
- if(regattaId != null) {
- path += "/regattas/" + URLEncoder.encode(regattaId);
+ if (regattaId != null) {
+ path += "/regattas/" + URLEncoder.encodeQueryString(regattaId);
}
return path;
- }else {
+ } else {
return null;
}
}
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java
index 58056ef778d..61c24dc9bda 100755
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java
@@ -480,9 +480,9 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel
@Override
public SafeHtml getValue(LeaderboardGroupDTO group) {
String debugParam = Window.Location.getParameter("gwt.codesvr");
- String link = URLEncoder.encode("/gwt/Spectator.html?leaderboardGroupId=" + group.getId()+"&showRaceDetails=true&"
+ String link = "/gwt/Spectator.html?leaderboardGroupId=" + group.getId()+"&showRaceDetails=true&"
+ RaceBoardPerspectiveOwnSettings.PARAM_CAN_REPLAY_DURING_LIVE_RACES + "=true"
- + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : ""));
+ + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + URLEncoder.encodeQueryString(debugParam) : "");
return ANCHORTEMPLATE.cell(UriUtils.fromString(link), group.getName());
}
};
diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupOverviewPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupOverviewPanel.java
index 7d09bcc6715..0b38a043712 100644
--- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupOverviewPanel.java
+++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/spectator/LeaderboardGroupOverviewPanel.java
@@ -362,10 +362,10 @@ public class LeaderboardGroupOverviewPanel extends FormPanel {
public SafeHtml getValue(StrippedLeaderboardDTO leaderboard) {
LeaderboardGroupDTO selectedGroup = groupsSelectionModel.getSelectedObject();
String debugParam = Window.Location.getParameter("gwt.codesvr");
- String link = URLEncoder.encode("/gwt/Leaderboard.html?name=" + leaderboard.getName()
+ String link = "/gwt/Leaderboard.html?name=" + URLEncoder.encodeQueryString(leaderboard.getName())
+ (showRaceDetails ? "&showRaceDetails=true" : "") + "&root=overview" + "&leaderboardGroupId="
+ selectedGroup.getId().toString()
- + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : ""));
+ + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + URLEncoder.encodeQueryString(debugParam) : "");
return ANCHORTEMPLATE.anchor(UriUtils.fromString(link), leaderboard.getName());
}
};
@@ -455,11 +455,11 @@ public class LeaderboardGroupOverviewPanel extends FormPanel {
StrippedLeaderboardDTO selectedLeaderboard = leaderboardsSelectionModel.getSelectedObject();
RegattaNameAndRaceName raceId = (RegattaNameAndRaceName) race.getRaceIdentifier(fleet);
String debugParam = Window.Location.getParameter("gwt.codesvr");
- String link = URLEncoder.encode(RaceBoardPanel.RACEBOARD_PATH+"?leaderboardName="
- + selectedLeaderboard.getName() + "&raceName=" + raceId.getRaceName() + "®attaName="
- + raceId.getRegattaName() + "&leaderboardGroupId=" + selectedGroup.getId().toString()
+ String link = RaceBoardPanel.RACEBOARD_PATH+"?leaderboardName="
+ + URLEncoder.encodeQueryString(selectedLeaderboard.getName()) + "&raceName=" + URLEncoder.encodeQueryString(raceId.getRaceName()) + "®attaName="
+ + URLEncoder.encodeQueryString(raceId.getRegattaName()) + "&leaderboardGroupId=" + selectedGroup.getId().toString()
+ "&root=overview"
- + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + debugParam : ""));
+ + (debugParam != null && !debugParam.isEmpty() ? "&gwt.codesvr=" + URLEncoder.encodeQueryString(debugParam) : "");
name = ANCHORTEMPLATE.anchor(UriUtils.fromString(link), raceDisplayName);
} else {
name = new SafeHtmlBuilder().appendHtmlConstant(raceDisplayName).toSafeHtml();
diff --git a/java/com.sap.sailing.server.gateway/resources/SharedProxy.html b/java/com.sap.sailing.server.gateway/resources/SharedProxy.html
index be0ee8e32ad..c31fcfbd261 100644
--- a/java/com.sap.sailing.server.gateway/resources/SharedProxy.html
+++ b/java/com.sap.sailing.server.gateway/resources/SharedProxy.html
@@ -11,6 +11,6 @@
${disable_redirect_end}
-
+