Fixes issues 1549 and 1949: fixes and standardizes which characters get escaped
when they're used in history tokens. We were previously being overzealous by
using escapeURIComponent.

patch by: ajr and jgw
reviewed by: jgw



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2791 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/impl/HistoryImpl.java b/user/src/com/google/gwt/user/client/impl/HistoryImpl.java
index 7feb616..7d5ca88 100644
--- a/user/src/com/google/gwt/user/client/impl/HistoryImpl.java
+++ b/user/src/com/google/gwt/user/client/impl/HistoryImpl.java
@@ -35,6 +35,16 @@
 
   public abstract void newItem(String historyToken);
 
+  protected native String decodeFragment(String encodedFragment) /*-{  
+    // decodeURI() does *not* decode the '#' character.
+    return decodeURI(encodedFragment.replace("%23", "#"));
+  }-*/;
+
+  protected native String encodeFragment(String fragment) /*-{
+    // encodeURI() does *not* encode the '#' character.
+    return encodeURI(fragment).replace("#", "%23");
+  }-*/;
+
   protected native void setToken(String token) /*-{
     $wnd.__gwt_historyToken = token;
   }-*/;
diff --git a/user/src/com/google/gwt/user/client/impl/HistoryImplIE6.java b/user/src/com/google/gwt/user/client/impl/HistoryImplIE6.java
index 4b0140f..14343f8 100644
--- a/user/src/com/google/gwt/user/client/impl/HistoryImplIE6.java
+++ b/user/src/com/google/gwt/user/client/impl/HistoryImplIE6.java
@@ -49,7 +49,7 @@
       if (hash.length > 0) {
         var token = '';
         try {
-          token = decodeURIComponent(hash.substring(1));
+          token = this.@com.google.gwt.user.client.impl.HistoryImpl::decodeFragment(Ljava/lang/String;)(hash.substring(1));
         } catch (e) {
           // If there's a bad hash, always reload. This could only happen if
           // if someone entered or linked to a bad url.
@@ -85,7 +85,7 @@
     var hash = $wnd.location.hash;
     if (hash.length > 0) {
       try {
-        $wnd.__gwt_historyToken = decodeURIComponent(hash.substring(1));
+        $wnd.__gwt_historyToken = this.@com.google.gwt.user.client.impl.HistoryImpl::decodeFragment(Ljava/lang/String;)(hash.substring(1));
       } catch (e) {
         // Clear the bad hash and __gwt_historyToken
         // (this can't have been a valid token).
@@ -101,12 +101,16 @@
 
   @Override
   protected native void injectGlobalHandler() /*-{
+    var historyImplRef = this;
+    
     $wnd.__gwt_onHistoryLoad = function(token) {
       // Change the URL and notify the application that its history frame
       // is changing.
+      
       if (token != $wnd.__gwt_historyToken) {
         $wnd.__gwt_historyToken = token;
-        $wnd.location.hash = encodeURIComponent(token);
+        $wnd.location.hash = historyImplRef.@com.google.gwt.user.client.impl.HistoryImpl::encodeFragment(Ljava/lang/String;)(token);
+        
         @com.google.gwt.user.client.impl.HistoryImpl::onHistoryChanged(Ljava/lang/String;)(token);
       }
     };
diff --git a/user/src/com/google/gwt/user/client/impl/HistoryImplMozilla.java b/user/src/com/google/gwt/user/client/impl/HistoryImplMozilla.java
index 2dc7830..3f1a189 100644
--- a/user/src/com/google/gwt/user/client/impl/HistoryImplMozilla.java
+++ b/user/src/com/google/gwt/user/client/impl/HistoryImplMozilla.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2007 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
@@ -19,10 +19,9 @@
  * History implementation for Mozilla-based browsers.
  */
 class HistoryImplMozilla extends HistoryImplStandard {
- 
+
   @Override
   public native void newItem(String historyToken) /*-{
-
     // When the historyToken is blank or null, we are not able to set
     // $wnd.location.hash to the empty string, due to a bug in Mozilla.
     // Every time $wnd.location.hash is set to the empty string, one of the
@@ -39,7 +38,14 @@
 
       $wnd.location = s + '#';
     } else {
-      $wnd.location.hash = encodeURIComponent(historyToken);
+      $wnd.location.hash = this.@com.google.gwt.user.client.impl.HistoryImpl::encodeFragment(Ljava/lang/String;)(historyToken);
     }
   }-*/;
+
+  @Override
+  protected String decodeFragment(String encodedFragment) {
+    // Mozilla browsers pre-decode the result of location.hash, so there's no
+    // need to decode it again (which would in fact be incorrect).
+    return encodedFragment;
+  }
 }
diff --git a/user/src/com/google/gwt/user/client/impl/HistoryImplSafari.java b/user/src/com/google/gwt/user/client/impl/HistoryImplSafari.java
index d30582d..81b2325 100644
--- a/user/src/com/google/gwt/user/client/impl/HistoryImplSafari.java
+++ b/user/src/com/google/gwt/user/client/impl/HistoryImplSafari.java
@@ -24,9 +24,9 @@
  */
 class HistoryImplSafari extends HistoryImplStandard {
 
-  private static boolean isOldSafari = detectOldSafari();
+  static boolean isOldSafari = detectOldSafari();
 
-  private static native boolean detectOldSafari() /*-{
+  static native boolean detectOldSafari() /*-{
     var exp = / AppleWebKit\/([\d]+)/;
     var result = exp.exec(navigator.userAgent);
     if (result) {
@@ -66,17 +66,6 @@
     super.newItem(historyToken);
   }
 
-  protected native String decode(String historyToken) /*-{
-    try {
-      // Unlike most other browsers, Safari does not unescape location.hash.
-      return decodeURIComponent(historyToken);
-    } catch (e) {
-      // This is very unlikely to be malformed, since it comes directly from
-      // location.hash.
-      return "";
-    }
-  }-*/;
-
   private native void initImpl() /*-{
     $wnd.__gwt_historyToken = '';
 
@@ -84,7 +73,7 @@
     var hash = $wnd.location.hash;
     if (hash.length > 0) {
       $wnd.__gwt_historyToken =
-        this.@com.google.gwt.user.client.impl.HistoryImplStandard::decode(Ljava/lang/String;)(hash.substring(1));
+        this.@com.google.gwt.user.client.impl.HistoryImpl::decodeFragment(Ljava/lang/String;)(hash.substring(1));
     }
 
     @com.google.gwt.user.client.impl.HistoryImpl::onHistoryChanged(Ljava/lang/String;)($wnd.__gwt_historyToken);
@@ -96,7 +85,7 @@
     var meta = $doc.createElement('meta');
     meta.setAttribute('http-equiv','refresh');
 
-    var newUrl = $wnd.location.href.split('#')[0] + '#' + encodeURIComponent(historyToken);
+    var newUrl = $wnd.location.href.split('#')[0] + '#' + this.@com.google.gwt.user.client.impl.HistoryImpl::encodeFragment(Ljava/lang/String;)(historyToken);
     meta.setAttribute('content','0.01;url=' + newUrl);
 
     $doc.body.appendChild(meta);
diff --git a/user/src/com/google/gwt/user/client/impl/HistoryImplStandard.java b/user/src/com/google/gwt/user/client/impl/HistoryImplStandard.java
index bac0dff..e5fa7e6 100644
--- a/user/src/com/google/gwt/user/client/impl/HistoryImplStandard.java
+++ b/user/src/com/google/gwt/user/client/impl/HistoryImplStandard.java
@@ -34,9 +34,7 @@
     $wnd.__checkHistory = function() {
       var token = '', hash = $wnd.location.hash;
       if (hash.length > 0) {
-        // Not all browsers decode location.hash the same way, so the
-        // implementation needs an opportunity to handle decoding.
-        token = historyImpl.@com.google.gwt.user.client.impl.HistoryImplStandard::decode(Ljava/lang/String;)(hash.substring(1));
+        token = historyImpl.@com.google.gwt.user.client.impl.HistoryImpl::decodeFragment(Ljava/lang/String;)(hash.substring(1));
       }
 
       if (token != $wnd.__gwt_historyToken) {
@@ -49,7 +47,6 @@
 
     // Kick off the timer.
     $wnd.__checkHistory();
-
     return true;
   }-*/;
 
@@ -58,10 +55,6 @@
     if (historyToken == null) {
       historyToken = "";
     }
-    $wnd.location.hash = encodeURIComponent(historyToken);
+    $wnd.location.hash = this.@com.google.gwt.user.client.impl.HistoryImpl::encodeFragment(Ljava/lang/String;)(historyToken);
   }-*/;
-
-  protected String decode(String historyToken) {
-    return historyToken;
-  }
 }
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 4835212..2c5c17b 100644
--- a/user/test/com/google/gwt/user/client/ui/HistoryTest.java
+++ b/user/test/com/google/gwt/user/client/ui/HistoryTest.java
@@ -30,9 +30,54 @@
     return "com.google.gwt.user.User";
   }
 
-  /* Tests against issue #572: Double unescaping of history tokens. */
+  private static native String getCurrentLocationHash() /*-{
+    var href = $wnd.location.href;
+    
+    splitted = href.split("#");
+    if (splitted.length != 2) {
+      return null;
+    }
+    
+    hashPortion = splitted[1];
+    
+    return hashPortion;
+  }-*/;
+  
   public void testTokenEscaping() {
+    final String shouldBeEncoded = "% ^[]|\"<>{}\\`";
+    final String shouldBeEncodedAs = "%25%20%5E%5B%5D%7C%22%3C%3E%7B%7D%5C%60";
+    
+    delayTestFinish(5000);
+    History.addHistoryListener(new HistoryListener() {
+      public void onHistoryChanged(String token) {
+        assertEquals(shouldBeEncodedAs, getCurrentLocationHash());
+        assertEquals(shouldBeEncoded, token);
+        finishTest();
+        History.removeHistoryListener(this);
+      }
+    });
+    History.newItem(shouldBeEncoded);
+  }
+  
+  public void testTokenNonescaping() {
+    final String shouldNotChange = "abc;,/?:@&=+$-_.!~*'()ABC123foo";
+    
+    delayTestFinish(5000);
+    History.addHistoryListener(new HistoryListener() {
+      public void onHistoryChanged(String token) {
+        assertEquals(shouldNotChange, getCurrentLocationHash());
+        assertEquals(shouldNotChange, token);
+        finishTest();
+        History.removeHistoryListener(this);
+      }
+    });
+    History.newItem(shouldNotChange);
+  }
+
+  /* Tests against issue #572: Double unescaping of history tokens. */
+  public void testDoubleEscaping() {
     final String escToken = "%24%24%24";
+
     delayTestFinish(5000);
     History.addHistoryListener(new HistoryListener() {
       public void onHistoryChanged(String token) {