Merge pull request #88 from nolaviz/nolaviz-devel-forceautoesc

Support "forced" auto-escaping policy.
diff --git a/src/main/java/freemarker/core/BuiltInBannedWhenForcedAutoEscaping.java b/src/main/java/freemarker/core/BuiltInBannedWhenForcedAutoEscaping.java
new file mode 100644
index 0000000..c877fa8
--- /dev/null
+++ b/src/main/java/freemarker/core/BuiltInBannedWhenForcedAutoEscaping.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 freemarker.core;
+
+/**
+ * A built-in whose usage is banned when auto-escaping is in the "forced" state.
+ * This is just a marker; the actual checking is in {@code FTL.jj}. 
+ */
+interface BuiltInBannedWhenForcedAutoEscaping {}
diff --git a/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java b/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java
index b4b9fe8..7397924 100644
--- a/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java
+++ b/src/main/java/freemarker/core/BuiltInsForOutputFormatRelated.java
@@ -23,7 +23,7 @@
 
 class BuiltInsForOutputFormatRelated {
 
-    static class no_escBI extends AbstractConverterBI {
+    static class no_escBI extends AbstractConverterBI implements BuiltInBannedWhenForcedAutoEscaping {
 
         @Override
         protected TemplateModel calculateResult(String lho, MarkupOutputFormat outputFormat, Environment env)
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 1056b26..4471349 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -2394,7 +2394,8 @@
      *       <br>String value: {@code "enable_if_default"} or {@code "enableIfDefault"} for
      *       {@link Configuration#ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY},
      *       {@code "enable_if_supported"} or {@code "enableIfSupported"} for
-     *       {@link Configuration#ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY}
+     *       {@link Configuration#ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY},
+     *       {@code "force" for {@link Configuration#FORCE_AUTO_ESCAPING_POLICY}, or
      *       {@code "disable"} for {@link Configuration#DISABLE_AUTO_ESCAPING_POLICY}.
      *       
      *   <li><p>{@code "default_encoding"}:
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index a89bb59..c92fbf7 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -437,7 +437,9 @@
     public static final int ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY = 21;
     /** Enable auto-escaping if the {@link OutputFormat} supports it. */
     public static final int ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY = 22;
-    
+    /** Require auto-escaping always. */
+    public static final int FORCE_AUTO_ESCAPING_POLICY = 23;
+
     /** FreeMarker version 2.3.0 (an {@link #Configuration(Version) incompatible improvements break-point}) */
     public static final Version VERSION_2_3_0 = new Version(2, 3, 0);
     
@@ -2174,7 +2176,8 @@
      * 
      * @param autoEscapingPolicy
      *          One of the {@link #ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY},
-     *          {@link #ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY}, and {@link #DISABLE_AUTO_ESCAPING_POLICY} constants.  
+     *          {@link #ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY}, {@link #DISABLE_AUTO_ESCAPING_POLICY}, and
+     *          {@link #FORCE_AUTO_ESCAPING_POLICY} constants.  
      * 
      * @see TemplateConfiguration#setAutoEscapingPolicy(int)
      * @see Configuration#setOutputFormat(OutputFormat)
@@ -3405,6 +3408,8 @@
                     setAutoEscapingPolicy(ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY);
                 } else if ("enable_if_supported".equals(value) || "enableIfSupported".equals(value)) {
                     setAutoEscapingPolicy(ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY);
+                } else if ("force".equals(value)) {
+                    setAutoEscapingPolicy(FORCE_AUTO_ESCAPING_POLICY);
                 } else if ("disable".equals(value)) {
                     setAutoEscapingPolicy(DISABLE_AUTO_ESCAPING_POLICY);
                 } else {
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 8fa3250..d1f61fb 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -140,10 +140,12 @@
     public static void validateAutoEscapingPolicyValue(int autoEscaping) {
         if (autoEscaping != Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY
                 && autoEscaping != Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY
+                && autoEscaping != Configuration.FORCE_AUTO_ESCAPING_POLICY
                 && autoEscaping != Configuration.DISABLE_AUTO_ESCAPING_POLICY) {
             throw new IllegalArgumentException("\"auto_escaping\" can only be set to one of these: "
                     + "Configuration.ENABLE_AUTO_ESCAPING_IF_DEFAULT, "
                     + "or Configuration.ENABLE_AUTO_ESCAPING_IF_SUPPORTED"
+                    + "or Configuration.FORCE_AUTO_ESCAPING_POLICY"
                     + "or Configuration.DISABLE_AUTO_ESCAPING");
         }
     }
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index b13069f..a744727 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -237,6 +237,11 @@
                 outputFormat = outputFormatFromExt;
             }
         }
+	if (!(outputFormat instanceof MarkupOutputFormat)
+	    && autoEscapingPolicy == Configuration.FORCE_AUTO_ESCAPING_POLICY) {
+	    throw new IllegalArgumentException(
+                    "Non-markup output format cannot be used when auto_escaping_policy is FORCE_AUTO_ESCAPING_POLICY.");
+	}
         recalculateAutoEscapingField();
 
         token_source.setParser(this);
@@ -358,7 +363,8 @@
         if (outputFormat instanceof MarkupOutputFormat) {
             if (autoEscapingPolicy == Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY) {
                 autoEscaping = ((MarkupOutputFormat) outputFormat).isAutoEscapedByDefault();
-            } else if (autoEscapingPolicy == Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY) {
+            } else if (autoEscapingPolicy == Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY
+	        || autoEscapingPolicy == Configuration.FORCE_AUTO_ESCAPING_POLICY) {
                 autoEscaping = true;
             } else if (autoEscapingPolicy == Configuration.DISABLE_AUTO_ESCAPING_POLICY) {
                 autoEscaping = false;
@@ -2214,16 +2220,24 @@
         }
         
         if (result instanceof BuiltInBannedWhenAutoEscaping) {
-	        if (outputFormat instanceof MarkupOutputFormat && autoEscaping) {
-	            throw new ParseException(
-	                    "Using ?" + t.image + " (legacy escaping) is not allowed when auto-escaping is on with "
-	                    + "a markup output format (" + outputFormat.getName() + "), to avoid double-escaping mistakes.",
-	                    template, t);
-	        }
+	    if (outputFormat instanceof MarkupOutputFormat && autoEscaping) {
+	        throw new ParseException(
+	                "Using ?" + t.image + " (legacy escaping) is not allowed when auto-escaping is on with "
+	                + "a markup output format (" + outputFormat.getName() + "), to avoid double-escaping mistakes.",
+	                template, t);
+	    }
             
             return result;
         }
 
+	if (result instanceof BuiltInBannedWhenForcedAutoEscaping) {
+	    if (autoEscapingPolicy == Configuration.FORCE_AUTO_ESCAPING_POLICY) {
+	        throw new ParseException(
+		        "Using ?" + t.image + " is not allowed while auto_escaping_policy is FORCE_AUTO_ESCAPING_POLICY.",
+			template, t);
+	    }
+	}
+
         if (result instanceof MarkupOutputFormatBoundBuiltIn) {
             if (!(outputFormat instanceof MarkupOutputFormat)) {
                 throw new ParseException(
@@ -4046,6 +4060,12 @@
             } else {
                 outputFormat = template.getConfiguration().getOutputFormat(paramStr);
             }
+	    if (!(outputFormat instanceof MarkupOutputFormat)
+	    	&& autoEscapingPolicy == Configuration.FORCE_AUTO_ESCAPING_POLICY) {
+	        throw new ParseException(
+			"Non-markup output format cannot be used when auto_escaping_policy is FORCE_AUTO_ESCAPING_POLICY.",
+			template, start);
+	    }
             recalculateAutoEscapingField();
         } catch (IllegalArgumentException e) {
             throw new ParseException("Invalid format name: " + e.getMessage(), template, start, e.getCause());
@@ -4100,6 +4120,11 @@
 {
     start = <NOAUTOESC>
     {
+	if (autoEscapingPolicy == Configuration.FORCE_AUTO_ESCAPING_POLICY) {
+	    throw new ParseException(
+	    	"<#noautoesc> cannot be used when auto_escaping_policy is FORCE_AUTO_ESCAPING_POLICY.",
+		template, start);
+	}
         lastAutoEscapingPolicy = autoEscapingPolicy;
         autoEscapingPolicy = Configuration.DISABLE_AUTO_ESCAPING_POLICY;
         recalculateAutoEscapingField();
@@ -4539,6 +4564,11 @@
                                 autoEscRequester = key;
                                 autoEscapingPolicy = Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY;
                             } else {
+				if (autoEscapingPolicy == Configuration.FORCE_AUTO_ESCAPING_POLICY) {
+				    throw new ParseException(
+				            "auto_esc setting cannot be used when auto_escaping_policy is FORCE_AUTO_ESCAPING_POLICY.",
+					    exp);
+				}
                                 autoEscapingPolicy = Configuration.DISABLE_AUTO_ESCAPING_POLICY;
                             }
                             recalculateAutoEscapingField();
diff --git a/src/test/java/freemarker/core/OutputFormatTest.java b/src/test/java/freemarker/core/OutputFormatTest.java
index 3e3207d..14d3ecf 100644
--- a/src/test/java/freemarker/core/OutputFormatTest.java
+++ b/src/test/java/freemarker/core/OutputFormatTest.java
@@ -851,7 +851,29 @@
                     + dExpted);
         }
     }
-    
+
+    @Test
+    public void testForcedAutoEsc() throws Exception {
+        Configuration cfg = getConfiguration();
+        cfg.setRegisteredCustomOutputFormats(ImmutableList.of(
+                SeldomEscapedOutputFormat.INSTANCE, DummyOutputFormat.INSTANCE));
+        cfg.setAutoEscapingPolicy(Configuration.FORCE_AUTO_ESCAPING_POLICY);
+
+        String commonFTL = "${'.'} ${.autoEsc?c}";
+        String esced = "\\. true";
+
+        cfg.setOutputFormat(SeldomEscapedOutputFormat.INSTANCE);
+        assertOutput(commonFTL, esced);
+
+        cfg.setOutputFormat(DummyOutputFormat.INSTANCE);
+        assertOutput(commonFTL, esced);
+
+        cfg.setOutputFormat(DummyOutputFormat.INSTANCE);
+        assertErrorContains("<#outputformat 'plainText'></#outputformat>", "auto_escaping_policy is FORCE_AUTO_ESCAPING_POLICY");
+        assertErrorContains("<#noAutoEsc></#noAutoEsc>", "auto_escaping_policy is FORCE_AUTO_ESCAPING_POLICY");
+        assertErrorContains("<#assign foo='bar'>${foo?noEsc}", "auto_escaping_policy is FORCE_AUTO_ESCAPING_POLICY");
+    }
+
     @Test
     public void testDynamicParsingBIsInherticContextOutputFormat() throws Exception {
         // Dynamic parser BI-s are supposed to use the parserConfiguration of the calling template, and ignore anything