This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git


The following commit(s) were added to refs/heads/3 by this push:
     new c178512  Forward ported from 2.3-gae: MemberAccessPolicy now also 
covers the special case when toString() is called to convert and object to 
string in a template. This was added as toString() might shows information that 
you don't want to be exposed.
c178512 is described below

commit c1785120c32fb77dd16e94b9bb91ccc4fd6e9f8a
Author: ddekany <[email protected]>
AuthorDate: Tue Jan 14 23:13:54 2020 +0100

    Forward ported from 2.3-gae: MemberAccessPolicy now also covers the special 
case when toString() is called to convert and object to string in a template. 
This was added as toString() might shows information that you don't want to be 
exposed.
---
 .../model/impl/DefaultMemberAccessPolicyTest.java  |  6 ++
 ...DefaultObjectWrapperMemberAccessPolicyTest.java | 75 ++++++++++++++++++++++
 .../MemberSelectorListMemberAccessPolicyTest.java  | 45 ++++++++++++-
 .../model/impl/AllowAllMemberAccessPolicy.java     |  5 ++
 .../core/model/impl/BeanAndStringModel.java        |  9 ++-
 .../model/impl/BlacklistMemberAccessPolicy.java    | 18 ++++++
 .../core/model/impl/ClassIntrospector.java         | 36 +++++++++--
 .../core/model/impl/DefaultMemberAccessPolicy.java | 10 +++
 .../core/model/impl/MemberAccessPolicy.java        | 10 +++
 .../freemarker/core/model/impl/StaticModel.java    |  2 +-
 .../model/impl/WhitelistMemberAccessPolicy.java    | 18 ++++++
 11 files changed, 225 insertions(+), 9 deletions(-)

diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultMemberAccessPolicyTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultMemberAccessPolicyTest.java
index 42cd905..5993071 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultMemberAccessPolicyTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultMemberAccessPolicyTest.java
@@ -81,6 +81,12 @@ public class DefaultMemberAccessPolicyTest {
     }
 
     @Test
+    public void testToString() throws NoSuchMethodException {
+        assertTrue(POLICY.isToStringAlwaysExposed());
+        
assertTrue(POLICY.forClass(UserClass.class).isMethodExposed(Object.class.getMethod("toString")));
+    }
+
+    @Test
     public void testWellKnownUnsafeMethodsAreBanned() throws 
NoSuchMethodException {
         {
             ClassMemberAccessPolicy classPolicy = POLICY.forClass(Class.class);
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperMemberAccessPolicyTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperMemberAccessPolicyTest.java
index 39c3030..529fd84 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperMemberAccessPolicyTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperMemberAccessPolicyTest.java
@@ -27,6 +27,7 @@ import java.io.StringWriter;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.Collections;
 import java.util.Map;
 
 import org.apache.freemarker.core.Configuration;
@@ -37,6 +38,7 @@ import org.apache.freemarker.core.model.TemplateFunctionModel;
 import org.apache.freemarker.core.model.TemplateHashModel;
 import org.apache.freemarker.core.model.TemplateModel;
 import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateStringModel;
 import org.junit.Test;
 
 import com.google.common.collect.ImmutableMap;
@@ -243,6 +245,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                     }
                 };
             }
+
+            @Override
+            public boolean isToStringAlwaysExposed() {
+                return true;
+            }
         });
         DefaultObjectWrapper ow = owb.build();
 
@@ -286,6 +293,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                     }
                 };
             }
+
+            @Override
+            public boolean isToStringAlwaysExposed() {
+                return true;
+            }
         });
         DefaultObjectWrapper ow = owb.build();
 
@@ -319,6 +331,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                     }
                 };
             }
+
+            @Override
+            public boolean isToStringAlwaysExposed() {
+                return true;
+            }
         });
         DefaultObjectWrapper ow = owb.build();
 
@@ -350,6 +367,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                     }
                 };
             }
+
+            @Override
+            public boolean isToStringAlwaysExposed() {
+                return true;
+            }
         });
         DefaultObjectWrapper ow = owb.build();
 
@@ -398,6 +420,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                     }
                 };
             }
+
+            @Override
+            public boolean isToStringAlwaysExposed() {
+                return true;
+            }
         });
         DefaultObjectWrapper ow = owb.build();
 
@@ -449,6 +476,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                             }
                         };
                     }
+
+                    @Override
+                    public boolean isToStringAlwaysExposed() {
+                        return true;
+                    }
                 })
                 .build();
 
@@ -503,6 +535,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                             }
                         };
                     }
