You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2022/12/28 20:48:36 UTC
[freemarker] 02/02: Improved StringUtil.jsStringEnc to support both quotation marks and apostrophe. Added a mode that is both compatible with JSON and JavaScript, without decreasing safety when used in JavaScript.
This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git
commit e5fa45f4c589b306b5a8ee8c5a42229d378eb607
Author: ddekany <dd...@apache.org>
AuthorDate: Wed Dec 28 21:46:24 2022 +0100
Improved StringUtil.jsStringEnc to support both quotation marks and apostrophe. Added a mode that is both compatible with JSON and JavaScript, without decreasing safety when used in JavaScript.
---
.../freemarker/core/AbstractLegacyCFormat.java | 5 +-
src/main/java/freemarker/core/JSONCFormat.java | 4 +-
.../java/freemarker/core/JavaScriptCFormat.java | 4 +-
.../freemarker/template/utility/StringUtil.java | 117 ++++++++++++++++-----
src/manual/en_US/book.xml | 8 ++
.../template/utility/StringUtilTest.java | 77 +++++++++++++-
6 files changed, 183 insertions(+), 32 deletions(-)
diff --git a/src/main/java/freemarker/core/AbstractLegacyCFormat.java b/src/main/java/freemarker/core/AbstractLegacyCFormat.java
index 99440684..bdf7577d 100644
--- a/src/main/java/freemarker/core/AbstractLegacyCFormat.java
+++ b/src/main/java/freemarker/core/AbstractLegacyCFormat.java
@@ -25,6 +25,8 @@ import freemarker.template.TemplateException;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.utility.StringUtil;
+import freemarker.template.utility.StringUtil.JsStringEncCompatibility;
+import freemarker.template.utility.StringUtil.JsStringEncQuotation;
/**
* Super class of {@link CFormat}-s that merely exist to mimic old {@code ?c} behavior for backward compatibility.
@@ -43,7 +45,8 @@ public abstract class AbstractLegacyCFormat extends CFormat {
@Override
final String formatString(String s, Environment env) throws TemplateException {
- return StringUtil.jsStringEnc(s, true, true);
+ return StringUtil.jsStringEnc(
+ s, JsStringEncCompatibility.JAVA_SCRIPT_OR_JSON, JsStringEncQuotation.QUOTATION_MARK);
}
@Override
diff --git a/src/main/java/freemarker/core/JSONCFormat.java b/src/main/java/freemarker/core/JSONCFormat.java
index 5cf4c52a..ee469941 100644
--- a/src/main/java/freemarker/core/JSONCFormat.java
+++ b/src/main/java/freemarker/core/JSONCFormat.java
@@ -23,6 +23,8 @@ import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import freemarker.template.Version;
import freemarker.template.utility.StringUtil;
+import freemarker.template.utility.StringUtil.JsStringEncCompatibility;
+import freemarker.template.utility.StringUtil.JsStringEncQuotation;
/**
* JSON {@link CFormat}; when this is used, values output by {@code ?c} are valid JSON values, and therefore also
@@ -46,7 +48,7 @@ public final class JSONCFormat extends AbstractJSONLikeFormat {
@Override
String formatString(String s, Environment env) throws TemplateException {
- return StringUtil.jsStringEnc(s, true, true);
+ return StringUtil.jsStringEnc(s, JsStringEncCompatibility.JSON, JsStringEncQuotation.QUOTATION_MARK);
}
@Override
diff --git a/src/main/java/freemarker/core/JavaScriptCFormat.java b/src/main/java/freemarker/core/JavaScriptCFormat.java
index d9ba7229..1cb44adf 100644
--- a/src/main/java/freemarker/core/JavaScriptCFormat.java
+++ b/src/main/java/freemarker/core/JavaScriptCFormat.java
@@ -21,6 +21,8 @@ package freemarker.core;
import freemarker.template.TemplateException;
import freemarker.template.utility.StringUtil;
+import freemarker.template.utility.StringUtil.JsStringEncCompatibility;
+import freemarker.template.utility.StringUtil.JsStringEncQuotation;
/**
* JavaScript {@link CFormat}. This is almost the same as {@link JSONCFormat}, but it uses shorter forms where
@@ -41,7 +43,7 @@ public final class JavaScriptCFormat extends AbstractJSONLikeFormat {
@Override
String formatString(String s, Environment env) throws TemplateException {
- return StringUtil.jsStringEnc(s, false, true);
+ return StringUtil.jsStringEnc(s, JsStringEncCompatibility.JAVA_SCRIPT, JsStringEncQuotation.QUOTATION_MARK);
}
@Override
diff --git a/src/main/java/freemarker/template/utility/StringUtil.java b/src/main/java/freemarker/template/utility/StringUtil.java
index 0d76a48f..51f22a73 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -19,6 +19,8 @@
package freemarker.template.utility;
+import static freemarker.template.utility.StringUtil.JsStringEncQuotation.*;
+
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
@@ -1320,7 +1322,7 @@ public class StringUtil {
/**
* Escapes a {@link String} to be safely insertable into a JavaScript string literal; for more see
- * {@link #jsStringEnc(String, boolean, boolean) jsStringEnc(s, false, false)}.
+ * {@link #jsStringEnc(String, JsStringEncCompatibility, JsStringEncQuotation) jsStringEnc(s, false, QuotationMode.NONE)}.
*/
public static String javaScriptStringEnc(String s) {
return jsStringEnc(s, false);
@@ -1340,12 +1342,12 @@ public class StringUtil {
/**
* Escapes a {@link String} to be safely insertable into a JSON or JavaScript string literal; for more see
- * {@link #jsStringEnc(String, boolean, boolean) jsStringEnc(s, json, false)}.
+ * {@link #jsStringEnc(String, JsStringEncCompatibility, JsStringEncQuotation) jsStringEnc(s, json, QuotationMode.NONE)}.
*
* @since 2.3.20
*/
public static String jsStringEnc(String s, boolean json) {
- return jsStringEnc(s, json, false);
+ return jsStringEnc(s, json ? JsStringEncCompatibility.JSON : JsStringEncCompatibility.JAVA_SCRIPT, null);
}
/**
@@ -1399,22 +1401,24 @@ public class StringUtil {
* </table>
*
* @param s The string to escape
- * @param json If escaping should restrict itself to rules that are valid in both JSON and JavaScript.
- * @param quote If quotation marks should be added around the result.
- * Currently, it's always ({@code "}, not {@code '}).
+ * @param compatibility If escaping should restrict itself to rules that are valid in JSON, in JavaScript, or in both.
+ * @param quotation In not {@code null}, quotation marks of this type are added around the value.
*
* @since 2.3.32
*/
- public static String jsStringEnc(String s, boolean json, boolean quote) {
+ public static String jsStringEnc(String s, JsStringEncCompatibility compatibility, JsStringEncQuotation quotation) {
NullArgumentException.check("s", s);
int ln = s.length();
StringBuilder sb;
- if (quote) {
- sb = new StringBuilder(ln + 8);
- sb.append('"');
- } else {
+ if (quotation == null) {
sb = null;
+ } else {
+ if (quotation == APOSTROPHE && compatibility.jsonCompatible) {
+ throw new IllegalArgumentException("JSON compatible mode doesn't allow quotationMode=" + quotation);
+ }
+ sb = new StringBuilder(ln + 8);
+ sb.append(quotation.getSymbol());
}
for (int i = 0; i < ln; i++) {
final char c = s.charAt(i);
@@ -1435,19 +1439,23 @@ public class StringUtil {
escapeType = ESC_HEXA;
}
} else if (c == '"') {
- escapeType = ESC_BACKSLASH;
+ escapeType = quotation == APOSTROPHE ? NO_ESC : ESC_BACKSLASH;
} else if (c == '\'') {
- escapeType = json || quote ? NO_ESC : ESC_BACKSLASH;
+ escapeType = !compatibility.javaScriptCompatible || quotation == QUOTATION_MARK ? NO_ESC
+ : (compatibility.jsonCompatible ? ESC_HEXA : ESC_BACKSLASH);
} else if (c == '\\') {
escapeType = ESC_BACKSLASH;
- } else if (c == '/' && (i == 0 || s.charAt(i - 1) == '<')) { // against closing elements
- escapeType = quote ? NO_ESC : ESC_BACKSLASH;
- } else if (c == '>') { // against "]]> and "-->"
+ } else if (c == '/'
+ && (i == 0 && quotation == null || i != 0 && s.charAt(i - 1) == '<')) {
+ // against closing elements with "</"
+ escapeType = ESC_BACKSLASH;
+ } else if (c == '>') {
+ // against "]]> and "-->"
final boolean dangerous;
- if (i == 0) {
- dangerous = true;
- } else if (quote) {
+ if (quotation != null && i < 2) {
dangerous = false;
+ } else if (i == 0) {
+ dangerous = true;
} else {
final char prevC = s.charAt(i - 1);
if (prevC == ']' || prevC == '-') {
@@ -1461,13 +1469,12 @@ public class StringUtil {
dangerous = false;
}
}
- escapeType = dangerous ? (json ? ESC_HEXA : ESC_BACKSLASH) : NO_ESC;
- } else if (c == '<') { // against "<!"
+ escapeType = dangerous ? (compatibility.jsonCompatible ? ESC_HEXA : ESC_BACKSLASH) : NO_ESC;
+ } else if (c == '<') {
+ // against "<!"
final boolean dangerous;
if (i == ln - 1) {
- dangerous = true;
- } else if (quote) {
- dangerous = false;
+ dangerous = quotation == null;
} else {
char nextC = s.charAt(i + 1);
dangerous = nextC == '!' || nextC == '?';
@@ -1491,7 +1498,7 @@ public class StringUtil {
if (escapeType > 0x20) {
sb.append((char) escapeType);
} else if (escapeType == ESC_HEXA) {
- if (!json && c < 0x100) {
+ if (!compatibility.jsonCompatible && c < 0x100) {
sb.append('x');
sb.append(toHexDigitUpperCase(c >> 4));
sb.append(toHexDigitUpperCase(c & 0xF));
@@ -1515,8 +1522,8 @@ public class StringUtil {
if (sb != null) sb.append(c);
} // for each characters
- if (quote) {
- sb.append('"');
+ if (quotation != null) {
+ sb.append(quotation.getSymbol());
}
return sb == null ? s : sb.toString();
@@ -2168,5 +2175,59 @@ public class StringUtil {
}
return sb.toString();
}
-
+
+ /**
+ * Used as the argument of {@link StringUtil#jsStringEnc(String, JsStringEncCompatibility, JsStringEncQuotation)}.
+ *
+ * @since 2.3.32
+ */
+ public enum JsStringEncCompatibility {
+ /**
+ * Output is expected to be used in JavaScript, not in JSON.
+ */
+ JAVA_SCRIPT(true, false),
+
+ /**
+ * Output is expected to be used in JSON, not in JavaScript. While JSON is compatible with JavaScript, in this
+ * mode we don't care about escaping apostrophe, as it's not special in JSON.
+ */
+ JSON(false, true),
+
+ /**
+ * Output is expected to be used both in JSON and JavaScript.
+ */
+ JAVA_SCRIPT_OR_JSON(true, true);
+
+ JsStringEncCompatibility(boolean javaScriptCompatible, boolean jsonCompatible) {
+ this.javaScriptCompatible = javaScriptCompatible;
+ this.jsonCompatible = jsonCompatible;
+ }
+
+ private final boolean javaScriptCompatible;
+ private final boolean jsonCompatible;
+
+ boolean isJSONCompatible() {
+ return jsonCompatible;
+ }
+ }
+
+ /**
+ * Used as the argument of {@link StringUtil#jsStringEnc(String, JsStringEncCompatibility, JsStringEncQuotation)}.
+ *
+ * @since 2.3.32
+ */
+ public enum JsStringEncQuotation {
+ QUOTATION_MARK('"'),
+ APOSTROPHE('\'');
+
+ private final char symbol;
+
+ JsStringEncQuotation(char symbol) {
+ this.symbol = symbol;
+ }
+
+ public char getSymbol() {
+ return symbol;
+ }
+ }
}
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index ed9ea259..b4288ced 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -30211,6 +30211,14 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
mode.</para>
</listitem>
+ <listitem>
+ <para>Improved <literal>StringUtil.jsStringEnc</literal> and
+ <literal>javaSctringEsc</literal> to support quoting. Also
+ <literal>jsStringEnc</literal> now have a mode that targets both
+ JavaScript and JSON, and doesn't give up apostrophe
+ escaping.</para>
+ </listitem>
+
<listitem>
<para><link
xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-198">FREEMARKER-198</link>:
diff --git a/src/test/java/freemarker/template/utility/StringUtilTest.java b/src/test/java/freemarker/template/utility/StringUtilTest.java
index 0bb29345..1614dc0d 100644
--- a/src/test/java/freemarker/template/utility/StringUtilTest.java
+++ b/src/test/java/freemarker/template/utility/StringUtilTest.java
@@ -19,6 +19,8 @@
package freemarker.template.utility;
+import static freemarker.template.utility.StringUtil.JsStringEncCompatibility.*;
+import static freemarker.template.utility.StringUtil.JsStringEncQuotation.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
@@ -30,6 +32,7 @@ import org.hamcrest.Matchers;
import org.junit.Test;
import freemarker.core.ParseException;
+import freemarker.template.utility.StringUtil.JsStringEncCompatibility;
public class StringUtilTest {
@@ -449,5 +452,77 @@ public class StringUtilTest {
StringUtil.RTFEnc(in, sw);
assertEquals(expected, sw.toString());
}
-
+
+ @Test
+ public void jsStringEncQuotationTests() {
+ for (JsStringEncCompatibility anyCompatibility : JsStringEncCompatibility.values()) {
+ JsStringEncCompatibility anyJsonCompatible = anyCompatibility.isJSONCompatible() ? anyCompatibility : JSON;
+
+ assertEquals("", StringUtil.jsStringEnc("", anyCompatibility, null));
+ assertEquals("''", StringUtil.jsStringEnc("", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"\"", StringUtil.jsStringEnc("", anyCompatibility, QUOTATION_MARK));
+
+ assertEquals("a", StringUtil.jsStringEnc("a", anyCompatibility, null));
+ assertEquals("a", StringUtil.jsStringEnc("a", anyCompatibility, null));
+ assertEquals("'a'", StringUtil.jsStringEnc("a", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"a\"", StringUtil.jsStringEnc("a", anyCompatibility, QUOTATION_MARK));
+
+ try {
+ StringUtil.jsStringEnc("", anyJsonCompatible, APOSTROPHE);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ assertEquals("a\\'b", StringUtil.jsStringEnc("a'b", JAVA_SCRIPT, null));
+ assertEquals("a'b", StringUtil.jsStringEnc("a'b", JSON, null));
+ assertEquals("a\\u0027b", StringUtil.jsStringEnc("a'b", JAVA_SCRIPT_OR_JSON, null));
+ assertEquals("'a\\'b'", StringUtil.jsStringEnc("a'b", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"a'b\"", StringUtil.jsStringEnc("a'b", anyCompatibility, QUOTATION_MARK));
+
+ assertEquals("<\\/e>", StringUtil.jsStringEnc("</e>", anyCompatibility, null));
+ assertEquals("'<\\/e>'", StringUtil.jsStringEnc("</e>", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"<\\/e>\"", StringUtil.jsStringEnc("</e>", anyCompatibility, QUOTATION_MARK));
+
+ assertEquals("\\/e>", StringUtil.jsStringEnc("/e>", anyCompatibility, null));
+ assertEquals("'/e>'", StringUtil.jsStringEnc("/e>", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"/e>\"", StringUtil.jsStringEnc("/e>", anyCompatibility, QUOTATION_MARK));
+
+ assertEquals("\\>", StringUtil.jsStringEnc(">", JAVA_SCRIPT, null));
+ assertEquals("\\u003E", StringUtil.jsStringEnc(">", JSON, null));
+ assertEquals("'>'", StringUtil.jsStringEnc(">", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\">\"", StringUtil.jsStringEnc(">", anyCompatibility, QUOTATION_MARK));
+
+ assertEquals("-\\>", StringUtil.jsStringEnc("->", JAVA_SCRIPT, null));
+ assertEquals("-\\u003E", StringUtil.jsStringEnc("->", JSON, null));
+ assertEquals("'->'", StringUtil.jsStringEnc("->", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"->\"", StringUtil.jsStringEnc("->", JAVA_SCRIPT, QUOTATION_MARK));
+
+ assertEquals("--\\>", StringUtil.jsStringEnc("-->", JAVA_SCRIPT, null));
+ assertEquals("--\\u003E", StringUtil.jsStringEnc("-->", anyJsonCompatible, null));
+ assertEquals("'--\\>'", StringUtil.jsStringEnc("-->", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"--\\>\"", StringUtil.jsStringEnc("-->", JAVA_SCRIPT, QUOTATION_MARK));
+ assertEquals("\"--\\u003E\"", StringUtil.jsStringEnc("-->", anyJsonCompatible, QUOTATION_MARK));
+
+ assertEquals("x->", StringUtil.jsStringEnc("x->", anyCompatibility, null));
+ assertEquals("'x->'", StringUtil.jsStringEnc("x->", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"x->\"", StringUtil.jsStringEnc("x->", anyCompatibility, QUOTATION_MARK));
+
+ assertEquals("\\x3C", StringUtil.jsStringEnc("<", JAVA_SCRIPT, null));
+ assertEquals("\\u003C", StringUtil.jsStringEnc("<", JSON, null));
+ assertEquals("'<'", StringUtil.jsStringEnc("<", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"<\"", StringUtil.jsStringEnc("<", anyCompatibility, QUOTATION_MARK));
+
+ assertEquals("\\x3C!", StringUtil.jsStringEnc("<!", JAVA_SCRIPT, null));
+ assertEquals("\\u003C!", StringUtil.jsStringEnc("<!", JSON, null));
+ assertEquals("'\\x3C!'", StringUtil.jsStringEnc("<!", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"\\x3C!\"", StringUtil.jsStringEnc("<!", JAVA_SCRIPT, QUOTATION_MARK));
+ assertEquals("\"\\u003C!\"", StringUtil.jsStringEnc("<!", anyJsonCompatible, QUOTATION_MARK));
+
+ assertEquals("<x", StringUtil.jsStringEnc("<x", anyCompatibility, null));
+ assertEquals("'<x'", StringUtil.jsStringEnc("<x", JAVA_SCRIPT, APOSTROPHE));
+ assertEquals("\"<x\"", StringUtil.jsStringEnc("<x", anyCompatibility, QUOTATION_MARK));
+ }
+ }
+
}