Make history token encoding configurable.

Change-Id: Iead6f6196dfe992c3182a3882afb2f08cee78552
diff --git a/user/src/com/google/gwt/user/History.gwt.xml b/user/src/com/google/gwt/user/History.gwt.xml
index 7a003a5..ea649c9 100644
--- a/user/src/com/google/gwt/user/History.gwt.xml
+++ b/user/src/com/google/gwt/user/History.gwt.xml
@@ -20,6 +20,19 @@
   <inherits name="com.google.gwt.core.Core"/>
   <inherits name="com.google.gwt.user.UserAgent"/>
 
+  <!-- Option to disable GWT double encoding of history tokens.               -->
+  <!-- This double encoding is necessary to make History work across          -->
+  <!-- browsers (especially Firefox).                                         -->
+  <!-- If applications choose to disable double encoding they are responsible -->
+  <!-- for ensuring that only tokens are used that work across browsers.      -->
+  <define-property name="history.noDoubleEncoding" values="true,false" />
+  <set-property name="history.noDoubleEncoding" value="false" />
+
+  <replace-with class="com.google.gwt.user.client.History.NoopHistoryTokenEncoder">
+    <when-type-is class="com.google.gwt.user.client.History.HistoryTokenEncoder" />
+    <when-property-is name="history.noDoubleEncoding" value="true" />
+  </replace-with>
+
   <!-- IE8 does not work with the standard implementation -->
   <replace-with class="com.google.gwt.user.client.History.HistoryImplIE8">
     <when-type-is class="com.google.gwt.user.client.History.HistoryImpl" />
diff --git a/user/src/com/google/gwt/user/client/History.java b/user/src/com/google/gwt/user/client/History.java
index b9d675f..5f3a167 100644
--- a/user/src/com/google/gwt/user/client/History.java
+++ b/user/src/com/google/gwt/user/client/History.java
@@ -83,6 +83,38 @@
   }
 
   /**
+   * HistoryTokenEncoder is responsible for encoding and decoding history token,
+   * thus ensuring that tokens are safe to use in the browsers URL.
+   */
+  private static class HistoryTokenEncoder {
+    public native String encode(String toEncode) /*-{
+      // encodeURI() does *not* encode the '#' character.
+      return $wnd.encodeURI(toEncode).replace("#", "%23");
+    }-*/;
+
+    public native String decode(String toDecode) /*-{
+      return $wnd.decodeURI(toDecode.replace("%23", "#"));
+    }-*/;
+  }
+
+  /**
+   * NoopHistoryTokenEncoder does not perform any encoding.
+   */
+  // Used from rebinding
+  @SuppressWarnings("unused")
+  private static class NoopHistoryTokenEncoder extends HistoryTokenEncoder {
+    @Override
+    public String encode(String toEncode) {
+      return toEncode;
+    }
+
+    @Override
+    public String decode(String toDecode) {
+      return toDecode;
+    }
+  }
+
+  /**
    * History implementation using hash tokens.
    * <p>This is the default implementation for all browsers except IE8.
    */
@@ -107,17 +139,6 @@
     public void replaceToken(String historyToken) {
       Window.Location.replace("#" + historyToken);
     }