+
+                    @Override
+                    public boolean isToStringAlwaysExposed() {
+                        return true;
+                    }
                 })
                 .build();
         TemplateHashModel statics = (TemplateHashModel) 
ow.getStaticModels().get(Statics.class.getName());
@@ -548,6 +585,34 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
         }
     }
 
+    @Test
+    public void testToString1()
+            throws TemplateException, NoSuchMethodException, 
NoSuchFieldException, ClassNotFoundException {
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                .memberAccessPolicy(
+                        new WhitelistMemberAccessPolicy(
+                                
MemberSelectorListMemberAccessPolicy.MemberSelector.parse(
+                                        
Collections.singleton(CExtended.class.getName() + ".toString()"),
+                                        false,
+                                        
DefaultObjectWrapperMemberAccessPolicyTest.class.getClassLoader()
+                                )
+                        )
+                )
+                .build();
+
+        assertEquals(BeanAndStringModel.TO_STRING_NOT_EXPOSED, 
((TemplateStringModel) ow.wrap(new C())).getAsString());
+        assertEquals(CExtended.class.getSimpleName(), ((TemplateStringModel) 
ow.wrap(new CExtended())).getAsString());
+    }
+
+    @Test
+    public void testToString2() throws TemplateException {
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                    
.memberAccessPolicy(DefaultMemberAccessPolicy.getInstance(Configuration.VERSION_3_0_0))
+                    .build();
+
+        assertEquals(C.class.getSimpleName(), ((TemplateStringModel) 
ow.wrap(new C())).getAsString());
+    }
+
     private static Object getHashValue(ObjectWrapperAndUnwrapper ow, 
TemplateHashModel objM, String key)
             throws TemplateException {
         return ow.unwrap(objM.get(key));
@@ -618,6 +683,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
         }
 
         public static void M1() { }
+
+        @Override
+        public String toString() {
+            return this.getClass().getSimpleName();
+        }
     }
 
     public static class CExtended extends C {
@@ -699,6 +769,11 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                 }
             };
         }
+
+        @Override
+        public boolean isToStringAlwaysExposed() {
+            return false;
+        }
     }
 }
 
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MemberSelectorListMemberAccessPolicyTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MemberSelectorListMemberAccessPolicyTest.java
index b8aeedd..9c7d92f 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MemberSelectorListMemberAccessPolicyTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MemberSelectorListMemberAccessPolicyTest.java
@@ -314,7 +314,7 @@ public class MemberSelectorListMemberAccessPolicyTest {
     }
 
     @Test
