Escape single characters in SafeHtmlBuilder/SafeHtmlUtils (external issue 6222)

Review at http://gwt-code-reviews.appspot.com/1413802


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9979 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeHtmlBuilder.java b/user/src/com/google/gwt/safehtml/shared/SafeHtmlBuilder.java
index b906d93..34b14de 100644
--- a/user/src/com/google/gwt/safehtml/shared/SafeHtmlBuilder.java
+++ b/user/src/com/google/gwt/safehtml/shared/SafeHtmlBuilder.java
@@ -80,9 +80,10 @@
    *
    * @param c the character whose string representation to append
    * @return a reference to this object
+   * @see SafeHtmlUtils#htmlEscape(char)
    */
   public SafeHtmlBuilder append(char c) {
-    sb.append(c);
+    sb.append(SafeHtmlUtils.htmlEscape(c));
     return this;
   }
 
@@ -147,6 +148,7 @@
    *
    * @param text the string to append
    * @return a reference to this object
+   * @see SafeHtmlUtils#htmlEscape(String)
    */
   public SafeHtmlBuilder appendEscaped(String text) {
     sb.append(SafeHtmlUtils.htmlEscape(text));
@@ -156,10 +158,11 @@
   /**
    * Appends a string consisting of several newline-separated lines after
    * HTML-escaping it. Newlines in the original string are converted to {@code
-   * <br>}.
+   * <br>} tags.
    *
    * @param text the string to append
    * @return a reference to this object
+   * @see SafeHtmlUtils#htmlEscape(String)
    */
   public SafeHtmlBuilder appendEscapedLines(String text) {
     sb.append(SafeHtmlUtils.htmlEscape(text).replaceAll("\n", "<br>"));
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java b/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
index 48b202b..1ea9c38 100644
--- a/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
+++ b/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
@@ -37,7 +37,7 @@
   private static final RegExp QUOT_RE = RegExp.compile("\"", "g");
 
   /**
-   * Returns a SafeHtml constructed from a safe string, i.e., without escaping
+   * Returns a {@link SafeHtml} constructed from a safe string, i.e., without escaping
    * the string.
    *
    * <p>
@@ -85,7 +85,7 @@
    * Returns a {@link SafeHtml} containing the escaped string.
    *
    * @param s the input String
-   * @return a SafeHtml instance
+   * @return a {@link SafeHtml} instance
    */
   public static SafeHtml fromString(String s) {
     return new SafeHtmlString(htmlEscape(s));
@@ -94,24 +94,59 @@
   /**
    * Returns a {@link SafeHtml} constructed from a trusted string, i.e., without
    * escaping the string. No checks are performed. The calling code should be
-   * carefully reviewed to ensure the argument meets the SafeHtml contract.
+   * carefully reviewed to ensure the argument meets the {@link SafeHtml} contract.
    *
    * @param s the input String
-   * @return a SafeHtml instance
+   * @return a {@link SafeHtml} instance
    */
   public static SafeHtml fromTrustedString(String s) {
     return new SafeHtmlString(s);
   }
 
   /**
+   * HTML-escapes a character.  HTML meta characters
+   * will be escaped as follows:
+   *
+   * <pre>
+   * &amp; - &amp;amp;
+   * &lt; - &amp;lt;
+   * &gt; - &amp;gt;
+   * &quot; - &amp;quot;
+   * &#39; - &amp;#39;
+   * </pre>
+   *
+   * @param c the character to be escaped
+   * @return a string containing either the input character
+   *     or an equivalent HTML Entity Reference
+   */
+  public static String htmlEscape(char c) {
+    switch (c) {
+      case '&':
+        return "&amp;";
+      case '<':
+        return "&lt;";
+      case '>':
+        return "&gt;";
+      case '"':
+        return "&quot;";
+      case '\'':
+        return "&#39;";
+      default:
+        return "" + c;
+    }
+  }
+
+  /**
    * HTML-escapes a string.
    *
    * Note: The following variants of this function were profiled on FF36,
    * Chrome6, IE8:
-   * #1) for each case, check indexOf, then use s.replace(regex, string)
-   * #2) for each case, check indexOf, then use s.replaceAll()
-   * #3) check if any metachar is present using a regex, then use #1
-   * #4) for each case, use s.replace(regex, string)
+   * <ol>
+   * <li>For each case, check indexOf, then use s.replace(regex, string)</li>
+   * <li>For each case, check indexOf, then use s.replaceAll()</li>
+   * <li>Check if any metachar is present using a regex, then use #1</li>
+   * <li>For each case, use s.replace(regex, string)</li>
+   * </ol>
    *
    * #1 was found to be the fastest, and is used below.
    *
diff --git a/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlBuilderTest.java b/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlBuilderTest.java
index 1704e51..354ee5d 100644
--- a/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlBuilderTest.java
+++ b/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlBuilderTest.java
@@ -70,6 +70,24 @@
     }
   }
 
+  public void testAppendChars() {
+    SafeHtmlBuilder b = new SafeHtmlBuilder();
+    b.append('a');
+    b.append('&');
+    b.append('b');
+    b.append('<');
+    b.append('c');
+    b.append('>');
+    b.append('d');
+    b.append('"');
+    b.append('e');
+    b.append('\'');
+    b.append('f');
+
+    SafeHtml html = b.toSafeHtml();
+    assertEquals("a&amp;b&lt;c&gt;d&quot;e&#39;f", html.asString());
+  }
+
   @Override
   public String getModuleName() {
     return "com.google.gwt.safehtml.SafeHtmlTestsModule";
diff --git a/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlUtilsTest.java b/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlUtilsTest.java
index fd1d648..e00d5fc 100644
--- a/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlUtilsTest.java
+++ b/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlUtilsTest.java
@@ -101,6 +101,26 @@
     assertEquals(SafeHtmlUtils.htmlEscape(CONSTANT_HTML), h.asString());
   }
 
+  public void testEscape_chars() {
+    String escaped = SafeHtmlUtils.htmlEscape('a');
+    assertEquals("a", escaped);
+
+    escaped = SafeHtmlUtils.htmlEscape('&');
+    assertEquals("&amp;", escaped);
+
+    escaped = SafeHtmlUtils.htmlEscape('<');
+    assertEquals("&lt;", escaped);
+
+    escaped = SafeHtmlUtils.htmlEscape('>');
+    assertEquals("&gt;", escaped);
+
+    escaped = SafeHtmlUtils.htmlEscape('"');
+    assertEquals("&quot;", escaped);
+
+    escaped = SafeHtmlUtils.htmlEscape('\'');
+    assertEquals("&#39;", escaped);
+  }
+
   @Override
   public String getModuleName() {
     return "com.google.gwt.safehtml.SafeHtmlTestsModule";