-
-    // Only kept in deferred binding to allow mocking frameworks to intercept calls
-    public native String decodeHistoryToken(String historyToken) /*-{
-      return $wnd.decodeURI(historyToken.replace("%23", "#"));
-    }-*/;
-
-    // Only kept in deferred binding to allow mocking frameworks to intercept calls
-    public native String encodeHistoryToken(String historyToken) /*-{
-      // encodeURI() does *not* encode the '#' character.
-      return $wnd.encodeURI(historyToken).replace("#", "%23");
-    }-*/;
   }
 
   /**
@@ -177,6 +198,7 @@
 
   private static HistoryImpl impl = GWT.create(HistoryImpl.class);
   private static HistoryEventSource historyEventSource = new HistoryEventSource();
+  private static HistoryTokenEncoder tokenEncoder = GWT.create(HistoryTokenEncoder.class);
   private static String token = getDecodedHash();
 
   /**
@@ -216,7 +238,7 @@
    * @return the encoded token, suitable for use as part of a URI
    */
   public static String encodeHistoryToken(String historyToken) {
-    return impl.encodeHistoryToken(historyToken);
+    return tokenEncoder.encode(historyToken);
   }
 
   /**
@@ -360,7 +382,7 @@
     if (hashToken == null || hashToken.isEmpty()) {
       return "";
     }
-    return  impl.decodeHistoryToken(hashToken.substring(1));
+    return tokenEncoder.decode(hashToken.substring(1));
   }
 
   // this is called from JS when the native onhashchange occurs
diff --git a/user/test/com/google/gwt/user/HistoryTestNoopTokenEncoder.gwt.xml b/user/test/com/google/gwt/user/HistoryTestNoopTokenEncoder.gwt.xml
new file mode 100644
index 0000000..edc6b29
--- /dev/null
+++ b/user/test/com/google/gwt/user/HistoryTestNoopTokenEncoder.gwt.xml
@@ -0,0 +1,18 @@
+<!--                                                                        -->
+<!-- Copyright 2014 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module type="fileset">
+  <inherits name="com.google.gwt.user.User"/>
+  <set-property name="history.noDoubleEncoding" value="true" />
+</module>
diff --git a/user/test/com/google/gwt/user/UiPart2Suite.java b/user/test/com/google/gwt/user/UiPart2Suite.java
index 8ba4117..05ff391 100644
--- a/user/test/com/google/gwt/user/UiPart2Suite.java
+++ b/user/test/com/google/gwt/user/UiPart2Suite.java
@@ -21,6 +21,7 @@
 import com.google.gwt.user.client.ui.HeaderPanelTest;
 import com.google.gwt.user.client.ui.HiddenTest;
 import com.google.gwt.user.client.ui.HistoryTest;
+import com.google.gwt.user.client.ui.HistoryTestNoopTokenEncoder;
 import com.google.gwt.user.client.ui.HorizontalPanelTest;
 import com.google.gwt.user.client.ui.HorizontalSplitPanelTest;
 import com.google.gwt.user.client.ui.HyperlinkTest;
@@ -85,6 +86,7 @@
     suite.addTestSuite(HeaderPanelTest.class);
     suite.addTestSuite(HiddenTest.class);
     suite.addTestSuite(HistoryTest.class);
+    suite.addTestSuite(HistoryTestNoopTokenEncoder.class);
     suite.addTestSuite(HorizontalPanelTest.class);
     suite.addTestSuite(HorizontalSplitPanelTest.class);
     suite.addTestSuite(HTMLPanelTest.class);
diff --git a/user/test/com/google/gwt/user/client/ui/HistoryTest.java b/user/test/com/google/gwt/user/client/ui/HistoryTest.java
index 9093f4a..c3985e9 100644
--- a/user/test/com/google/gwt/user/client/ui/HistoryTest.java
+++ b/user/test/com/google/gwt/user/client/ui/HistoryTest.java
@@ -56,6 +56,14 @@
     return "com.google.gwt.user.User";
   }
 
+  protected String getHistoryToken2() {
+    return "token 2";
+  }
+
+  protected String getHistoryToken2_encoded() {
+    return "token%202";
+  }
+
   // TODO(dankurka): Fix up HTML unit hash change handling
   @DoNotRunWith(Platform.HtmlUnitUnknown)
   public void testClickLink() {
@@ -176,8 +184,8 @@
      */
     History.newItem("if-you-see-this-then-history-went-back-too-far");
 
-    final String historyToken1 = "token 1";
-    final String historyToken2 = "token 2";
+    final String historyToken1 = "token1";
+    final String historyToken2 = getHistoryToken2();
     delayTestFinish(10000);
 
     addHistoryListenerImpl(new ValueChangeHandler<String>() {
@@ -243,12 +251,11 @@
       }
     };
     addHistoryListenerImpl(new ValueChangeHandler<String>() {
-      final ArrayList<Object> counter = new ArrayList<Object>();
+      private int count = 0;
 
       @Override
       public void onValueChange(ValueChangeEvent<String> event) {
-        counter.add(null);
-        if (counter.size() != 1) {
+        if (count++ != 0) {
           fail("onHistoryChanged called multiple times");
         }
         // wait 500ms to see if we get called multiple times
@@ -268,9 +275,9 @@
      */
     History.newItem("if-you-see-this-then-history-went-back-too-far");
 
-    final String historyToken1 = "token 1";
-    final String historyToken2 = "token 2";
-    final String historyToken3 = "token 3";
+    final String historyToken1 = "token1";
+    final String historyToken2 = getHistoryToken2();
+    final String historyToken3 = "token3";
 
     delayTestFinish(10000);
 
@@ -337,9 +344,10 @@
      * app containing our test module.
      */
     History.newItem("if-you-see-this-then-history-went-back-too-far");
-    final String historyToken1 = "token 1";
-    final String historyToken2 = "token 2";
-    final String historyToken2_encoded = "token%202";
+
+    final String historyToken1 = "token1";
+    final String historyToken2 = getHistoryToken2();
+    final String historyToken2_encoded = getHistoryToken2_encoded();
 
     History.newItem(historyToken1);
 
diff --git a/user/test/com/google/gwt/user/client/ui/HistoryTestNoopTokenEncoder.java b/user/test/com/google/gwt/user/client/ui/HistoryTestNoopTokenEncoder.java
new file mode 100644
index 0000000..0e54ae5
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/HistoryTestNoopTokenEncoder.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+/**
+ * Tests for the history system without encoding of history tokens.
+ */
+public class HistoryTestNoopTokenEncoder extends HistoryTest {
+
+  private static native boolean isFirefox() /*-{
+    var ua = navigator.userAgent.toLowerCase();
+    var docMode = $doc.documentMode;
+    return (ua.indexOf('gecko') != -1 && typeof(docMode) == 'undefined');
+  }-*/;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.HistoryTestNoopTokenEncoder";
+  }
+
+  protected String getHistoryToken2() {
+    return isFirefox() ? "token2" : "token 2";
+  }
+
+  protected String getHistoryToken2_encoded() {
+    return isFirefox() ? "token2" : "token%202";
+  }
+
+  public void testTokenEscaping() {
+    if (isFirefox()) {
+      return; // encoding is broken for Firefox.
+    }
+    super.testTokenEscaping();
+  }
+}