-    public void testBlacklistIgnoredAnnotation() throws NoSuchMethodException, 
NoSuchFieldException {
+    public void testBlacklistIgnoresAnnotation() throws NoSuchMethodException, 
NoSuchFieldException {
         BlacklistMemberAccessPolicy policy = newBlacklistMemberAccessPolicy(
                 CAnnotationsTest1.class.getName() + ".m5()",
                 CAnnotationsTest1.class.getName() + ".f5",
@@ -328,6 +328,49 @@ public class MemberSelectorListMemberAccessPolicyTest {
     }
 
     @Test
+    public void testBlacklistAndToString() throws NoSuchMethodException {
+        {
+            BlacklistMemberAccessPolicy policy = 
newBlacklistMemberAccessPolicy(
+                    C1.class.getName() + ".m1()",
+                    C1.class.getName() + ".m2()"
+            );
+            assertTrue(policy.isToStringAlwaysExposed());
+            
assertTrue(policy.forClass(C1.class).isMethodExposed(Object.class.getMethod("toString")));
+        }
+        {
+            BlacklistMemberAccessPolicy policy = 
newBlacklistMemberAccessPolicy(
+                    C1.class.getName() + ".m1()",
+                    C2.class.getName() + ".toString()",
+                    C1.class.getName() + ".m2()"
+            );
+            assertFalse(policy.isToStringAlwaysExposed());
+            
assertTrue(policy.forClass(C1.class).isMethodExposed(Object.class.getMethod("toString")));
+            
assertFalse(policy.forClass(C2.class).isMethodExposed(Object.class.getMethod("toString")));
+            
assertFalse(policy.forClass(C3.class).isMethodExposed(Object.class.getMethod("toString")));
+        }
+    }
+
+    @Test
+    public void testWhitelistAndToString() throws NoSuchMethodException {
+        {
+            WhitelistMemberAccessPolicy policy = 
newWhitelistMemberAccessPolicy(
+                    C2.class.getName() + ".toString()"
+            );
+            assertFalse(policy.isToStringAlwaysExposed());
+            
assertFalse(policy.forClass(C1.class).isMethodExposed(Object.class.getMethod("toString")));
+            
assertTrue(policy.forClass(C2.class).isMethodExposed(Object.class.getMethod("toString")));
+            
assertTrue(policy.forClass(C3.class).isMethodExposed(Object.class.getMethod("toString")));
+        }
+        {
+            WhitelistMemberAccessPolicy policy = 
newWhitelistMemberAccessPolicy(
+                    Object.class.getName() + ".toString()"
+            );
+            assertTrue(policy.isToStringAlwaysExposed());
+            
assertTrue(policy.forClass(C1.class).isMethodExposed(Object.class.getMethod("toString")));
+        }
+    }
+
+    @Test
     public void memberSelectorParserIgnoresWhitespace() throws 
NoSuchMethodException {
         WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
                 (CArrayArgs.class.getName() + 
".m1(java.lang.String)").replace(".", "\n\t. "),
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/AllowAllMemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/AllowAllMemberAccessPolicy.java
index f96838b..3e5d747 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/AllowAllMemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/AllowAllMemberAccessPolicy.java
@@ -50,4 +50,9 @@ final class AllowAllMemberAccessPolicy implements 
MemberAccessPolicy {
     public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
         return CLASS_POLICY_INSTANCE;
     }
+
+    @Override
+    public boolean isToStringAlwaysExposed() {
+        return true;
+    }
 }
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
index 4fb931a..52c140c 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java
@@ -41,12 +41,19 @@ public class BeanAndStringModel extends BeanModel 
implements TemplateStringModel
         super(object, wrapper);
     }
 
+    // Package visible for testing
+    static final String TO_STRING_NOT_EXPOSED = "[toString not exposed]";
+
     /**
      * Returns the result of calling {@link Object#toString()} on the wrapped
      * object.
      */
     @Override
     public String getAsString() {
-        return object.toString();
+        boolean exposeToString = 
wrapper.getMemberAccessPolicy().isToStringAlwaysExposed()
+                || !wrapper.getClassIntrospector().get(object.getClass())
+                        
.containsKey(ClassIntrospector.TO_STRING_HIDDEN_FLAG_KEY);
+        return exposeToString ? object.toString() : TO_STRING_NOT_EXPOSED;
     }
+
 }
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BlacklistMemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BlacklistMemberAccessPolicy.java
index a7dc07f..8e465e1 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BlacklistMemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BlacklistMemberAccessPolicy.java
@@ -19,6 +19,7 @@
 
 package org.apache.freemarker.core.model.impl;
 
+import java.lang.reflect.Method;
 import java.util.Collection;
 
 /**
@@ -36,6 +37,8 @@ import java.util.Collection;
  */
 public class BlacklistMemberAccessPolicy extends 
MemberSelectorListMemberAccessPolicy {
 
+    private final boolean toStringAlwaysExposed;
+
     /**
      * @param memberSelectors
      *      List of member selectors; see {@link 
MemberSelectorListMemberAccessPolicy} class-level documentation for
@@ -43,6 +46,21 @@ public class BlacklistMemberAccessPolicy extends 
MemberSelectorListMemberAccessP
      */
     public BlacklistMemberAccessPolicy(Collection<? extends MemberSelector> 
memberSelectors) {
         super(memberSelectors, ListType.BLACKLIST, null);
+
+        boolean toStringBlacklistedAnywhere = false;
+        for (MemberSelector memberSelector : memberSelectors) {
+            Method method = memberSelector.getMethod();
+            if (method != null && method.getName().equals("toString") && 
method.getParameterTypes().length == 0) {
+                toStringBlacklistedAnywhere = true;
+                break;
+            }
+        }
+        toStringAlwaysExposed = !toStringBlacklistedAnywhere;
+    }
+
+    @Override
+    public boolean isToStringAlwaysExposed() {
+        return toStringAlwaysExposed;
     }
 
 }
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
index b1650ff..dcb48c8 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
@@ -82,6 +82,8 @@ class ClassIntrospector {
             new ExecutableMemberSignature("get", new Class[] { String.class });
     private static final ExecutableMemberSignature GET_OBJECT_SIGNATURE =
             new ExecutableMemberSignature("get", new Class[] { Object.class });
+    private static final ExecutableMemberSignature TO_STRING_SIGNATURE =
+            new ExecutableMemberSignature("toString", new Class[0]);
 
     private static final ClassChangeNotifier CLASS_CHANGE_NOTIFIER;
     static {
@@ -129,6 +131,8 @@ class ClassIntrospector {
     static final Object CONSTRUCTORS_KEY = new Object();
     /** Key in the class info Map to the get(String|Object) Method */
     static final Object GENERIC_GET_KEY = new Object();
+    /** Key in the class info Map to the toString() Method */
+    static final Object TO_STRING_HIDDEN_FLAG_KEY = new Object();
 
     // 
-----------------------------------------------------------------------------------------------------------------
     // Introspection configuration properties:
@@ -256,7 +260,8 @@ class ClassIntrospector {
      */
     private Map<Object, Object> createClassIntrospectionData(Class<?> clazz) {
         final Map<Object, Object> introspData = new HashMap<>();
-        ClassMemberAccessPolicy effClassMemberAccessPolicy = 
getEffectiveClassMemberAccessPolicy(clazz);
+        MemberAccessPolicy effMemberAccessPolicy = 
getEffectiveMemberAccessPolicy();
+        ClassMemberAccessPolicy effClassMemberAccessPolicy = 
effMemberAccessPolicy.forClass(clazz);
 
         if (exposeFields) {
             addFieldsToClassIntrospectionData(introspData, clazz, 
effClassMemberAccessPolicy);
@@ -264,6 +269,10 @@ class ClassIntrospector {
 
         final Map<ExecutableMemberSignature, List<Method>> accessibleMethods = 
discoverAccessibleMethods(clazz);
 
+        if (!effMemberAccessPolicy.isToStringAlwaysExposed()) {
+            addToStringHiddenFlagToClassIntrospectionData(introspData, 
accessibleMethods, effClassMemberAccessPolicy);
+        }
+
         addGenericGetToClassIntrospectionData(introspData, accessibleMethods, 
effClassMemberAccessPolicy);
 
         if (exposureLevel != DefaultObjectWrapper.EXPOSE_NOTHING) {
@@ -668,6 +677,19 @@ class ClassIntrospector {
         }
     }
 
+    private void addToStringHiddenFlagToClassIntrospectionData(Map<Object, 
Object> introspData,
+            Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+            ClassMemberAccessPolicy effClassMemberAccessPolicy) {
+        Method toStringMethod = getFirstAccessibleMethod(TO_STRING_SIGNATURE, 
accessibleMethods);
+        if (toStringMethod == null) {
+            throw new BugException("toString() method not found");
+        }
+        // toString() is pretty much always exposed, so we make the negative 
case to take extra memory:
+        if (!effClassMemberAccessPolicy.isMethodExposed(toStringMethod)) {
+            introspData.put(TO_STRING_HIDDEN_FLAG_KEY, true);
+        }
+    }
+
     private void addConstructorsToClassIntrospectionData(final Map<Object, 
Object> introspData,
             Class<?> clazz, ClassMemberAccessPolicy 
effClassMemberAccessPolicy) {
         try {
@@ -788,12 +810,14 @@ class ClassIntrospector {
     }
 
     /**
-     * Returns the {@link ClassMemberAccessPolicy} to actually use, which is 
not just
-     * {@link DefaultObjectWrapper#getMemberAccessPolicy()} if {@link 
DefaultObjectWrapper#getExposureLevel()} is more allowing than
-     * {@link DefaultObjectWrapper#EXPOSE_SAFE}. {@link 
DefaultObjectWrapper#EXPOSE_NOTHING} though is not factored in here.
+     * Returns the {@link MemberAccessPolicy} to actually use, which is not 
just
+     * {@link DefaultObjectWrapper#getMemberAccessPolicy()} if {@link 
DefaultObjectWrapper#getExposureLevel()} is more
+     * allowing than {@link DefaultObjectWrapper#EXPOSE_SAFE}. {@link 
DefaultObjectWrapper#EXPOSE_NOTHING} though is
+     * not factored in here.
      */
-    ClassMemberAccessPolicy getEffectiveClassMemberAccessPolicy(Class<?> 
containingClass) {
-        return exposureLevel < DefaultObjectWrapper.EXPOSE_SAFE ? 
AllowAllMemberAccessPolicy.CLASS_POLICY_INSTANCE : 
memberAccessPolicy.forClass(containingClass);
+    MemberAccessPolicy getEffectiveMemberAccessPolicy() {
+        return exposureLevel < DefaultObjectWrapper.EXPOSE_SAFE ? 
AllowAllMemberAccessPolicy.INSTANCE
+                : memberAccessPolicy;
     }
 
     private static Map<Method, Class<?>[]> getArgTypesByMethod(Map<Object, 
Object> classInfo) {
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultMemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultMemberAccessPolicy.java
index 57659eb..faef934 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultMemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultMemberAccessPolicy.java
@@ -48,6 +48,7 @@ public final class DefaultMemberAccessPolicy implements 
MemberAccessPolicy {
     private final Set<Class<?>> whitelistRuleNonFinalClasses;
     private final WhitelistMemberAccessPolicy whitelistMemberAccessPolicy;
     private final BlacklistMemberAccessPolicy blacklistMemberAccessPolicy;
+    private final boolean toStringAlwaysExposed;
 
     /**
      * Returns the singleton that's compatible with the given incompatible 
improvements version.
@@ -145,6 +146,10 @@ public final class DefaultMemberAccessPolicy implements 
MemberAccessPolicy {
                 }
             }
             blacklistMemberAccessPolicy = new 
BlacklistMemberAccessPolicy(blacklistMemberSelectors);
+
+            toStringAlwaysExposed =
+                    whitelistMemberAccessPolicy.isToStringAlwaysExposed()
+                    && blacklistMemberAccessPolicy.isToStringAlwaysExposed();
         } catch (Exception e) {
             throw new IllegalStateException("Couldn't init " + 
this.getClass().getName() + " instance", e);
         }
@@ -174,6 +179,11 @@ public final class DefaultMemberAccessPolicy implements 
MemberAccessPolicy {
         }
     }
 
+    @Override
+    public boolean isToStringAlwaysExposed() {
+        return toStringAlwaysExposed;
+    }
+
     private boolean isTypeWithWhitelistRule(Class<?> contextClass) {
         if (whitelistRuleFinalClasses.contains(contextClass)) {
             return true;
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
index b603c82..e4d9547 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
@@ -66,4 +66,14 @@ public interface MemberAccessPolicy {
      *      The exact class of object from which members will be get in the 
templates.
      */
     ClassMemberAccessPolicy forClass(Class<?> contextClass);
+
+    /**
+     * If this returns {@code true}, we won't invoke the probably more 
expensive lookup to figure out if
+     * {@link Object#toString()} (including its overridden variants) is 
exposed for a given object. If this returns
+     * {@code false}, then no such optimization is made. This method was 
introduced as {@link Object#toString()} is
+     * called frequently, as it's used whenever an object is converted to 
string, like printed to the output, and it's
+     * not even a reflection-based call (we just call {@link 
Object#toString()} in Java). So we try to avoid the
+     * overhead of a more generic method call.
+     */
+    boolean isToStringAlwaysExposed();
 }
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
index e5e0e9d..fad96e8 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
@@ -119,7 +119,7 @@ final class StaticModel implements TemplateHashModelEx {
         }
 
         ClassMemberAccessPolicy effClassMemberAccessPolicy =
-                
wrapper.getClassIntrospector().getEffectiveClassMemberAccessPolicy(clazz);
+                
wrapper.getClassIntrospector().getEffectiveMemberAccessPolicy().forClass(clazz);
 
         Field[] fields = clazz.getFields();
         for (Field field : fields) {
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicy.java
index ecd6c77..ffacad0 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicy.java
@@ -19,6 +19,7 @@
 
 package org.apache.freemarker.core.model.impl;
 
+import java.lang.reflect.Method;
 import java.util.Collection;
 
 import org.apache.freemarker.core.model.ObjectWrapper;
@@ -40,6 +41,17 @@ import org.apache.freemarker.core.model.ObjectWrapper;
  */
 public class WhitelistMemberAccessPolicy extends 
MemberSelectorListMemberAccessPolicy {
 
+    private static final Method TO_STRING_METHOD;
+    static {
+        try {
+            TO_STRING_METHOD = Object.class.getMethod("toString");
+        } catch (NoSuchMethodException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private final boolean toStringAlwaysExposed;
+
     /**
      * @param memberSelectors
      *      List of member selectors; see {@link 
MemberSelectorListMemberAccessPolicy} class-level documentation for
@@ -47,6 +59,12 @@ public class WhitelistMemberAccessPolicy extends 
MemberSelectorListMemberAccessP
      */
     public WhitelistMemberAccessPolicy(Collection<? extends MemberSelector> 
memberSelectors) {
         super(memberSelectors, ListType.WHITELIST, TemplateAccessible.class);
+        toStringAlwaysExposed = 
forClass(Object.class).isMethodExposed(TO_STRING_METHOD);
+    }
+
+    @Override
+    public boolean isToStringAlwaysExposed() {
+        return toStringAlwaysExposed;
     }
 
 }
\ No newline at end of file

Reply via email to